🖼Interface & Application Programming

I am going to create a file server that uses HTTP to transfer audio files from the ESP32 to an iOS app.

👉 Formalizing the API Endpoints

GET /device return a JSON blob with information about the device, such as current battery level, storage remaining, etc.

GET /manifest will return a JSON blob of all audio files stored on device along with associated metadata, such as the file size, the duration, a timestamp and the file identifier.

GET /file/HjkA92naS-891jS-1SjwEas will return an HTTP stream of the audio file associated with the given identifier.

DELETE /file/HjkA92naS-891jS-1SjwEas will delete the audio file associated with the given identifier.

POST /device?op=disconnect with an op (operation) parameters. Can be disconnect, reset, or clean.

⚖️Arduino vs IDF

I still need to figure out whether I am going to program my final project in IDF or with the Arduino port. Using the IDF puts me closer to the metal which might be necessary for performance when saving i2s to the SD card. On the other hand, using the Arduino libraries offers some nice abstractions. Assuming the overhead of using the Arduino libraries isn't a problem, the project would almost certainly be easier to implement using Arduino. But it would suck to spend time implementing the 90% of the functionality in Arduino, only to discover that the critical 10% — the audio recording — isn't possible without using RTOS to access multiple cores and DMA buffers directly.

Building the file server is certainly possible with both:

In networking week, I set up a soft access point using IDF. I know this is possible to do with Arduino as well.

Getting button presses and other I/O events should be possible in both.

Recording and saving the audio in real-time is the tricky part.

  1. With the IDF, I would use the i2s_driver combined with the sdmmc_driver.

  2. It should be possible with the Arduino libraries as well. This is an example of a project recording audio on an Arduino Pro Mini — which runs on an ATMEGA — using an Arduino library called TMRh20.

    I also found this Arduino project.

  3. The final alternative is using Espressif's Audio Development Framework (ADF) which abstracts away some of the lower level implementation details.

I want to check the microphone as well.

🔊Getting Started with Espressif's Audio Development Framework
  1. Clone the directory git clone --recursive https://github.com/espressif/esp-adf.git

  2. Set up the ADF_PATH in ~/.profile

ADF only spec'd to run on certain Espressif development boards (Lytra and a few others) so the code is tightly coupled to a few specific configurations of pins. I am not sure if it is worth mapping my board to this board until I know if it works.

In the end, I decided not to use the ADF because it seemed like a lot of work to configure it for a custom PCB rather than one of the existing devboards.

📂 ESP32 File Server from microSD card

I started off by looking at the IDF example code for running a file sever from SPIFF.

I changed it so that was reading files from the mounted microSD card as well as returning details about the device (version number, timezone, storage remaining).

I wrote my own set of C functions/macros for generating JSON output.

#include <stdio.h>
#include <string.h>

//WiFi Soft-Access Point
#include "esp_wifi.h"
#include "esp_event_loop.h"
#include "nvs_flash.h"

//file server
#include "esp_http_server.h"
#include "esp_vfs.h"

//file info
#include <dirent.h> 
#include <sys/stat.h>
#include <time.h>


#define DEVICE_SSID      "auxbridge"
#define DEVICE_PASS      ""

//HTTP Servers
#include "tcpip_adapter.h"

#include "esp_log.h"
#include "esp_err.h"
#include "esp_system.h"

//SD Card
#include <sys/unistd.h>
#include <sys/stat.h>
#include "esp_vfs_fat.h"
#include "driver/sdmmc_host.h"
#include "sdmmc_cmd.h"

/* Max length a file path can have on storage */
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN)

/* Max size of an individual file. Make sure this
 * value is same as that set in upload_script.html */
#define MAX_FILE_SIZE   (200*1024) // 200 KB
#define MAX_FILE_SIZE_STR "200KB"

/* Scratch buffer size */
#define SCRATCH_BUFSIZE  8192

struct file_server_data {
    /* Base path of file storage */
    char base_path[ESP_VFS_PATH_MAX + 1];

    /* Scratch buffer for temporary storage during file transfer */
    char scratch[SCRATCH_BUFSIZE];
};

static const char *TAG = "auxbridge";

void sd_init(void)
{
    ESP_LOGI(TAG, "Initializing SD card");
    ESP_LOGI(TAG, "Using SDMMC peripheral");
    sdmmc_host_t host = SDMMC_HOST_DEFAULT();
    sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
    gpio_set_pull_mode(15, GPIO_PULLUP_ONLY);   // CMD, needed in 4- and 1- line modes
    gpio_set_pull_mode(2, GPIO_PULLUP_ONLY);    // D0, needed in 4- and 1-line modes
    gpio_set_pull_mode(4, GPIO_PULLUP_ONLY);    // D1, needed in 4-line mode only
    gpio_set_pull_mode(12, GPIO_PULLUP_ONLY);   // D2, needed in 4-line mode only
    gpio_set_pull_mode(13, GPIO_PULLUP_ONLY);   // D3, needed in 4- and 1-line modes

    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
        .format_if_mount_failed = false,
        .max_files = 5,
        .allocation_unit_size = 16 * 1024
    };

    sdmmc_card_t* card;
    esp_err_t ret = esp_vfs_fat_sdmmc_mount("/sd", &host, &slot_config, &mount_config, &card);

    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount filesystem. "
                "If you want the card to be formatted, set format_if_mount_failed = true.");
        } else {
            ESP_LOGE(TAG, "Failed to initialize the card (%s). "
                "Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
        }
        return;
    }

    sdmmc_card_print_info(stdout, card);
}

#define IS_FILE_EXT(filename, ext) \
    (strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0)

/* Set HTTP response content type according to file extension */
static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filename)
{
    if (IS_FILE_EXT(filename, ".pdf")) {
        return httpd_resp_set_type(req, "application/pdf");
    } else if (IS_FILE_EXT(filename, ".html")) {
        return httpd_resp_set_type(req, "text/html");
    } else if (IS_FILE_EXT(filename, ".jpeg")) {
        return httpd_resp_set_type(req, "image/jpeg");
    } else if (IS_FILE_EXT(filename, ".ico")) {
        return httpd_resp_set_type(req, "image/x-icon");
    }
    /* This is a limited set only */
    /* For any other type always set as plain text */
    return httpd_resp_set_type(req, "text/plain");
}

/* Handler to download a file kept on the server */
static esp_err_t download_get_handler(httpd_req_t *req)
{
    char filepath[FILE_PATH_MAX];
    FILE *fd = NULL;
    struct stat file_stat;

    const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
                                             req->uri, sizeof(filepath));
    if (!filename) {
        ESP_LOGE(TAG, "Filename is too long");
        /* Respond with 500 Internal Server Error */
        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
        return ESP_FAIL;
    }


    if (stat(filepath, &file_stat) == -1) {
        
        ESP_LOGE(TAG, "Failed to stat file : %s", filepath);
        /* Respond with 404 Not Found */
        httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
        return ESP_FAIL;
    }

    fd = fopen(filepath, "r");
    if (!fd) {
        ESP_LOGE(TAG, "Failed to read existing file : %s", filepath);
        /* Respond with 500 Internal Server Error */
        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
        return ESP_FAIL;
    }

    ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size);
    set_content_type_from_file(req, filename);

    /* Retrieve the pointer to scratch buffer for temporary storage */
    char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
    size_t chunksize;
    do {
        /* Read file in chunks into the scratch buffer */
        chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);

        /* Send the buffer contents as HTTP response chunk */
        if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
            fclose(fd);
            ESP_LOGE(TAG, "File sending failed!");
            /* Abort sending file */
            httpd_resp_sendstr_chunk(req, NULL);
            /* Respond with 500 Internal Server Error */
            httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
            return ESP_FAIL;
        }

        /* Keep looping till the whole file is sent */
    } while (chunksize != 0);

    /* Close file after sending complete */
    fclose(fd);
    ESP_LOGI(TAG, "File sending complete");

    /* Respond with an empty chunk to signal HTTP response completion */
    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

static esp_err_t sample_get_handler(httpd_req_t *req)
{
    ESP_LOGI(TAG, "Reading sample file");

    char filepath[FILE_PATH_MAX];
    FILE *fd = NULL;
    struct stat file_stat;

    fd = fopen("/sd/sample.wav", "r");
    if (!fd) {
        ESP_LOGE(TAG, "Failed to read existing file : %s", filepath);
        /* Respond with 500 Internal Server Error */
        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
        return ESP_FAIL;
    }

    ESP_LOGI(TAG, "Sending file : sample.txt...");
    set_content_type_from_file(req, "sample.txt");

    /* Retrieve the pointer to scratch buffer for temporary storage */
    char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
    size_t chunksize;
    do {
        /* Read file in chunks into the scratch buffer */
        chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);

        /* Send the buffer contents as HTTP response chunk */
        if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
            fclose(fd);
            ESP_LOGE(TAG, "File sending failed!");
            /* Abort sending file */
            httpd_resp_sendstr_chunk(req, NULL);
            /* Respond with 500 Internal Server Error */
            httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
            return ESP_FAIL;
        }

        /* Keep looping till the whole file is sent */
    } while (chunksize != 0);

    /* Close file after sending complete */
    fclose(fd);
    ESP_LOGI(TAG, "File sending complete");

    /* Respond with an empty chunk to signal HTTP response completion */
    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

#define JSON_NO_DELIMITER 0
#define JSON_WITH_DELIMITER 1
#define JSON_STRING_FORMAT "\"%s\":\"%s\"%s"
#define JSON_NUM_FORMAT "\"%s\":%s%s"
#define JSON_FILESIZE_FORMAT "\"%s\":%ld%s"

#define JSON_STRING_FORMAT_WITH_DELIMITER JSON_STRING_FORMAT, JSON_WITH_DELIMITER
#define JSON_STRING_FORMAT_NO_DELIMITER JSON_STRING_FORMAT, JSON_NO_DELIMITER
#define JSON_NUM_FORMAT_WITH_DELIMITER JSON_NUM_FORMAT, JSON_WITH_DELIMITER
#define JSON_NUM_FORMAT_NO_DELIMITER JSON_NUM_FORMAT, JSON_NO_DELIMITER

char* json(char *ret, char *key, char *val, const char *fmt, int deliminator) {
    sprintf(ret, fmt, key, val, (deliminator == JSON_NO_DELIMITER) ? "\n": ",\n");
    return ret;
} 


static esp_err_t device_status_handler(httpd_req_t *req)
{
    char buf[80];

    httpd_resp_set_type(req, "application/json");

    httpd_resp_sendstr_chunk(req, "{\n");

    httpd_resp_sendstr_chunk(req, json(buf, "name", "auxbridge", JSON_STRING_FORMAT_WITH_DELIMITER));
    httpd_resp_sendstr_chunk(req, json(buf, "author", "mschrage", JSON_STRING_FORMAT_WITH_DELIMITER));
    httpd_resp_sendstr_chunk(req, json(buf, "version", "1.0", JSON_NUM_FORMAT_NO_DELIMITER));

    httpd_resp_sendstr_chunk(req, "}");

    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

static esp_err_t manifest_handler(httpd_req_t *req) {
    char buf[80];

    char entrypath[20];

    struct dirent *entry;
    struct stat entry_stat;
    char entrysize[16];

    char *dirpath = "/sd/";
    DIR *dir = opendir(dirpath);
    const size_t dirpath_len = strlen(dirpath);
    strlcpy(entrypath, dirpath, sizeof(entrypath));

    httpd_resp_set_type(req, "application/json");

    httpd_resp_sendstr_chunk(req, "{\n");

    httpd_resp_sendstr_chunk(req, "\"files\": [\n");

    int count = 0;

    while ((entry = readdir(dir)) != NULL) {

        strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len);
        if (stat(entrypath, &entry_stat) == -1) {
            ESP_LOGE(TAG, "Failed to stat %s, %s", entrypath, entry->d_name);
            continue;
        }

        sprintf(entrysize, "%ld", entry_stat.st_size);
        ESP_LOGI(TAG, "Found %s (%s bytes)", entry->d_name, entrysize);

        if (count > 0) {
            httpd_resp_sendstr_chunk(req, ",");
        }

        httpd_resp_sendstr_chunk(req, "{\n");
        httpd_resp_sendstr_chunk(req, json(buf, "id", entry->d_name, JSON_STRING_FORMAT_WITH_DELIMITER));
        //httpd_resp_sendstr_chunk(req, json(buf, "dt", ctime(&entry_stat.st_mtime), JSON_STRING_FORMAT_WITH_DELIMITER));
        httpd_resp_sendstr_chunk(req, json(buf, "size", entrysize, JSON_NUM_FORMAT_WITH_DELIMITER));


        httpd_resp_sendstr_chunk(req, "}");
        count++;

    }
    closedir(dir);
    httpd_resp_sendstr_chunk(req, "]\n");


    httpd_resp_sendstr_chunk(req, "}");

    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

esp_err_t server_init(const char *base_path) {
 static struct file_server_data *server_data = NULL;

    /* Validate file storage base path */
    if (!base_path || strcmp(base_path, "/sd") != 0) {
        ESP_LOGE(TAG, "File server presently supports only '/sd' as base path");
        return ESP_ERR_INVALID_ARG;
    }

    if (server_data) {
        ESP_LOGE(TAG, "File server already started");
        return ESP_ERR_INVALID_STATE;
    }

    /* Allocate memory for server data */
    server_data = calloc(1, sizeof(struct file_server_data));
    if (!server_data) {
        ESP_LOGE(TAG, "Failed to allocate memory for server data");
        return ESP_ERR_NO_MEM;
    }
    strlcpy(server_data->base_path, base_path,
            sizeof(server_data->base_path));

    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    /* Use the URI wildcard matching function in order to
     * allow the same handler to respond to multiple different
     * target URIs which match the wildcard scheme */
    config.uri_match_fn = httpd_uri_match_wildcard;

    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) != ESP_OK) {
        ESP_LOGE(TAG, "Error starting file server!");
        return ESP_FAIL;
    }

    ESP_LOGI(TAG, "Registering URI handlers");

    /* URI handler for getting uploaded files */
    httpd_uri_t file_download = {
        .uri       = "/file/*",  // Match all URIs of type /path/to/file
        .method    = HTTP_GET,
        .handler   = download_get_handler,
        .user_ctx  = server_data    // Pass server data as context
    };
    httpd_register_uri_handler(server, &file_download);

    httpd_uri_t device_status = {
        .uri       = "/device",   // Match all URIs of type /upload/path/to/file
        .method    = HTTP_GET,
        .handler   = device_status_handler,
        .user_ctx  = server_data    // Pass server data as context
    };
    httpd_register_uri_handler(server, &device_status);

    /* URI handler for deleting files from server */
    httpd_uri_t manifest = {
        .uri       = "/manifest",   // Match all URIs of type /delete/path/to/file
        .method    = HTTP_GET,
        .handler   = manifest_handler,
        .user_ctx  = server_data    // Pass server data as context
    };
    httpd_register_uri_handler(server, &manifest);

    return ESP_OK;
}

/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;

static esp_err_t event_handler(void *ctx, system_event_t *event)
{
    switch(event->event_id) {
    case SYSTEM_EVENT_AP_STACONNECTED:
        ESP_LOGI(TAG, "station:"MACSTR" join, AID=%d",
                 MAC2STR(event->event_info.sta_connected.mac),
                 event->event_info.sta_connected.aid);
        break;
    case SYSTEM_EVENT_AP_STADISCONNECTED:
        ESP_LOGI(TAG, "station:"MACSTR"leave, AID=%d",
                 MAC2STR(event->event_info.sta_disconnected.mac),
                 event->event_info.sta_disconnected.aid);
        break;
    default:
        break;
    }
    return ESP_OK;
}

void wifi_init(void)
{
     esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
      ESP_ERROR_CHECK(nvs_flash_erase());
      ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    s_wifi_event_group = xEventGroupCreate();

    tcpip_adapter_init();
    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    wifi_config_t wifi_config = {
        .ap = {
            .ssid = DEVICE_SSID,
            .ssid_len = strlen(DEVICE_SSID),
            .password = DEVICE_PASS,
            .max_connection = 1,
            .authmode = WIFI_AUTH_OPEN
        },
    };

    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

            tcpip_adapter_ip_info_t ip_info;
        IP4_ADDR(&ip_info.ip, 192, 168, 1, 1);
        IP4_ADDR(&ip_info.gw, 192, 168, 1, 1);
        IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
        //192.168.4.1

        // tcpip_adapter_ap_start((uint8_t *)"9C:B6:D0:E7:30:0F", &ip_info);

    ESP_LOGI(TAG, "wifi_init_softap finished.SSID:%s password:%s",
             DEVICE_SSID, DEVICE_PASS);

}

void app_main()
{
    printf("starting app_main()\n");
    sd_init();
    wifi_init();
    server_init("/sd");
}