Uploading Images to Thinger.io

Hello Thinger.io Community

Since I started using Thinger.io I have wanted to include some image capture into my projects so I could visually see what is happening around the systems that I am monitoring and controlling, but I wasn’t able to find a way of doing this without setting up a separate webcam, until now. :grin:

This guide will hopefully let you also realise this useful addition to the Thinger.io capability.

The hardware that you need for this is an ESP32 Camera module. They are available from lots of different suppliers, I suggest that you go for the one that is supplied with the coms module, as shown below.

image

There are lots of websites explaining how to set-up the ESP32, here are a few that I found useful.

https://dronebotworkshop.com/esp32-cam-intro

I had already managed to capture an image but I couldn’t find a way of uploading this to the Thinger.io File Storage, however a method of using EndPoints and NodeRED was suggested by @jaimebs at Thinger.io, thanks Jaime.

The steps required to get this project to works are…

  1. Compile and load c++ into ESP32_cam.
  2. Set-up Thinger EndPoints and File Storage.
  3. Set-up nodeRED.
  4. Create dashboard to include the image.

The ESP32 c++ code is shown below, I will attempt to explain what each section is doing. NOTE: - I have extracted the relevant code from a much larger piece of work, I have tested the code and its works OK but there is a possibility that I may have left some unused variables :slightly_smiling_face:

  • First the libraries are specified and I’m defining the constant values. NOTE: - I am using Scratch for the DEVICE_ID, but you need to change this to match your device ID.

  • Next I’m setting up the variables that will be needed for controlling when an image is taken, camera & SD Card Reader setting.

  • Next the various functions are set-up.

    • readSerial is really useful and this is something that I use on the majority of my projects as it allows me to control functions and set variables while the hardware is running. I have only set-up two controls, but you can add many more.
    • connectWiFi is called from the Set-Up section and makes the WiFi connection. There are other ways of doing this but I find this works well for me.
    • Camera_config and init_camera set-up and initialise the camera. Init_camera is called from set-up. Camera_config contains some parameters that can affect the image quality, I recommend checking the information in Github regarding these settings. GitHub - espressif/esp32-camera. NOTE: - THE MAXIMUM ALLOWED PAYLOAD IS 512KB, SO BE CAREFULL NOT TO HAVE TOO LARGE A FRAMESIZE. I chose FRAMESIZE_QVGA because it was plenty for my project and each image is approximately 14kb.
    • initSD initialises the SD Card Reader.
    • saveImage saves the image to the SD Card.This is called from the next function. NOTE: - The filename is generated in the next function, but you could include it in this saveImage function. I have used the DEVICE_ID for the image name so that its easier to find and add to a dashboard.
    • imageCapture, this is where the first bit of magic happens. An image is taken and stored in memory variable pic. This pic is then saved to an SD card and uploaded to a Thinger EndPoint. The EndPoint is key and I will get to that later. NOTE: - Its vitally important that the esp_camera_fb_return(pic); function is run after finishing with the image, otherwise memory over-run will eventually occur.
  • Set-up section initialises the H/W and sets up Thinger pson’s.

  • Loop is where the image capturing is controlled. I am using time to control when an image is taken, but you could look for an external input (e.g. pressure pad or proximity sensor) to control when an image is captured.

Hopefully this explanation makes sense :slightly_smiling_face: here is the whole code.

//ThingerIO + camera
//Programmed using Visual Studio + Platformio Core

#define _DISABLE_TLS_
#define _DEBUG_ //Comment out to prevent constant debug OP on serial coms
#define THINGER_SERVER "XXXXXXX.aws.thinger.io" // Only required if host has been set up (xxxx=host name),
//https://docs.thinger.io/server-configuration/cluster-server-status

#include <ThingerESP32.h> //IOT Platform www.thinger.io
#include <ThingerESP32OTA.h> //OTA library for remote updates via VSC & Platformio
#include <esp_camera.h>
#include "FS.h"
#include "SD_MMC.h"

//Thinger Variables, change as required
#define USERNAME "XXXXX"    //Thinger Account User Name
#define DEVICE_ID "Scratch"   //Device ID, as set in Thinger.io
#define DEVICE_CREDENTIAL "XXXXX"  //Device Credential, as set in Thinger.io
ThingerESP32 thing(USERNAME, DEVICE_ID, DEVICE_CREDENTIAL);

//Thinger Endpoint definitions
#define IMAGE_ENDPOINT "ImageUpload"

//WiFi variables, change as required.
char ssid[50]     = "VM5089777"; //The SSID (name) of the Wi-Fi network you want to connect to
char password[50] = "dzt2vktrHfXs"; //The password of the Wi-Fi network

/***********************************************
**             VERSION NOTES                  **
************************************************
V00.01.00
  •Original
************************************************/
String Version = "V00.01.00";

/**********************
 ***** VARIABLES ******
 **********************/
//General
  long msTimer1 = 0;
  long msTimer2 = 0;
  int msOutput = 5000; //Output time period in ms

//Camera + SD Card
#define SD_CS_PIN 13
#define CAMERA_MODEL_AI_THINKER
#define CAM_PIN_PWDN 32
#define CAM_PIN_RESET -1 //software reset will be performed
#define CAM_PIN_XCLK 0
#define CAM_PIN_SIOD 26
#define CAM_PIN_SIOC 27
#define CAM_PIN_D7 35
#define CAM_PIN_D6 34
#define CAM_PIN_D5 39
#define CAM_PIN_D4 36
#define CAM_PIN_D3 21
#define CAM_PIN_D2 19
#define CAM_PIN_D1 18
#define CAM_PIN_D0 5
#define CAM_PIN_VSYNC 25
#define CAM_PIN_HREF 23
#define CAM_PIN_PCLK 22

camera_fb_t *pic; //variable to store camera image

bool sdCardOK = 0; //Is there an SD card? 0=NG
bool cameraOK = 0; //Is the camera OK? 0=NG

//Image save SD card
char fileName[50] = ""; // Maximum 50 characters for the file name
char imageCtStr[10]; // Maximum 10 characters for the imageCt value to append to the image file name
int imageCt = 0;

//variables used for the serial coms commands
String Input = "" ; //Holding variable for serial.read
String Com = ""; //initial character from serial.read
int Val = 0; //numeric value following the command character from serial.read
String Set = ""; //string from serial command

/**********************
 ***** FUNCTIONS ******
 **********************/
//reset ESP
void resetESP()
{
  ESP.restart();
}

/*SERIAL COMMANDS **************
 Note
 * h = Help
 * R = Reset
 *****************************************/
void readSerial(void)
{
  Com = ""; //reset command
  Input = Serial.readString(); //read the whole string
  Com = Input.substring(0,1); //extract the initial character
  Val = Input.substring(1,4).toInt(); //store the Value (3 digits) entered after the address. **WILL BE ZERO (0) IF NO VALUE ENTERED
  Set = Input.substring(1,33); //store the Data (32 characters) entered after the initial command character. **WILL BE ZERO (0) IF NO VALUE ENTERED //Change in V0.08.00
 
  if (Com.length() >0)
  { //Check that the Com string has a character in it
  //h = Help*********************    
    if (Com == "h") {
      Serial.println("****************");
      Serial.println("*****Serial Commands*****");
      Serial.println(" NOTE - CHECK CAPS LOCK");
      Serial.println("   h = Help");
      Serial.println("   R = Reset ESP");
      Serial.println("*************************");
    }      
  //R = Reset
    if (Com == "R")
    { //reset command
      Serial.println ("Reset");
      resetESP();
    }
  }
}
   
//Initialise and connect to WiFi
void connectWiFi()
{
  //Check is WiFi Password and SSID have been set
  if (strcmp(ssid, "") != 0 || strcmp(password, "") != 0)
  {
    WiFi.disconnect(); //Remove previous SSID & Password
    thing.add_wifi(ssid, password);
    Serial.print("WiFi Connecting to ");
    Serial.print(ssid); Serial.println(" ...");

    Serial.println("Connection established!");  
    Serial.print("Local WiFi IP address: ");
    Serial.println(WiFi.localIP()); // O/P the WiFi IP address
  }else{
    Serial.println ("Please set SSID & Password!");
  }
}

//Configure camera pins and image type
static camera_config_t camera_config = {
    .pin_pwdn = CAM_PIN_PWDN,
    .pin_reset = CAM_PIN_RESET,
    .pin_xclk = CAM_PIN_XCLK,
    .pin_sscb_sda = CAM_PIN_SIOD,
    .pin_sscb_scl = CAM_PIN_SIOC,
    .pin_d7 = CAM_PIN_D7,
    .pin_d6 = CAM_PIN_D6,
    .pin_d5 = CAM_PIN_D5,
    .pin_d4 = CAM_PIN_D4,
    .pin_d3 = CAM_PIN_D3,
    .pin_d2 = CAM_PIN_D2,
    .pin_d1 = CAM_PIN_D1,
    .pin_d0 = CAM_PIN_D0,
    .pin_vsync = CAM_PIN_VSYNC,
    .pin_href = CAM_PIN_HREF,
    .pin_pclk = CAM_PIN_PCLK,

    //XCLK 20MHz or 10MHz for OV2640 double FPS (Experimental)
    .xclk_freq_hz = 20000000,
    //.ledc_timer = LEDC_TIMER_0,
    //.ledc_channel = LEDC_CHANNEL_0,

    .pixel_format = PIXFORMAT_JPEG, //YUV422,GRAYSCALE,RGB565,JPEG
    .frame_size = FRAMESIZE_QVGA,
  /*
  FRAMESIZE_UXGA (1600 x 1200)
  FRAMESIZE_QVGA (320 x 240)
  FRAMESIZE_CIF (352 x 288)
  FRAMESIZE_VGA (640 x 480)
  FRAMESIZE_SVGA (800 x 600)
  FRAMESIZE_XGA (1024 x 768)
  FRAMESIZE_SXGA (1280 x 1024)
  */
    .jpeg_quality = 5, //0-63 lower number means higher quality
    .fb_count = 1,       //if more than one, i2s runs in continuous mode. Use only with JPEG
    .grab_mode = CAMERA_GRAB_LATEST, //CAMERA_GRAB_LATEST or CAMERA_GRAB_WHEN_EMPTY
};

//Initialise Camera
static esp_err_t init_camera()
{
    //initialize the camera
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK)
    {
      Serial.println ("Camera NOT Initialised");
      cameraOK = 0;
      return err;
    }else{
      Serial.println ("Camera Initialised OK");
      cameraOK = 1;
      return ESP_OK;
    }
   
}
 
//Initialise SD
void initSD(void)
{
  if (!SD_MMC.begin())
  {
    Serial.println ("SD Card NOT Initialised");
    sdCardOK = 0;
  }else{
    Serial.println ("SD Card Initialised OK");
    sdCardOK = 1;
  }
}

//Save jpeg to SD
void saveImage(fs::FS &fs, const char* path, uint8_t *fileData, size_t fileSize)
{
  fs::File file = fs.open(path, FILE_WRITE);
  if (!file)
  {
    Serial.println ("Failed to open file for writing");
    sdCardOK = 0;
  }else{
    file.write(fileData, fileSize);
    file.close();
    Serial.print("Image saved ");
    Serial.println(fileName);
    sdCardOK = 1;
    imageCt++; //Increment image counter
  }
}

// Take picture
void imageCapture()
{    
    camera_fb_t *pic = esp_camera_fb_get();
    if (!pic)
    {
      Serial.println("Camera capture failed");
      esp_camera_fb_return(pic); //Clear memory
      return;
    }else{
      Serial.println("Picture taken");
      // Generate file name
      sprintf(imageCtStr, "%d", imageCt);
      snprintf(fileName, sizeof(fileName), "/image_%s.jpg", imageCtStr);
      //Save Image to SD
      saveImage(SD_MMC, fileName, pic->buf, pic->len);
     
      //VERY IMPORTANT STUFF  -  Send image to Thinger Endpoint
      Serial.println ("Uploading to Thinger");
      pson payload;
      String suffix = ".jpg";
      payload["device_id"] = String(DEVICE_ID + suffix); //Create name of image file to be uploaded
      payload["bytes"].set_bytes(pic->buf, pic->len); //define image file and size
      /*the above pson works when its combined with node_RED
      which listens for and intercepts the endpointcall,
      then converts the payload bytes to a buffer
      and convert the device_id from msg.payload.payload.device_id to msg.file.
      */
      thing.call_endpoint(IMAGE_ENDPOINT,payload); //Trigger Thinger ImageUpload Endpoint  
    }
  esp_camera_fb_return(pic);
}

/*****************************************************
 * * Setup section of code, prior to the main loop * *
 *****************************************************/

void setup() {
//Initialise Serial Coms  
  Serial.begin(115200);  // start serial for output
  Serial.setTimeout(5); //timeout (ms) when checking the serial port for input
  connectWiFi();

//initialise camera
  init_camera();
  delay(250); //wait for camera to initialise

// Initialize SD card
  initSD();

//////////////////////////////////////////////////////
//                Thinger Outputs                   //
//////////////////////////////////////////////////////

//Basic Data
  thing["Data"] >> [](pson & out) {
    out["RunTime(ms)"] = millis();
    out["Version"] = Version;
  };
}

/*******************************************************
*************      MAIN LOOP SECTION     ***************
********************************************************/
void loop() {
//Connect to thinger depending on what connection is avaiable
  thing.handle();  

//check the serial port for data input
  readSerial();

//Check cycle timer
  if ((millis()-msTimer1) >= (msOutput))
  {
    msTimer1 = millis(); //Reset cycle timer to current ms time
    //OP some information about status
    Serial.print("RunTime = ");
    Serial.println(millis());
    //Capture Image
    imageCapture();
  }
}

Note: - I used Platformio as my IDE, and I had problems with getting the code to upload, but after searching the internet I found some recommendations to change the board_build.f_cpu and board_build.f_flash settings in the platformio.ini file, also set monitor_rts and monitor_dtr = 0 as this helps with auto uploading the code to ESP without having to press the IO0 button everytime you want to upload new code . I also had some problems with what I thought was memory allocation, so I found that build_flags = -fstack-protector-strong can help with preventing this, however the reason I had these problems was because I wasnt clearing the memory allocation correctly. I have also defined the library versions that I used in the ini file.

[env:esp32cam]
platform = espressif32
board = esp32cam
board_build.f_cpu = 240000000L
board_build.f_flash = 40000000L
framework = arduino
build_flags =
    -fstack-protector-strong
monitor_speed = 115200
lib_deps =
    espressif/esp32-camera@^2.0.0
    thinger-io/thinger.io@^2.21.1
monitor_rts = 0
monitor_dtr = 0

Ok so this code will save an image to the SD card, and upload some bytes to an EndPoint, so you need to add a new EndPoint named ImageUpload (this name is also defined in the ESP32 code) with the parameters shown below. Note the End Point type is Virtual.
image

But this EndPoint doesn’t do anything (its Virtual), so nodeRED needs to be used to intercept this data and save it in the Thinger File Storage, but before setting up nodeRED you need to add a FileStorage so that there is somewhere to save the image, I set-up one called Images but you can call it whatever you like as long as you use the same name when the nodeRED is set-up. See FILE STORAGES - Thinger.io Documentation

Now the nodeRED needs to be set-up, this is where the other magic happens, which is very powerful :slightly_smiling_face:. Again I had some very valuable help from @jaimebs at Thinger.io on how to set this up correctly. Thanks again Jaime.

When you have finished the node red flow should look something like this
image

Step 1. Add a Server Event Node. I have named this Monitor End Point Call, but you can call this what you like. The important thing is that the ImageUpload end point is monitored. ColourSPi is the name of my server, I believe that this can be left blank to monitor the default server for your account.

image

Step 2. Add Function Node to set the FileName, this requires some coding and wasn’t obvious how to set this up, again thanks to Jaime for assisting with this.

msg.file = msg.payload.payload.device_id
return msg;

image

Step 3. Add another Function Node to convert the Bytes into a Buffer. Again some code is required.

msg.payload.payload.bytes.bytes = Buffer.from(msg.payload.payload.bytes.bytes);
msg.payload = msg.payload.payload.bytes.bytes;
return msg;

image

Step 4. Finally add a Storage Write Node. This is where you need to set the Storage to the name of the File Storage that you have set up previously, and make sure that you have selected the overwrite file Action option, otherwise you could fill up your server space with 000’s of images.

image

Note: - I also added some debug Nodes so that I could see what was going on with the output from each node, but this isn’t compulsory.
image

The last thing to do is add an image to your dashboard, this is easy to do. First add a new widget and select image/MJPEG as the type, then set the Image URL to point to the image that has been saved to your File Storage. To get the URL, open the File Storage, click on the image that has been uploaded, then right-click on the displayed image and Copy the image address, then paste this into the Image URL box in the widget settings.
image
image

Et Voila
image

Hopefully you will find this very useful for your projects :grinning:

1 Like

Hi @MarkAustin902,

Awesome project and development you got there. This type of need has been a recurrent ask in Thinger.io over the years, and we thank you for the detailed post and code. Lets hope more people can use this as the possibilities are endless. Maybe tracking the current print of a 3D printer, take pictures on demand or even through a trigger by motion sensor or the likes.

Thank you very much for sharing!

We welcome further contributions to this post :smiley: