diff --git a/CMakeLists.txt b/CMakeLists.txt index d4d7d2c..6eb1987 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ idf_component_register( "src/MQTT.c" "src/CronTimers.c" "src/SerialPort.c" + src/sdcard.c src/FileBlockHandler.c src/OTA.c src/RestApiHandler.c @@ -45,6 +46,7 @@ idf_component_register( REQUIRES nvs_flash spiffs + fatfs esp_http_server mbedtls lwip diff --git a/Kconfig b/Kconfig index 9d7cdd6..e158df6 100644 --- a/Kconfig +++ b/Kconfig @@ -260,6 +260,57 @@ menu "WebGUIApp" endif endmenu + + menu "SDCARD settings" + config SDCARD_ENABLE + bool "Enabled SDCARD interface" + default n + help + Set enabled SDCARD + + if SDCARD_ENABLE + + config SDCARD_SPI_HOST + int "SPI Host Number" + range 0 2 + default 2 + help + Set the SPI host used. + + config SDCARD_SPI_CS_GPIO + int "SDCARD SPI CS GPIO number" + range GPIO_RANGE_MIN GPIO_RANGE_MAX + default 0 if IDF_TARGET_ESP32 + default 0 if IDF_TARGET_ESP32S3 + help + Set the GPIO number used by SPI SCLK. + + config SDCARD_SPI_SCLK_GPIO + int "SDCARD SPI SCLK GPIO number" + range GPIO_RANGE_MIN GPIO_RANGE_MAX + default 9 if IDF_TARGET_ESP32 + default 9 if IDF_TARGET_ESP32S3 + help + Set the GPIO number used by SPI SCLK. + + config SDCARD_SPI_MOSI_GPIO + int "SDCARD SPI MOSI GPIO number" + range GPIO_RANGE_MIN GPIO_RANGE_MAX + default 34 if IDF_TARGET_ESP32 + default 44 if IDF_TARGET_ESP32S3 + help + Set the GPIO number used by SPI MOSI. + + config SDCARD_SPI_MISO_GPIO + int "SDCARD SPI MISO GPIO number" + range GPIO_RANGE_MIN GPIO_RANGE_MAX + default 33 if IDF_TARGET_ESP32 + default 43 if IDF_TARGET_ESP32S3 + help + Set the GPIO number used by SPI MISO. + + endif + endmenu menu "WiFi settings" diff --git a/include/HTTPServer.h b/include/HTTPServer.h index 1753234..d441bdc 100644 --- a/include/HTTPServer.h +++ b/include/HTTPServer.h @@ -45,8 +45,8 @@ /* Max length a file path can have on storage */ #define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN) -#define MAX_FILE_SIZE (1000*1024) // 200 KB -#define MAX_FILE_SIZE_STR "1MB" +#define MAX_FILE_SIZE (100*1000*1024) // 200 KB +#define MAX_FILE_SIZE_STR "100MB" /* Scratch buffer size */ #define SCRATCH_BUFSIZE EXPECTED_MAX_DATA_SIZE @@ -109,6 +109,7 @@ esp_err_t upload_post_handler(httpd_req_t *req); esp_err_t delete_post_handler(httpd_req_t *req); esp_err_t ParseBlockDataObject(char *argres, blockdata_transaction_t *ft); -void FileBlockHandler(char *argres, int rw); +void FileBlockHandler(char *argres, int rw, const char* path); +void FileListHandler(char *argres, int rw, const char* path); #endif /* COMPONENTS_WEB_GUI_APPLICATION_INCLUDE_HTTPSERVER_H_ */ diff --git a/include/SystemApplication.h b/include/SystemApplication.h index 09ceafc..a0632bc 100644 --- a/include/SystemApplication.h +++ b/include/SystemApplication.h @@ -111,6 +111,7 @@ typedef struct }UART_DATA_SEND_STRUCT; void InitSerialPort(void); +void InitSysSDCard(); esp_err_t TransmitSerialPort(char *data, int ln); esp_err_t GetConfVar(char* name, char* val, rest_var_types *tp); diff --git a/src/FileBlockHandler.c b/src/FileBlockHandler.c index 93b5dc7..37ef97b 100644 --- a/src/FileBlockHandler.c +++ b/src/FileBlockHandler.c @@ -42,7 +42,6 @@ #define DELETE_ORERATION 2 #define WRITE_ORERATION 3 -static const char *dirpath = "/data/"; static blockdata_transaction_t FileTransaction = { .opertype = 0 @@ -78,9 +77,9 @@ esp_err_t ParseBlockDataObject(char *argres, blockdata_transaction_t *ft) if (result.elements == 1) { ft->parts = atoi((char*) result.pValue); - if (ft->parts < 0 || ft->parts > 500) + if (ft->parts < 0 || ft->parts > 20000) { - snprintf(argres, VAR_MAX_VALUE_LENGTH, "\"ERROR:'parts' value not in [0...500]\""); + snprintf(argres, VAR_MAX_VALUE_LENGTH, "\"ERROR:'parts' value not in [0...20000]\""); return ESP_ERR_INVALID_ARG; } } @@ -140,7 +139,7 @@ esp_err_t ParseBlockDataObject(char *argres, blockdata_transaction_t *ft) } -void FileBlockHandler(char *argres, int rw) +void FileBlockHandler(char *argres, int rw, const char* path) { if (ParseBlockDataObject(argres, &FileTransaction) != ESP_OK) @@ -155,7 +154,7 @@ void FileBlockHandler(char *argres, int rw) else if (FileTransaction.part == (FileTransaction.parts - 1)) FileTransaction.operphase = 2; //Last block of multipart data (close file) - strcpy(FileTransaction.filepath, dirpath); + strcpy(FileTransaction.filepath, path); strcat(FileTransaction.filepath, FileTransaction.mem_object); if (FileTransaction.operphase == 1 || FileTransaction.operphase == 3) @@ -297,3 +296,43 @@ void FileBlockHandler(char *argres, int rw) } } + +void FileListHandler(char *argres, int rw, const char* path) +{ + char entrypath[FILE_PATH_MAX]; + char entrysize[16]; + const char *entrytype = "file"; + struct dirent *entry; + struct stat entry_stat; + DIR *dir = opendir(path); + const size_t dirpath_len = strlen(path); + strlcpy(entrypath, path, sizeof(entrypath)); + + if (!dir) + { + ESP_LOGE("FILE_API", "Failed to stat dir : %s", path); + snprintf(argres, VAR_MAX_VALUE_LENGTH, "\"ERROR:DIR_NOT_FOUND\""); + return; + } + + struct jWriteControl jwc; + jwOpen(&jwc, argres, VAR_MAX_VALUE_LENGTH, JW_ARRAY, JW_COMPACT); + while ((entry = readdir(dir)) != NULL) + { + strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len); + entrytype = (entry->d_type == DT_DIR ? "directory" : "file"); + if (stat(entrypath, &entry_stat) == -1) + { + ESP_LOGE("FILE_API", "Failed to stat %s : %s", entrytype, entry->d_name); + continue; + } + + jwArr_object(&jwc); + jwObj_raw(&jwc, "sel", "false"); + jwObj_string(&jwc, "name", (char*) entry->d_name); + jwObj_int(&jwc, "size", entry_stat.st_size); + jwEnd(&jwc); + } + jwClose(&jwc); +} + diff --git a/src/HTTPServer.c b/src/HTTPServer.c index 5e675a1..e6b3975 100644 --- a/src/HTTPServer.c +++ b/src/HTTPServer.c @@ -460,7 +460,11 @@ esp_err_t start_file_server(void) return ESP_ERR_NO_MEM; } strlcpy(server_data->base_path, "/", sizeof("/")); +#if CONFIG_SDCARD_ENABLE + strlcpy(server_data->base_path2, "/sdcard", sizeof("/sdcard")); +#else strlcpy(server_data->base_path2, "/data", sizeof("/data")); +#endif server = start_webserver(); return ESP_OK; } diff --git a/src/RestApiHandler.c b/src/RestApiHandler.c index 1e2abad..94d9695 100644 --- a/src/RestApiHandler.c +++ b/src/RestApiHandler.c @@ -21,7 +21,6 @@ * \copyright Apache License, Version 2.0 */ - #include "SystemApplication.h" #include #include @@ -216,7 +215,6 @@ static void funct_wifiscanres(char *argres, int rw) strcpy(argres, "\"SYS_ERROR_UNKNOWN\""); } - #if CONFIG_WEBGUIAPP_GPRS_ENABLE void funct_gsm_module(char *argres, int rw) { @@ -255,7 +253,6 @@ void funct_gsm_rssi(char *argres, int rw) } #endif - #ifdef CONFIG_WEBGUIAPP_LORAWAN_ENABLE void funct_lora_stat(char *argres, int rw) { @@ -283,7 +280,6 @@ void funct_lora_appkey(char *argres, int rw) } #endif - static void funct_ota_state(char *argres, int rw) { snprintf(argres, VAR_MAX_VALUE_LENGTH, "\"%s\"", GetUpdateStatus()); @@ -358,13 +354,11 @@ static void funct_serial_mode(char *argres, int rw) } - static void funct_objsinfo(char *argres, int rw) { GetObjectsInfo(argres); } - static void funct_exec(char *argres, int rw) { if (rw) @@ -373,54 +367,28 @@ static void funct_exec(char *argres, int rw) snprintf(argres, VAR_MAX_VALUE_LENGTH, "\"EXECUTED\""); } -static const char *dirpath = "/data/"; - static void funct_file_list(char *argres, int rw) { - - char entrypath[FILE_PATH_MAX]; - char entrysize[16]; - const char *entrytype = "file"; - struct dirent *entry; - struct stat entry_stat; - DIR *dir = opendir(dirpath); - const size_t dirpath_len = strlen(dirpath); - strlcpy(entrypath, dirpath, sizeof(entrypath)); - - if (!dir) - { - ESP_LOGE("FILE_API", "Failed to stat dir : %s", dirpath); - snprintf(argres, VAR_MAX_VALUE_LENGTH, "\"ERROR:DIR_NOT_FOUND\""); - return; - } - - struct jWriteControl jwc; - jwOpen(&jwc, argres, VAR_MAX_VALUE_LENGTH, JW_ARRAY, JW_COMPACT); - while ((entry = readdir(dir)) != NULL) - { - strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len); - entrytype = (entry->d_type == DT_DIR ? "directory" : "file"); - if (stat(entrypath, &entry_stat) == -1) - { - ESP_LOGE("FILE_API", "Failed to stat %s : %s", entrytype, entry->d_name); - continue; - } - - jwArr_object(&jwc); - jwObj_raw(&jwc, "sel", "false"); - jwObj_string(&jwc, "name", (char*) entry->d_name); - jwObj_int(&jwc, "size", entry_stat.st_size); - jwEnd(&jwc); - } - jwClose(&jwc); - + FileListHandler(argres, rw, "/data/"); } static void funct_file_block(char *argres, int rw) { -FileBlockHandler(argres, rw); + FileBlockHandler(argres, rw, "/data/"); } +#if CONFIG_SDCARD_ENABLE +static void funct_sd_list(char *argres, int rw) +{ + FileListHandler(argres, rw, "/sdcard/"); +} + +static void funct_sd_block(char *argres, int rw) +{ + FileBlockHandler(argres, rw, "/sdcard/"); +} +#endif + const int hw_rev = CONFIG_BOARD_HARDWARE_REVISION; const bool VAR_TRUE = true; const bool VAR_FALSE = false; @@ -428,7 +396,7 @@ const bool VAR_FALSE = false; const rest_var_t SystemVariables[] = { - { 0, "exec", &funct_exec, VAR_FUNCT, RW, 0, 0 }, + { 0, "exec", &funct_exec, VAR_FUNCT, RW, 0, 0 }, { 0, "time", &funct_time, VAR_FUNCT, R, 0, 0 }, { 0, "uptime", &funct_uptime, VAR_FUNCT, R, 0, 0 }, { 0, "free_ram", &funct_fram, VAR_FUNCT, R, 0, 0 }, @@ -442,14 +410,12 @@ const rest_var_t SystemVariables[] = { 0, "hw_rev", ((int*) &hw_rev), VAR_INT, R, 1, 1024 }, //{ 0, "hw_opt", CONFIG_BOARD_HARDWARE_OPTION, VAR_STRING, R, 1, 256 }, - { 0, "net_bios_name", &SysConfig.NetBIOSName, VAR_STRING, RW, 3, 31 }, { 0, "sys_name", &SysConfig.SysName, VAR_STRING, RW, 3, 31 }, { 0, "sys_pass", &SysConfig.SysPass, VAR_PASS, RW, 3, 31 }, { 0, "primary_color", CONFIG_WEBGUIAPP_ACCENT_COLOR, VAR_STRING, RW, 3, 31 }, { 0, "dark_theme", (bool*) (&VAR_TRUE), VAR_BOOL, R, 0, 1 }, - { 0, "ota_url", &SysConfig.OTAURL, VAR_STRING, RW, 3, 128 }, { 0, "ota_auto_int", &SysConfig.OTAAutoInt, VAR_INT, RW, 0, 65535 }, { 0, "ota_state", &funct_ota_state, VAR_FUNCT, R, 0, 0 }, @@ -540,7 +506,7 @@ const rest_var_t SystemVariables[] = { 0, "wifi_scan_res", &funct_wifiscanres, VAR_FUNCT, R, 0, 0 }, { 0, "wifi_level", &funct_wifi_level, VAR_FUNCT, R, 0, 0 }, - #endif +#endif #if CONFIG_WEBGUIAPP_GPRS_ENABLE { 0, "gsm_enab", &SysConfig.gsmSettings.Flags1.bIsGSMEnabled, VAR_BOOL, RW, 0, 1 }, @@ -566,7 +532,7 @@ const rest_var_t SystemVariables[] = { 0, "gsm_visible", (bool*) (&VAR_TRUE), VAR_BOOL, R, 0, 1 }, #else { 0, "gsm_visible", (bool*) (&VAR_FALSE), VAR_BOOL, R, 0, 1 }, -#endif + #endif #ifdef CONFIG_WEBGUIAPP_UART_TRANSPORT_ENABLE { 0, "serial_enab", &SysConfig.serialSettings.Flags.IsSerialEnabled, VAR_BOOL, RW, 0, 1 }, @@ -577,8 +543,7 @@ const rest_var_t SystemVariables[] = { 0, "serial_visible", (bool*) (&VAR_TRUE), VAR_BOOL, R, 0, 1 }, #else { 0, "serial_visible", (bool*) (&VAR_FALSE), VAR_BOOL, R, 0, 1 }, -#endif - + #endif #ifdef CONFIG_WEBGUIAPP_LORAWAN_ENABLE { 0, "lora_enab", &SysConfig.lorawanSettings.Flags1.bIsLoRaWANEnabled, VAR_BOOL, RW, 0, 1 }, @@ -590,12 +555,7 @@ const rest_var_t SystemVariables[] = #else { 0, "lora_visible", (bool*) (&VAR_FALSE), VAR_BOOL, R, 0, 1 }, -#endif - - - - - + #endif #ifdef CONFIG_WEBGUIAPP_MBTCP_ENABLED { 0, "mbtcp_enab", &SysConfig.modbusSettings.IsModbusTCPEnabled, VAR_BOOL, RW, 0, 1 }, @@ -603,16 +563,23 @@ const rest_var_t SystemVariables[] = { 0, "mbtcp_visible", (bool*) (&VAR_TRUE), VAR_BOOL, R, 0, 1 }, #else { 0, "mbtcp_visible", (bool*) (&VAR_FALSE), VAR_BOOL, R, 0, 1 }, -#endif + #endif { 0, "cronrecs", &funct_cronrecs, VAR_FUNCT, RW, 0, 0 }, { 0, "objsinfo", &funct_objsinfo, VAR_FUNCT, R, 0, 0 }, { 0, "file_list", &funct_file_list, VAR_FUNCT, R, 0, 0 }, { 0, "file_block", &funct_file_block, VAR_FUNCT, R, 0, 0 }, +#if CONFIG_SDCARD_ENABLE + { 0, "sd_list", &funct_sd_list, VAR_FUNCT, R, 0, 0 }, + { 0, "sd_block", &funct_sd_block, VAR_FUNCT, R, 0, 0 }, + { 0, "sd_visible", (bool*) (&VAR_TRUE), VAR_BOOL, R, 0, 1 } +#else + { 0, "sd_visible", (bool*) (&VAR_FALSE), VAR_BOOL, R, 0, 1 }, +#endif + }; - esp_err_t SetConfVar(char *name, char *val, rest_var_types *tp) { rest_var_t *V = NULL; diff --git a/src/SysConfiguration.c b/src/SysConfiguration.c index 01b9262..a550e5b 100644 --- a/src/SysConfiguration.c +++ b/src/SysConfiguration.c @@ -99,6 +99,10 @@ esp_err_t WebGuiAppInit(void) #if CONFIG_WEBGUIAPP_I2C_ENABLE InitSysI2C(); #endif +#if CONFIG_SDCARD_ENABLE + InitSysSDCard(); +#endif + esp_err_t err = nvs_flash_init(); ESP_ERROR_CHECK(esp_netif_init()); diff --git a/src/sdcard.c b/src/sdcard.c new file mode 100644 index 0000000..7fe60aa --- /dev/null +++ b/src/sdcard.c @@ -0,0 +1,119 @@ + /* Copyright 2024 Bogdan Pilyugin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * File name: sdcard.c + * Project: webguiapp + * Created on: 2024-05-07 + * Author: bogd + * Description: + */ + +#include +#include +#include +#include "esp_vfs_fat.h" +#include "sdmmc_cmd.h" +#include "SysConfiguration.h" + + +#if CONFIG_SDCARD_ENABLE +static const char *TAG = "sdcard"; +#define MOUNT_POINT "/sdcard" + +// Pin assignments can be set in menuconfig, see "SD SPI Example Configuration" menu. +// You can also change the pin assignments here by changing the following 4 lines. +#define PIN_NUM_MISO CONFIG_SDCARD_SPI_MISO_GPIO +#define PIN_NUM_MOSI CONFIG_SDCARD_SPI_MOSI_GPIO +#define PIN_NUM_CLK CONFIG_SDCARD_SPI_SCLK_GPIO +#define PIN_NUM_CS CONFIG_SDCARD_SPI_CS_GPIO + +void InitSysSDCard() +{ + esp_err_t ret; + + // 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 = { + #ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED + .format_if_mount_failed = true, +#else + .format_if_mount_failed = false, + #endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED + .max_files = 5, + .allocation_unit_size = 16 * 1024 + }; + sdmmc_card_t *card; + const char mount_point[] = MOUNT_POINT; + ESP_LOGI(TAG, "Initializing SD card"); + + // Use settings defined above to initialize SD card and mount FAT filesystem. + // Note: esp_vfs_fat_sdmmc/sdspi_mount is all-in-one convenience functions. + // Please check its source code and implement error recovery when developing + // production applications. + ESP_LOGI(TAG, "Using SPI peripheral"); + + // By default, SD card frequency is initialized to SDMMC_FREQ_DEFAULT (20MHz) + // For setting a specific frequency, use host.max_freq_khz (range 400kHz - 20MHz for SDSPI) + // Example: for fixed frequency of 10MHz, use host.max_freq_khz = 10000; + sdmmc_host_t host = SDSPI_HOST_DEFAULT(); + host.slot = CONFIG_SDCARD_SPI_HOST; + + spi_bus_config_t bus_cfg = { + .mosi_io_num = PIN_NUM_MOSI, + .miso_io_num = PIN_NUM_MISO, + .sclk_io_num = PIN_NUM_CLK, + .quadwp_io_num = -1, + .quadhd_io_num = -1, + .max_transfer_sz = 4000, + }; + ret = spi_bus_initialize(host.slot, &bus_cfg, SDSPI_DEFAULT_DMA); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "Failed to initialize bus."); + 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 = PIN_NUM_CS; + slot_config.host_id = host.slot; + + ESP_LOGI(TAG, "Mounting filesystem"); + ret = esp_vfs_fat_sdspi_mount(mount_point, &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 the CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option."); + } + 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; + } + ESP_LOGI(TAG, "Filesystem mounted"); + + // Card has been initialized, print its properties + sdmmc_card_print_info(stdout, card); +} +#endif diff --git a/upload_script.html b/upload_script.html index c8d56bd..c527180 100644 --- a/upload_script.html +++ b/upload_script.html @@ -35,8 +35,8 @@ function upload() { /* Max size of an individual file. Make sure this * value is same as that set in file_server.c */ - var MAX_FILE_SIZE = 1000*1024; - var MAX_FILE_SIZE_STR = "1MB"; + var MAX_FILE_SIZE = 100*1000*1024; + var MAX_FILE_SIZE_STR = "100MB"; if (fileInput.length == 0) { alert("No file selected!"); @@ -46,8 +46,8 @@ function upload() { alert("File path on server cannot have spaces!"); } else if (filePath[filePath.length-1] == '/') { alert("File name not specified after path!"); - } else if (fileInput[0].size > 1000*1024) { - alert("File size must be less than 1MB!"); + } else if (fileInput[0].size > MAX_FILE_SIZE) { + alert("File size must be less than 100MB!"); } else { document.getElementById("newfile").disabled = true; document.getElementById("filepath").disabled = true;