0

I'm trying to send an image file from a node.js server across a TCP/IP connection. I converted the image file to a hexadecimal string using fs.createReadStream and received the hex string as expected on the client side. Now I need to figure out how to reconstruct the image on the client side using the hex string.

Node.js code:

function newStream(res, imageFile){
    var readStream = fs.createReadStream(imageFile);
    readStream.on('data', chunk => {
        res.send(chunk.toString('hex'));
    });
}

Client side code (in C) used on an ESP32 development board to save data to a png file in spiffs:

#define MAX_HTTP_OUTPUT_BUFFER 512
int max_buff = 512;

static void http_native_request(void)
{
    char output_buffer[MAX_HTTP_OUTPUT_BUFFER] = {0};
    int content_length = 0;
    int track_length = 0;

    esp_http_client_config_t config = {
        .url = "http://192.168.1.122/api?file=file01",
    };
    esp_http_client_handle_t client = esp_http_client_init(&config);

    // GET Request
    esp_http_client_set_method(client, HTTP_METHOD_GET);
    esp_err_t err = esp_http_client_open(client, 0);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
    } else {
        content_length = esp_http_client_fetch_headers(client);
        track_length = content_length;
        if (content_length < 0) {
            ESP_LOGE(TAG, "HTTP client fetch headers failed");
        } else {
            do {
            int data_read = esp_http_client_read_response(client, output_buffer, max_buff);
            output_buffer[max_buff] = '\0';
            create_file_app(output_buffer); //saves output_buffer to a newly created png file
            if (data_read >= 0) {
                track_length -= data_read;
                if (max_buff > track_length){
                    max_buff = track_length;
                }
            } else {
                ESP_LOGE(TAG, "Failed to read response");
            }
            } while (
                track_length>0
            );
        }
    }
    esp_http_client_close(client);
}

Hex string from server looks like this (has the signature of a png file):

89504e470d0a1a0a0000000d494844520000006400000064080600 ... f03b8c85cc0643044ae0000000049454e44ae426082

My research indicates that I need to convert this hex string into binary data on the client side. But when I do that (code for conversion not shown here), the png file created by create_file_app function simply displays the binary string (which correctly corresponds to the hex string) received by client as opposed to displaying the image that I thought I downloaded from the server. How do I save this hex data so I get the image I was expecting when I open the png file created on the client side? Or is there C library that can help with this?

Edit 1:

My code for sending image data from the node.js server as is, without converting to hex or another format:

function newStream(res, imageFile){
    var readStream = fs.createReadStream(imageFile);
    readStream.on('data', chunk => {
        res.send(chunk);
    });
}

Here's what the node.js console shows for "chunk":

<Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 00 64 00 00 00 64 08 06 00 00 00 70 e2 95 54 00 00 00 06 62 4b 47 44 00 ff 00 ff 00 ff a0 bd a7 ... >

On the client side, here's ESP's code for esp_http_client_read_response():

int esp_http_client_read_response(esp_http_client_handle_t client, char *buffer, int len)
{
    int read_len = 0;
    while (read_len < len) {
        int data_read = esp_http_client_read(client, buffer + read_len, len - read_len);
        if (data_read <= 0) {
            return read_len;
        }
        read_len += data_read;
    }
    return read_len;
}

Esp_http_client_read_response calls esp_http_client_read (again, this was published by ESP) - here's the code for that:

int esp_http_client_read(esp_http_client_handle_t client, char *buffer, int len)
{
    esp_http_buffer_t *res_buffer = client->response->buffer;

    int rlen = ESP_FAIL, ridx = 0;
    if (res_buffer->raw_len) {
        int remain_len = client->response->buffer->raw_len;
        if (remain_len > len) {
            remain_len = len;
        }
        memcpy(buffer, res_buffer->raw_data, remain_len);
        res_buffer->raw_len -= remain_len;
        res_buffer->raw_data += remain_len;
        ridx = remain_len;
    }
    int need_read = len - ridx;
    bool is_data_remain = true;
    while (need_read > 0 && is_data_remain) {
        if (client->response->is_chunked) {
            is_data_remain = !client->is_chunk_complete;
        } else {
            is_data_remain = client->response->data_process < client->response->content_length;
        }

        if (!is_data_remain) {
            break;
        }
        int byte_to_read = need_read;
        if (byte_to_read > client->buffer_size_rx) {
            byte_to_read = client->buffer_size_rx;
        }
        errno = 0;
        rlen = esp_transport_read(client->transport, res_buffer->data, byte_to_read, client->timeout_ms);
        ESP_LOGD(TAG, "need_read=%d, byte_to_read=%d, rlen=%d, ridx=%d", need_read, byte_to_read, rlen, ridx);

        if (rlen <= 0) {
            if (errno != 0) {
                esp_log_level_t sev = ESP_LOG_WARN;
                /* On connection close from server, recv should ideally return 0 but we have error conversion
                 * in `tcp_transport` SSL layer which translates it `-1` and hence below additional checks */
                if (rlen == -1 && errno == ENOTCONN && client->response->is_chunked) {
                    /* Explicit call to parser for invoking `message_complete` callback */
                    http_parser_execute(client->parser, client->parser_settings, res_buffer->data, 0);
                    /* ...and lowering the message severity, as closed connection from server side is expected in chunked transport */
                    sev = ESP_LOG_DEBUG;
                }
                ESP_LOG_LEVEL(sev, TAG, "esp_transport_read returned:%d and errno:%d ", rlen, errno);
            }
            if (rlen < 0 && ridx == 0) {
                return ESP_FAIL;
            } else {
                return ridx;
            }
        }
        res_buffer->output_ptr = buffer + ridx;
        http_parser_execute(client->parser, client->parser_settings, res_buffer->data, rlen);
        ridx += res_buffer->raw_len;
        need_read -= res_buffer->raw_len;

        res_buffer->raw_len = 0; //clear
        res_buffer->output_ptr = NULL;
    }

    return ridx;
}

Here's my code for create_file_app() which saves retrieved data in a spiffs file:

void create_file_app(char *buffer)
{
    ESP_LOGI(TAG, "Opening file");
    FILE* f = fopen("/spiffs/hello1.png", "a+");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for writing");
        return;
    }
    fprintf(f, buffer);
    fclose(f);
}
the busybee
  • 10,755
  • 3
  • 13
  • 30
coder101
  • 383
  • 4
  • 21
  • How do you display the PNG file? Is the hex string present as text in a text editor? If you have problems with converting hex string into binary data, why do you think, the conversion function might be not relevant? Looks like the very first place to look at. for me. – Gerhardh May 20 '21 at 07:40
  • When I open the png file in a browser (let's say Chrome), it simply shows the binary string associated with the hex data). I'm not having trouble converting the hex string to binary string. But is a binary string the correct thing to save in the png file or do i need some other arrangement for the binary data? I'm very new to this and appreciate some direction. – coder101 May 20 '21 at 07:52
  • 2
    Why at all do you need to convert the image into a hex string? Can't you just send the bytes as such? Creating the image is then as simple as storing the received bytes. – the busybee May 20 '21 at 13:12
  • @thebusybee sending the image just as a buffer from the server causes it to be received as garbage characters (as expected) on the client side, read by ESP's esp_http_client_read_response function. When I save this to the spiffs file, the file size is nowhere close to the buffer data length on the server side. If you have an idea on how to fix this, I'd appreciate some guidance since I'm relatively new to C. – coder101 May 20 '21 at 16:24
  • Then you're doing something wrong with how you're handling it. There's no need to convert the image to anything; you can just transfer the bytes over HTTP. That's the way it's normally done. – romkey May 20 '21 at 19:50
  • @romkey thanks for that feedback. Care to show a newbie how it's done correctly? How do I make sure output_buffer in esp_http_client_read_response() correctly capture bytes data when it's send over HTTP? esp_http_client_read_response is a low level API used by the ESP32 chip to handle responses over TCP/IP. – coder101 May 20 '21 at 20:00
  • Please [edit] your question and add the version that tries to receive and store the binary data. The receiving function apparently returns the number of bytes to save, you "just" need to use the value. – the busybee May 20 '21 at 20:16
  • @thebusybee please see additional code in edited version. – coder101 May 20 '21 at 20:33
  • Oh, then it's clear that you cannot save the bytes correctly: You are using `fprintf()`. This function expects a format string as the second parameter, and even if you were using `"%s"`, it would expect a zero-terminated C-string. And the first zero byte is at offset 8 in your data. Please try `fwrite()` and provide the length. C's array work differently than Javascript's arrays. – the busybee May 21 '21 at 06:05
  • The length of the resulting file should equal the number of bytes transferred. With `fprintf()` it is 8, I'm sure. Did you check the file length? – the busybee May 21 '21 at 06:07
  • @thebusybee Can you please clarify what you mean by "C's array work differently than javascript's arrays" in the context of what I'm trying to accomplish (send over image file to an ESP chip from a node.js server)? Also, how is it obvious that there is an offset 8 in my data? Is that because the first null character is 8 bytes out (I think those 8 bytes represent the signature of a png file). – coder101 May 21 '21 at 16:32
  • @thebusybee Also, the length of the bytes transferred from the server is 4517. But the buffer being written to the spiffs file with fwrite is only 36. Not sure why. I'm pretty sure all 4517 bytes come through across the server in segments of 512 bytes (as defined by MAX_HTTP_OUTPUT_BUFFER) - takes 9 iterations of esp_http_client_read_response. – coder101 May 21 '21 at 16:38
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/232715/discussion-between-the-busybee-and-coder101). – the busybee May 21 '21 at 19:56
  • AFAK, there's absolutely no reason to save a PNG as a *"hex dump"* in a SPIFFS file. It should look exactly the same as how it looks on your host OS. – Mark Setchell May 21 '21 at 20:14

1 Answers1

0

Instead of calling fprintf() you need to call fwrite().

You receive the binary bytes with esp_http_client_read_response(). Store the returned length.

Then use this function to create the file:

void create_file_app(char *buffer, size_t length)
{
    ESP_LOGI(TAG, "Opening file");
    FILE* f = fopen("/spiffs/hello1.png", "wb");
    if (f == NULL) {
        ESP_LOGE(TAG, "Failed to open file for writing");
        return;
    }
    fwrite(buffer, 1, length, f);
    fclose(f);
}

Why is fprintf() the wrong function?

Because it works only with C-strings. These are sequences of characters, that means of type char[]. The length of such a string is determined by a special character, '\0', which has commonly the value 0. And the received bytes have a lot of zeroes in them. This character marks the end of the string, independent of the size of the character array.

Arrays in C don't "carry" their length with them, if you hand their address to other functions. You need to provide the length separately. Other languages do this in other ways.

the busybee
  • 10,755
  • 3
  • 13
  • 30