ESP web serve a SD Image

Hi,
I'm attempting to serve an image from sd card via the server... It is ammended from CarmeraWebServer project example.

String read_file(const char * path)
{
  String s = "";
  Serial.printf("Reading file: %s\n", path);
  fs::FS fs = SD_MMC;
  File file = fs.open(path, FILE_READ);
  if(!file){
      Serial.println("Failed to open file for reading");
      return s;
  }
  while(file.available()){
      s += String((char)file.read());
  }
  file.close();
  return s;
}

static esp_err_t handler_test(httpd_req_t *req){
  httpd_resp_set_type(req, "image/jpeg");
  //httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
  //httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  String idata = read_file("/picture4.jpg");
  return httpd_resp_send(req, idata.c_str(), idata.length());
}

The web browser says "The image http://http://192.168.1.200/test cannot be displayed, because it contains errors"...

Any ideas please?

That looks odd to me. How about to you?

Yeah, that's the error message being reparsed by Discourse... Ignore it!

I've tried re-casting the data, is it in the wrong format?

I don't think putting a binary image into String is a good idea. For example binary pixel 0 is completely legitimate, whereas char 0 designates the end of String ...

Can you check if the length of String corresponds to the image size?

... you can malloc an array of chars, send it to the client and don't forget to free it afterwards.

The other option is to base64 encode binary images. This way they can be represented as text in Strings.

I tried the malloc and calloc way and got similar issues...

However, as you mention, the string length did report as being about 500b smaller each time than was reported using SD / FS...

I ended up doing a full rewrite using ESPAsyncWebSrv and used its internal static serve and it served images that could be displayed.

Thankyou

For the road less travelled...

  server.on("/view", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SD_MMC, "/picture3.jpg", "image/jpeg");
  });

  //server.serveStatic("/", SD_MMC, "/");   //  DANGER... serves any file on the SD!!!
  server.serveStatic("/img/", SD_MMC, "/");   //  DANGER... serves any file on the SD!!!

I thought I had a problem, now I'm not sure, well obviously I have problems, but I'm so tired that I've broken off the lens and the CCD is bare, the pictures are so random and even look like they are moving, yup, problems... I thought / think that when clicking to take a new photo that it stores something from a buffer (???) which is two images previous... So I rewrote the code back to bare essentials and into a single file, but now I'm not so sure that it's saving old images, anyway...

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
//#include <ESPAsyncWebServer.h>
#include <ESPAsyncWebSrv.h>

#include "esp_camera.h"

#include "soc/soc.h"           // Disable brownour problems
#include "soc/rtc_cntl_reg.h"  // Disable brownour problems

//#include "driver/rtc_io.h"
#include <EEPROM.h>            // read and write from flash memory

#include "FS.h"
#include "SD_MMC.h"            // SD Card ESP32

const char* ssid = "ssid";        //Write your own Wi-Fi name here
const char* password = "password";    //Write your own password here


IPAddress local_IP(192, 168, 1, 200);
IPAddress gateway(192, 168, 1, 255);
IPAddress subnet(255, 255, 0, 0);
IPAddress primaryDNS(8, 8, 8, 8);
IPAddress secondaryDNS(8, 8, 4, 4);


AsyncWebServer server(80);         //object created on default port 80



// define the number of bytes you want to access
#define EEPROM_SIZE 1


// Pin definition for CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27

#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22


char webpage[] PROGMEM = R"=====(
<!DOCTYPE html>
<html>
<head>
<title>CCam</title>
<style>
 body(background-color:rgba(128,128,128, 0.8)}
</style>
</head>
<body>
<div>
  <div style="width:90%">
    <h2>CCam</h2>
    <button onclick="photo()">Photo</button><br /><br />
  </div>
  
  <div>
    <div id="" style="float:left;padding:15px;">
      <b>Files</b><br />
      <div>
        <button onclick="fm_refresh()">Refresh</button>
      </div>
      <div id="div_dir" style="width:250px;height:300px;overflow:scroll;"></div>
    </div>
    
    <div id="" style="float:left;padding:15px;width:50%;">
      <img src='' id='img1' name='img1' style="width:50%;">
    </div>
  </div>

</div>

<script>

function photo()
{
  var req = new XMLHttpRequest();
  req.onreadystatechange = function()
  {
    if(this.readyState == 4 && this.status == 200)
    {
      //document.getElementById("div_dir2").innerHTML = this.responseText;
    }
  };
  req.open("GET", "photo", true);
  req.send();
}
function img_set(fn) {
  var pic = document.getElementById("img1");
  //pic.src = "img?fn=picture4.jpg";
  pic.src = "img?fn=" + fn;
}

function fm_refresh()
{
  var req = new XMLHttpRequest();
  req.onreadystatechange = function()
  {
    if(this.readyState == 4 && this.status == 200)
    {
      var data = JSON.parse(this.responseText);
      var ss = "<ul>";
      var len =  data.length;
      for(var i=0;i<len;i++ ){
          ss += "<li onclick=\"img_set('" + data[i][1] + "')\">" + data[i][1] + "</li>";
      }
      ss += "</ul>";
      document.getElementById("div_dir").innerHTML = ss;
    }
  };
  req.open("GET", "ls", true);
  req.send();
}
</script>
</body>
</html>)=====";


void init_wifi()
{ 
  // Configures static IP address
  if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) 
  {
    Serial.println("Static IP Fail");
  }
  
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("Connecting WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());
}

void init_sd()
{ 
  //if(!SD_MMC.begin())
  if(!SD_MMC.begin("/sdcard", true))  //  disables the flash!!! yeah!
  {
    Serial.println("Initialization Failed");
    return;
  }
  uint8_t cardType = SD_MMC.cardType();

  if(cardType == CARD_NONE){
    Serial.println("SD card is not present!");
    return;
  }

  Serial.print("SD Card Type: ");
  if(cardType == CARD_MMC){
    Serial.println("MMC");
  } else if(cardType == CARD_SD){
    Serial.println("SDSC");
  } else if(cardType == CARD_SDHC){
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }
  uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  uint64_t card_free = SD_MMC.usedBytes();  // / (1024 * 1024);
  //Serial.printf("SD Card Used: %lluMB\n", card_free);
  Serial.printf("SD Card Used: %lluB\n", card_free);
}

int fs_file_exist(fs::FS &fs, const char * path){
  File file = fs.open(path);
  if(!file){
    return 0; //  file not exist
  }
  return 1; //  file exists
}

String dir_list(fs::FS &fs, const char * dirname)
{
    String s = "";
    File root = fs.open(dirname);
    if(!root){
        Serial.println("Failed to open directory");
        return s;
    }
    if(!root.isDirectory()){
        Serial.println("Not a directory");
        return s;
    }

    s += "[";
    File file = root.openNextFile();
    while(file){
        if(!file.isDirectory())
        {
          String fn = file.name();
          if(fn.endsWith(".jpg"))
          {
            s += "[\"f\",\"" + fn + "\"],";
          }
        }
        file = root.openNextFile();
    }
    s = s.substring(0, s.length()-1);
    s += "]";
    return s;
}


void cam_setup()
{
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG; 
  
  if(psramFound()){
    config.frame_size = FRAMESIZE_UXGA; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
  
  // Init Camera
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  // initialize EEPROM with predefined size
  EEPROM.begin(EEPROM_SIZE);
  //pictureNumber = EEPROM.read(0) + 1;
  
  //cam_pic();
}

void cam_pic()
{
  camera_fb_t * fb = NULL;
  
  // Take Picture with Camera
  fb = esp_camera_fb_get();  
  if(!fb) {
    Serial.println("Camera capture failed");
    return;
  }


  int pictureNumber = EEPROM.read(0) + 1;

  // Path where new picture will be saved in SD Card
  String path = "/picture" + String(pictureNumber) +".jpg";

  fs::FS &fs = SD_MMC; 
  Serial.printf("Picture file name: %s\n", path.c_str());
  
  File file = fs.open(path.c_str(), FILE_WRITE);
  if(!file){
    Serial.println("Failed to open file in writing mode");
  } 
  else {
    file.write(fb->buf, fb->len); // payload (image), payload length
    Serial.printf("Saved file to path: %s\n", path.c_str());
    EEPROM.write(0, pictureNumber);
    EEPROM.commit();
  }
  file.close();
  esp_camera_fb_return(fb);
  
}

void setup() {

  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector

  
  Serial.begin(115200);
  init_sd();
  init_wifi();
  cam_setup();


  
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
  {
    //  http://192.168.1.200/
    request->send(200, "text/html", webpage);
  });

  server.on("/img", HTTP_GET, [](AsyncWebServerRequest *request)
  {
    //  http://192.168.1.200/img?fn=picture4.jpg
    bool flag = true;
    if(request->hasParam("fn", false))    //  for GET
    {
        String fn = request->getParam("fn", false)->value();    //  for GET
        fn = "/" + fn;
        if(fs_file_exist(SD_MMC, fn.c_str())){
          flag = false;
          //  RETURN REQUESTED IMAGE
          request->send(SD_MMC, fn.c_str(), "image/jpeg");
        }
    }
    
    if(flag)
    {
        //  RETURN DEFAULT IMAGE (e.g. latest, first)
        request->send(SD_MMC, "/picture3.jpg", "image/jpeg");
    }
  });
  
  server.on("/ls", HTTP_GET, [](AsyncWebServerRequest *request)
  {
    //  http://192.168.1.200/ls
    //String msg = "[[\"f\", \"picture4.jpg\"],[\"f\",\"picture3.jpg\"]]";
    String msg = dir_list(SD_MMC, "/");
    Serial.println(msg);
    request->send(200, "application/json", msg);
  });
  

  server.on("/photo", HTTP_GET, [](AsyncWebServerRequest *request)
  {
    //  http://192.168.1.200/photo
    cam_pic();
    request->send(200, "text/html", "ok");
  });
  
  server.begin();
}

void loop() {
  // put your main code here, to run repeatedly:

}

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.