main/task_sdcard.c

Thu, 29 Jul 2021 22:36:17 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Thu, 29 Jul 2021 22:36:17 +0200
changeset 114
1413c4c5cd8c
parent 106
191dccfe4252
child 119
1cef3c25426b
permissions
-rw-r--r--

Fixed Brewfather beerxml import.

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

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[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 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[64];

    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.flags = SDMMC_HOST_FLAG_SPI | SDMMC_HOST_FLAG_DEINIT_ARG | SDMMC_HOST_FLAG_1BIT;
    //host.max_freq_khz = 10000;
    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 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, "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_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[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