main/task_sdcard.c

Mon, 17 May 2021 20:44:35 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Mon, 17 May 2021 20:44:35 +0200
changeset 91
255a75322212
parent 88
7f02dbee58d0
child 92
bac0a860f5dd
permissions
-rw-r--r--

Added 7 sgemnt fonts for the web client. Log messages in the components more compact. Added fonts mime-types to the webserver. Switched to stable esp-idf 4.2.1. Upgraded the SD-mmc card API.

/**
 * @file task_sdcard.c
 * @brief SD/MMC driver. This driver is for a slot for the user on
 *        the front panel. It will detect an inserted card and then
 *        mount it. It also detects the removal of the card and then
 *        unmounts it. Be carefull, only do this in the main menu.
 *        Also, brew logging is handled here and finished brewlogs
 *        are copied to the SD card if it is mounted.
 *        Recipes to import must go into the /sdcard/recipe folder
 *        and have extension .xml and the contents be a beerxml file.
 *        Backup and restore is also done to this card.
 */


#include "vfs_fat_internal.h"
#include "driver/sdmmc_host.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "diskio_impl.h"
#include "diskio_sdmmc.h"

#include "config.h"



SDCARD_State		*sdcard_state;			///< SD card status
JSON_log		*json_log;			///< JSON log array
EventGroupHandle_t	xEventGroupSDcard;		///< SD card events.

static const char	*TAG = "task_sdcard";
static sdmmc_card_t* 	s_card = NULL;
static uint8_t		s_pdrv = 0;
static char		* s_base_path = NULL;
static uint8_t		pdrv = FF_DRV_NOT_USED;


#define SDCARD_HOST_SLOT	VSPI_HOST		///< HSPI_HOST is used by the TFT
#define SDCARD_PIN_NUM_MISO	2			///< MISO pin
#define SDCARD_PIN_NUM_MOSI	15			///< MOSI pin
#define SDCARD_PIN_NUM_CLK	14			///< CLOCK
#define SDCARD_PIN_NUM_CS	13			///< Chip select pin
#define	SDCARD_DMA_CHANNEL	2			///< Channel 1 is used by the TFT


const int TASK_SDCARD_LOG_CLEAN = BIT0;			///< Clean spiffs logfile directory
const int TASK_SDCARD_LOG_CLOSE = BIT1;			///< Close the logfile.

extern time_t		now;
extern struct tm	timeinfo;
extern uint32_t		TimeBrewing;
extern bool             System_TimeOk;



void log_msg(const char *tag, const char *format, ...)
{
    char	*outstr, logfn[64], stamp[32];
    va_list	va_ptr;
    FILE	*fp;

    outstr = calloc(1024, sizeof(char));
    va_start(va_ptr, format);
    vsnprintf(outstr, 1023, format, va_ptr);
    ESP_LOGI(tag, "%s", outstr);

    if (System_TimeOk && sdcard_state->card_present == true) {
	snprintf(logfn, 64, "/sdcard/w/log/sys%04d%02d%02d.log", timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday);
	snprintf(stamp, 31, "%02d-%02d %02d:%02d:%02d", timeinfo.tm_mday, timeinfo.tm_mon+1, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
	fp = fopen(logfn, "a+");
	if (fp) {
	    fprintf(fp, "%s %s: %s\n", stamp, tag, outstr);
	    fclose(fp);
	}
    }

    va_end(va_ptr);
    free(outstr);
}



void log_begin(time_t t)
{
    struct tm	timeinfo;

    /*
     * If there is an open logfile from an crashed brew, open that one.
     */
    if (! strlen(sdcard_state->logfile) && strlen(runtime.Logfile)) {
	sprintf(sdcard_state->logfile, "%s", runtime.Logfile);
	log_msg(TAG, "Resumed %s", sdcard_state->logfile);
    } else {
    	localtime_r(&t, &timeinfo);
    	if (timeinfo.tm_year > (2016 - 1900)) {
	    // Valid time, construct the filename.
	    strftime(sdcard_state->logfile, sizeof(sdcard_state->logfile), "br%y%m%d%H%M", &timeinfo);
    	} else {
	    sprintf(sdcard_state->logfile, "brewlog");
    	}
	log_msg(TAG, "Create %s", sdcard_state->logfile);
    	sprintf(runtime.Logfile, "%s", sdcard_state->logfile);
    }
}



void log_close(void)
{
    xEventGroupSetBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLOSE);
}



void log_clean(void)
{
    xEventGroupSetBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLEAN);
}



void log_json(void)
{
    char        filename[64], strftime_buf[64];
    FILE	*f;
    bool	addcomma = true;

    if (strlen(sdcard_state->logfile)) {
	sprintf(filename, "/spiffs/log/%s.json", sdcard_state->logfile);
	f = fopen(filename, "r");
	if (f == NULL) {
	    // Create the file and add the header
	    f = fopen(filename, "a");
	    if (f) {
		strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
		fprintf(f, "{\"brew\":[{\"Recipe\":\"%s %s\",\"Date\":\"%s\",\"brewdata\":[\n", recipe.Code, recipe.Name, strftime_buf);
		addcomma = false;
		fclose(f);
	    } else {
		ESP_LOGE(TAG, "Failed to create %s error %d", filename, errno);
		return;
	    }
	} else {
	    fclose(f);	// Was open for reading.
	}

	f = fopen(filename, "a");
	if (f) {
	    if (addcomma) {
		fprintf(f, ",\n");
	    }
	    addcomma = true;
	    fprintf(f, "   {\"MLT_sp\":\"%.3f\",\"MLT_pv\":\"%.3f\",\"MLT_pwm\":\"%d\",\"MLT_tr\":\"%d\",\"Pump\":\"%d\",", 
			    json_log->mlt_sp, json_log->mlt_pv, json_log->mlt_power, json_log->mlt_tempreached, json_log->pump_run);
	    if (json_log->hlt_sp > 0.0) {
	    	fprintf(f, "\"HLT_sp\":\"%.3f\",\"HLT_pv\":\"%.3f\",\"HLT_pwm\":\"%d\",", json_log->hlt_sp, json_log->hlt_pv, json_log->hlt_power);
	    }
	    fprintf(f, "\"Label\":\"%s\"}", json_log->time);
	    fclose(f);
	}
    }
}



void log_annotation(int annotation_type, char *label)
{
    char        filename[64];
    char	bordercolor[9], color[9], pos[8];
    FILE        *f;
    bool        addcomma = true;
    int		Hour   = (TimeBrewing / 3600);
    int		Minute = ((TimeBrewing % 3600) / 60);

    if (strlen(sdcard_state->logfile)) {
	sprintf(filename, "/spiffs/log/%s.anno", sdcard_state->logfile);

	switch (annotation_type) {
	    case ANNOTATION_STAGE:	snprintf(bordercolor, 8, "#8942f4");
					snprintf(color, 8, "#00215b");
					snprintf(pos, 7, "bottom");
		    			break;

	    case ANNOTATION_EVENT:	snprintf(bordercolor, 8, "#42f445");
					snprintf(color, 8, "#00215b");
					snprintf(pos, 7, "top");
					break;

	    case ANNOTATION_SYSTEM:	snprintf(bordercolor, 8, "black");
					snprintf(color, 8, "red");
					snprintf(pos, 7, "center");
	 				break;
	}

	// Check if the file exists to see if we need to insert a comma.
	f = fopen(filename, "r");
	if (f == NULL) {
	    addcomma = false;
	} else {
	    fclose(f);
	}

	snprintf(json_log->time, 11, "%02d:%02d", Hour, Minute);
	f = fopen(filename, "a");
	if (f) {
	    if (addcomma) {
		fprintf(f, ",\n");
	    }
	    addcomma = true;
	    fprintf(f, "{\"type\":\"line\",\"mode\":\"vertical\",\"scaleID\":\"x-axis-0\",\"value\":\"%s\",\"borderColor\":\"%s\",\"borderWidth\":2,\"label\":{\"backgroundColor\":\"%s\",\"content\":\"%s\",\"position\":\"%s\",\"enabled\":true}}", json_log->time, bordercolor, color, label, pos);
	    fclose(f);
	}
    }
}



static esp_err_t my_init_sdspi_host(int slot, const void *slot_config, int *out_slot)
{
    esp_err_t err = sdspi_host_init_device((const sdspi_device_config_t*)slot_config, out_slot);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to attach sdspi device onto an SPI bus (rc=0x%x), please initialize the \
bus first and check the device parameters.", err);
    }
    return err;
}



/**
 * @brief This is a local modified version of the esp_vfs_fat_sdmmc_mount() function in
 *        the FreeRTOS components library. It is here so we can better handle errors
 *        for our application.
 * @param base_path The mount path
 * @param host_config SPI host configuration
 * @param slot_config Slot configuration
 * @return Error condition.
 */
esp_err_t my_vfs_fat_sdmmc_init(const char* base_path, const sdmmc_host_t* host_config, const void* slot_config, int *card_handle)
{
    if (s_card != NULL) {
	return ESP_ERR_INVALID_STATE;
    }

    // connect SDMMC driver to FATFS
    pdrv = FF_DRV_NOT_USED;
    if (ff_diskio_get_drive(&pdrv) != ESP_OK || pdrv == FF_DRV_NOT_USED) {
	ESP_LOGE(TAG, "the maximum count of volumes is already mounted");
	return ESP_ERR_NO_MEM;
    }

    s_base_path = strdup(base_path);
    if (!s_base_path) {
	ESP_LOGE(TAG, "could not copy base_path");
	return ESP_ERR_NO_MEM;
    }
    esp_err_t err = ESP_OK;
    s_card = malloc(sizeof(sdmmc_card_t));
    if (s_card == NULL) {
	err = ESP_ERR_NO_MEM;
	goto fail;
    }

    err = (*host_config->init)();
    if (err != ESP_OK) {
	ESP_LOGE(TAG, "host init returned rc=0x%x", err);
	goto fail;
    }

    // configure SD host
    err = my_init_sdspi_host(host_config->slot, slot_config, &card_handle);
    if (err != ESP_OK) {
	ESP_LOGE(TAG, "init_sdspi_host() rc=0x%x", err);
	goto fail;
    }
    return ESP_OK;

fail:
    host_config->deinit();
    free(s_card);
    s_card = NULL;
    return err;
}



/**
 * @brief Mount SD card.
 * @param base_path The mountpoint
 * @param host_config SPI host configuration
 * @param slot_config Slot configuration
 * @param mount_config Mount configuration
 * @param[out] out_card Card information
 * @return Error condition
 */
esp_err_t my_esp_vfs_fat_sdmmc_mount(const char* base_path,
	const sdmmc_host_t* host_config,
	const void* slot_config,
	const esp_vfs_fat_mount_config_t* mount_config, 
	sdmmc_card_t** out_card,
	int card_handle)
{
    FATFS* fs = NULL;
    esp_err_t err = ESP_OK;

    if (s_card == NULL) {
	return ESP_ERR_INVALID_STATE;
    }

    // probe and initialize card
    err = sdmmc_card_init(host_config, s_card);
    if (err != ESP_OK) {
	if (err != ESP_ERR_INVALID_RESPONSE) {  // No card present, do not log
	    ESP_LOGI(TAG, "sdmmc_card_init failed 0x(%x)", err);
	}
	goto fail;
    }
    if (out_card != NULL) {
	*out_card = s_card;
    }

    ff_diskio_register_sdmmc(pdrv, s_card);
    s_pdrv = pdrv;
    ESP_LOGD(TAG, "using pdrv=%i", pdrv);
    char drv[3] = {(char)('0' + pdrv), ':', 0};

    // connect FATFS to VFS
    err = esp_vfs_fat_register(base_path, drv, mount_config->max_files, &fs);
    if (err == ESP_ERR_INVALID_STATE) {
    	// it's okay, already registered with VFS
    } else if (err != ESP_OK) {
    	ESP_LOGI(TAG, "esp_vfs_fat_register failed 0x(%x)", err);
    	goto fail;
    }

    // Try to mount partition
    FRESULT res = f_mount(fs, drv, 1);
    if (res != FR_OK) {
	err = ESP_FAIL;
	ESP_LOGD(TAG, "f_mount failed (%d)", res);
	goto fail;
    }
    return ESP_OK;

fail:
    if (fs) {
	f_mount(NULL, drv, 0);
    }
    esp_vfs_fat_unregister_path(base_path);
    ff_diskio_unregister(pdrv);
    return err;
}



/**
 * @brief Unmount a mounted SD card,
 * @return Error condition
 */
esp_err_t my_esp_vfs_fat_sdmmc_unmount(void)
{
    if (s_card == NULL) {
	return ESP_ERR_INVALID_STATE;
    }
    // unmount
    char drv[3] = {(char)('0' + s_pdrv), ':', 0};
    f_mount(0, drv, 0);
    return ESP_OK;
}



int FileCopy(char *ff, char *tf)
{
    FILE	*f, *t;
    uint8_t	buf[512];
    size_t	bytes;

    f = fopen(ff, "r");
    if (f == NULL) {
	ESP_LOGW(TAG, "FileCopy cannot open %s for read, error %d", ff, errno);
	return 1;
    }

    t = fopen(tf, "w+");
    if (t == NULL) {
	ESP_LOGW(TAG, "FileCopy cannot open %s for create/write, error %d", tf, errno);
	fclose(f);
	return 1;
    }

    while ((bytes = fread(&buf, 1, 512, f))) {
	fwrite(&buf, 1, bytes, t);
	vTaskDelay(10 / portTICK_PERIOD_MS);
    }

    fclose(f);
    fclose(t);
    return 0;
}



void task_sdcard(void *pvParameter)
{
    sdmmc_card_t*	card;
    esp_err_t		ret;
    EventBits_t		uxBits;
    char		filename[64];
    int			card_handle = -1;   //uninitialized

    sdcard_state = malloc(sizeof(SDCARD_State));
    sdcard_state->host_ok = false;
    sdcard_state->card_present = false;
    sdcard_state->logfile[0] = '\0';

    json_log = malloc(sizeof(JSON_log));

    ESP_LOGI(TAG, "Start SD card");
    sdmmc_host_t host = SDSPI_HOST_DEFAULT();
    host.slot = SDCARD_HOST_SLOT;	// HSPI_HOST is in use by the TFT.
    spi_bus_config_t bus_cfg = {
        .mosi_io_num = SDCARD_PIN_NUM_MOSI,
        .miso_io_num = SDCARD_PIN_NUM_MISO,
        .sclk_io_num = SDCARD_PIN_NUM_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 4000,
    };
    ret = spi_bus_initialize(host.slot, &bus_cfg, SDCARD_DMA_CHANNEL);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to initialize bus.");
	vTaskDelete(NULL);
        return;
    }

    // This initializes the slot without card detect (CD) and write protect (WP) signals.
    // Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
    sdspi_device_config_t slot_config = SDSPI_DEVICE_CONFIG_DEFAULT();
    slot_config.gpio_cs     = SDCARD_PIN_NUM_CS;
    slot_config.host_id     = host.slot;

    /*
     * No errors from the sdmmc_cmd driver.
     */
    esp_log_level_set("sdmmc_cmd", ESP_LOG_NONE);

    /*
     * Options for mounting the filesystem.
     * If format_if_mount_failed is set to true, SD card will be partitioned and
     * formatted in case when mounting fails.
     */
    esp_vfs_fat_sdmmc_mount_config_t mount_config = {
	.format_if_mount_failed = false,
	.max_files = 5,
	.allocation_unit_size = 16 * 1024
    };

    ret = my_vfs_fat_sdmmc_init("/sdcard", &host, &slot_config, &card_handle);
    if (ret == ESP_OK) {
	ESP_LOGI(TAG, "SPI card interface ready");
	sdcard_state->host_ok = true;
    } else {
	ESP_LOGE(TAG, "SPI card interface failed, abort task");
	vTaskDelete(NULL);
	return;
    }

    xEventGroupSDcard = xEventGroupCreate();

    /*
     * Task loop, continues check of the inserted cards.
     */
    while (1) {

	if (sdcard_state->card_present == false) {
	    /*
	     * If the card is not mounted, try it.
	     */
	    ret = my_esp_vfs_fat_sdmmc_mount("/sdcard", &host, &slot_config, &mount_config, &card, card_handle);
	    if (ret == ESP_OK) {
		ESP_LOGI(TAG, "SD card mounted on /sdcard");
		sdcard_state->card_present = true;

		DIR* dir = opendir("/sdcard/w/log");
		if (dir == NULL) {
		    ret = mkdir("/sdcard/w/log", 0755);
		} else {
		    closedir(dir);
		}
	    }
	} else {
	    /*
	     * Check if the mounted card is still in the slot.
	     */
	    DIR* dir = opendir("/sdcard/w/log");
	    if (dir == NULL) {
		ESP_LOGI(TAG, "SD card missing, unmount");
		my_esp_vfs_fat_sdmmc_unmount();
		sdcard_state->card_present = false;
	    } else {
		closedir(dir);
	    }
	}
	
	uxBits = xEventGroupWaitBits(xEventGroupSDcard, 
			TASK_SDCARD_LOG_CLEAN | TASK_SDCARD_LOG_CLOSE, pdFALSE, pdFALSE, 1000 / portTICK_PERIOD_MS);
	
	if (uxBits & TASK_SDCARD_LOG_CLEAN) {
	    DIR   *dir = opendir("/spiffs/log");
	    char  lf[32];

	    if (dir != NULL) {
		struct dirent   *de = readdir(dir);
	    	while (de) {
		    sprintf(lf, "/spiffs/log/");
		    strncat(lf, de->d_name, 31 - strlen(lf));
		    if (unlink(lf) == ESP_OK) {
		    	ESP_LOGI(TAG, "Removed old %s", lf);
		    }
		    de = readdir(dir);
		    vTaskDelay(2 / portTICK_PERIOD_MS);
	    	}
	    	closedir(dir);
	    }
	    xEventGroupClearBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLEAN);
	}

	if (uxBits & TASK_SDCARD_LOG_CLOSE) {
	    // Close the logfile.
	    if (strlen(sdcard_state->logfile) && (sdcard_state->card_present == true)) {
		log_msg(TAG, "Closing logfile");
		char	destname[64];
		sprintf(destname, "/sdcard/w/log");
		int rc = mkdir(destname, 0755);
		if (rc && (errno != EEXIST)) {
		    ESP_LOGE(TAG, "Cannot create %s error %d", destname, errno);
		} else {
		    sprintf(filename, "/spiffs/log/%s.json", sdcard_state->logfile);
		    // First close the JSON data records
		    FILE *f = fopen(filename, "a+");
		    if (f) {
			fprintf(f, "],\"annotations\":[\n");	// End of brewdata
			// Insert annotation records
			sprintf(destname, "/spiffs/log/%s.anno", sdcard_state->logfile);
			FILE *a = fopen(destname, "r");
			char buf[256];
			if (a) {
			    while(true) {
				if (fgets(buf, sizeof(buf), a)) {
				    fprintf(f, "%s", buf);
				} else {
				    break;
				}
			    }
			    fclose(a);
			    unlink(destname);
			}
			fprintf(f, "]}]}\n");	// End of annotations and brew
			fclose(f);
		    }
		    sprintf(destname, "/sdcard/w/log/%s.json", sdcard_state->logfile);
		    if (FileCopy(filename, destname) == 0) {
			log_msg(TAG, "JSON file copied to %s", destname);
			unlink(filename);
		    }
		}
	    }
	    sdcard_state->logfile[0] = '\0';		// Clear logfile name
	    runtime.Logfile[0] = '\0';
	    xEventGroupClearBits(xEventGroupSDcard, TASK_SDCARD_LOG_CLOSE);
	}
    }
}

mercurial