main/task_sdcard.c

Fri, 28 Jun 2024 15:33:24 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Fri, 28 Jun 2024 15:33:24 +0200
branch
idf 5.1
changeset 137
e0f50087c909
parent 129
31f9d3e4a85f
permissions
-rw-r--r--

Fixed changing runtime datarecord size during switching between IDF 4.2 and 5.1. Fixed wiping the /spiffs filesystem. The directory listing from the SD card doesn't overwrite parts of the screen anymore. Solved the slow speed issue with the SD card. Try to force the SD card to operate at 20 MHz. More project settings changed to improve performance and memory usage.

/**
 * @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"


extern my_runtime_t     runtime;
extern my_recipe_t	recipe;


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

int			card_handle = -1;
static const char	*TAG = "task_sdcard";
static sdmmc_card_t* 	card = NULL;
static uint8_t		pdrv = FF_DRV_NOT_USED;
char			drv[3] = {'0', ':', 0};


#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[96], 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[96];
    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 mount_prepare_mem(const char *base_path, BYTE *out_pdrv, char **out_dup_path)
{
    esp_err_t err = ESP_OK;
    char* dup_path = NULL;

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

    }

    // not using ff_memalloc here, as allocation in internal RAM is preferred
    card = (sdmmc_card_t*)malloc(sizeof(sdmmc_card_t));
    if (card == NULL) {
        ESP_LOGD(TAG, "could not malloc sdmmc_card_t");
        err = ESP_ERR_NO_MEM;
        goto cleanup;
    }

    dup_path = strdup(base_path);
    if (!dup_path){
        ESP_LOGD(TAG, "could not copy base_path");
        err = ESP_ERR_NO_MEM;
        goto cleanup;
    }

    *out_pdrv = pdrv;
    *out_dup_path = dup_path;
    return ESP_OK;
cleanup:
    free(card);
    card = NULL;
    free(dup_path);
    return err;
}



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_sdspi_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_sdspi_init(const char* base_path, const sdmmc_host_t* host_config, const void* slot_config)
{
    esp_err_t err;
    char* dup_path = NULL;

    if (card != NULL) {
	ESP_LOGE(TAG, "card not NULL");
	return ESP_ERR_INVALID_STATE;
    }

    err = mount_prepare_mem(base_path, &pdrv, &dup_path);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "mount_prepare_mem 0x%x", err);
        return err;
    }

    //the init() function is usually empty, doesn't require any deinit to revert it
    err = (*host_config->init)();
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "host_config->init() 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, "my_init_sdspi_host() 0x%x", err);
	goto fail;
    }
    return ESP_OK;

fail:
    host_config->deinit();
    free(card);
    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_sdspi_mount(const char* base_path,
	const sdmmc_host_t* host_config_input,
	const sdspi_device_config_t* slot_config,
	const esp_vfs_fat_mount_config_t* mount_config,
	sdmmc_card_t** out_card)
{
    const sdmmc_host_t* host_config = host_config_input;
    FATFS* fs = NULL;
    esp_err_t err = ESP_OK;

    if (card == NULL) {
	ESP_LOGE(TAG, "card NULL");
	return ESP_ERR_INVALID_STATE;
    }

    /*
     * The `slot` argument inside host_config should be replaced by the SD SPI handled returned
     * above. But the input pointer is const, so create a new variable.
     */
    sdmmc_host_t new_config;
    if (card_handle != host_config->slot) {
        new_config = *host_config_input;
        host_config = &new_config;
        new_config.slot = card_handle;
    }

    // probe and initialize card
    err = sdmmc_card_init(host_config, 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 = card;
    }

    /*
     * mount to vfs fat
     */
    ff_diskio_register_sdmmc(pdrv, card);
    ESP_LOGD(TAG, "using pdrv=%i", pdrv);
    drv[0] = (char)('0' + pdrv);

    // 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
    // See: esp-idf/components/fatfs/src/ff.h
    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_sdspi_unmount(void)
{
    if (card == NULL) {
	return ESP_ERR_INVALID_STATE;
    }
    // unmount
    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[96];

    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.
    host.max_freq_khz = 20000;		// 20 MHz speed limit.
    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;

    /*
     * Since IDF 5.0 the SD card is dead slow. Use pullup resistors
     * on the board, see schematic.
     * Or just use gpio pullup as workaround.
     * Or both (not tested).
     * See https://github.com/espressif/esp-idf/issues/10493
     */
    gpio_set_pull_mode(SDCARD_PIN_NUM_MOSI, GPIO_PULLUP_ONLY);
    gpio_set_pull_mode(SDCARD_PIN_NUM_MISO, GPIO_PULLUP_ONLY);
    gpio_set_pull_mode(SDCARD_PIN_NUM_CLK, GPIO_PULLUP_ONLY);
    gpio_set_pull_mode(SDCARD_PIN_NUM_CS, GPIO_PULLUP_ONLY);

    /*
     * No errors from the sdspi_transaction driver.
     */
    esp_log_level_set("sdspi_transaction", 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_sdspi_init("/sdcard", &host, &slot_config);
    if (ret == ESP_OK) {
	ESP_LOGI(TAG, "SD card interface ready");
	sdcard_state->host_ok = true;
    } else {
	ESP_LOGE(TAG, "SD card interface failed: %s, abort task", esp_err_to_name(ret));
	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_sdspi_mount("/sdcard", &host, &slot_config, &mount_config, &card);
	    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", 0755);
		    ESP_LOGI(TAG, "mkdir(/sdcard/w)=%d", ret);
		    ret = mkdir("/sdcard/w/log", 0755);
		    ESP_LOGI(TAG, "mkdir(/sdcard/w/log)=%d", ret);
		} else {
		    closedir(dir);
		}
		esp_log_level_set("sdmmc_sd", ESP_LOG_INFO);
	    }
	} 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_sdspi_unmount();
		sdcard_state->card_present = false;
		esp_log_level_set("sdmmc_sd", ESP_LOG_NONE);
	    } 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[96];
		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