diff --git a/examples/storage/nvs/.build-test-rules.yml b/examples/storage/nvs/.build-test-rules.yml index 761c75830b..aada4fee7e 100644 --- a/examples/storage/nvs/.build-test-rules.yml +++ b/examples/storage/nvs/.build-test-rules.yml @@ -9,6 +9,14 @@ examples/storage/nvs/nvs_bootloader: - if: CONFIG_NAME == "nvs_enc_hmac" and (SOC_HMAC_SUPPORTED != 1 or (SOC_HMAC_SUPPORTED == 1 and (SOC_AES_SUPPORTED != 1 and ESP_ROM_HAS_MBEDTLS_CRYPTO_LIB != 1))) reason: As of now in such cases, we do not have any way to perform AES operations in the bootloader build +examples/storage/nvs/nvs_console: + depends_components: + - nvs_flash + - console + - vfs + disable_test: + - if: IDF_TARGET not in ["esp32", "esp32c3"] + reason: only one target per arch needed examples/storage/nvs/nvs_rw_blob: depends_components: - nvs_flash diff --git a/examples/storage/nvs/nvs_console/CMakeLists.txt b/examples/storage/nvs/nvs_console/CMakeLists.txt new file mode 100644 index 0000000000..1382eb42de --- /dev/null +++ b/examples/storage/nvs/nvs_console/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/examples/system/console/advanced/components") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(nvs_console_example) diff --git a/examples/storage/nvs/nvs_console/README.md b/examples/storage/nvs/nvs_console/README.md new file mode 100644 index 0000000000..15a8f57ac1 --- /dev/null +++ b/examples/storage/nvs/nvs_console/README.md @@ -0,0 +1,111 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | + +# NVS Console Example + +This example demonstrates how to use Non-Volatile Storage (NVS) through an interactive console interface. It provides a set of commands to read, write, and manage data in NVS. + +## Overview + +The example implements a command-line interface with the following features: +- Interactive console with command history +- Command auto-completion +- Command hints +- Persistent command history (optional) +- Color-coded output (on supported terminals) + +## Hardware Required + +This example can run on any ESP32 family development board. + +## Configuration + +The example can be configured through `menuconfig`: +1. Enable/disable command history storage (`CONFIG_CONSOLE_STORE_HISTORY`) +2. Configure UART parameters +3. Configure console prompt color settings + +## How to Use + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +```bash +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +### Console Commands + +The following commands are available: + +1. NVS Operations: + - `nvs_namespace ` - Set current namespace + - Example: `nvs_namespace storage` + + - `nvs_set -v ` - Set a value in NVS + - type can be: i8, u8, i16, u16 i32, u32 i64, u64, str, blob + - Example: `nvs_set counter i32 -v 42` + - Example: `nvs_set name str -v "esp"` + + - `nvs_get ` - Get a value from NVS + - Example: `nvs_get counter` + + - `nvs_erase ` - Erase a key from NVS + - Example: `nvs_erase counter` + + - `nvs_list [-n ] [-t ]` - List stored key-value pairs stored in NVS. + - Example: `nvs_list nvs -n storage -t i8` + + - `nvs_erase_namespace ` - Erases specified namespace + - Example: `nvs_erase_namespace storage` + +2. System Commands: + - `help` - List all commands + - `free` - Get the current size of free heap memory + - `restart` - Software reset of the chip + - `version` - Get version of chip and SDK + - `heap` - Get minimum size of free heap memory that was available during program execution + +### Example Output + +``` bash +... +NVS Console Example +------------------- +Type 'help' to get the list of commands. +Use UP/DOWN arrows to navigate through command history. +Press TAB when typing command name to auto-complete. +Press Ctrl+C to exit the console. + +nvs> version +IDF Version:v5.5-dev-2627-g2cbfce97768-dirt +Chip info: + model:ESP32 + cores:2 + feature:/802.11bgn/BLE/BT/External-Flash:2 MB + revision number:0 +nvs> free +298172 +nvs> heap +min heap size: 298156 +nvs> nvs_list nvs +namespace 'storage_1', key 'u8_key', type 'u8' +namespace 'storage_1', key 'i8_key', type 'i8' +namespace 'storage_1', key 'u16_key', type 'u16' +namespace 'storage_2', key 'u32_key', type 'u32' +namespace 'storage_2', key 'i32_key', type 'i32' +namespace 'storage_2', key 'str_key', type 'str' +nvs> nvs_namespace storage_1 +I (85497) cmd_nvs: Namespace set to 'storage_1' +nvs> nvs_get i8_key i8 +-128 +nvs> nvs_set i8_key i8 -v -50 +I (233297) cmd_nvs: Value stored under key 'i8_key' +nvs> nvs_get i8_key i8 +-50 +nvs> +... +``` diff --git a/examples/storage/nvs/nvs_console/main/CMakeLists.txt b/examples/storage/nvs/nvs_console/main/CMakeLists.txt new file mode 100644 index 0000000000..1c17609fd8 --- /dev/null +++ b/examples/storage/nvs/nvs_console/main/CMakeLists.txt @@ -0,0 +1,10 @@ +idf_component_register(SRCS "nvs_console_main.c" + "console_settings.c" + REQUIRES cmd_nvs cmd_system esp_driver_uart fatfs + INCLUDE_DIRS ".") + +# Create a NVS image from the contents of the `nvs_data` CSV file +# that fits the partition named 'nvs'. FLASH_IN_PROJECT indicates that +# the generated image should be flashed when the entire project is flashed to +# the target with 'idf.py -p PORT flash'. +nvs_create_partition_image(nvs ../nvs_data.csv FLASH_IN_PROJECT) diff --git a/examples/storage/nvs/nvs_console/main/console_settings.c b/examples/storage/nvs/nvs_console/main/console_settings.c new file mode 100644 index 0000000000..fbd0f8d46e --- /dev/null +++ b/examples/storage/nvs/nvs_console/main/console_settings.c @@ -0,0 +1,125 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include "esp_system.h" +#include "esp_log.h" +#include "esp_console.h" +#include "esp_vfs_dev.h" +#include "driver/uart.h" +#include "driver/uart_vfs.h" +#include "linenoise/linenoise.h" +#include "argtable3/argtable3.h" +#include "console_settings.h" + +#define CONSOLE_MAX_CMDLINE_ARGS 8 +#define CONSOLE_MAX_CMDLINE_LENGTH 256 +#define CONSOLE_PROMPT_MAX_LEN (32) + +static const char *TAG = "console_settings"; +static char prompt[CONSOLE_PROMPT_MAX_LEN]; + +void initialize_console_peripheral(void) +{ + /* Drain stdout before reconfiguring it */ + fflush(stdout); + fsync(fileno(stdout)); + + /* Disable buffering on stdin */ + setvbuf(stdin, NULL, _IONBF, 0); + + /* Minicom, screen, idf_monitor send CR when ENTER key is pressed */ + uart_vfs_dev_port_set_rx_line_endings(CONFIG_ESP_CONSOLE_UART_NUM, ESP_LINE_ENDINGS_CR); + /* Move the caret to the beginning of the next line on '\n' */ + uart_vfs_dev_port_set_tx_line_endings(CONFIG_ESP_CONSOLE_UART_NUM, ESP_LINE_ENDINGS_CRLF); + + /* Configure UART */ + const uart_config_t uart_config = { + .baud_rate = CONFIG_ESP_CONSOLE_UART_BAUDRATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, +#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 + .source_clk = UART_SCLK_REF_TICK, +#else + .source_clk = UART_SCLK_XTAL, +#endif + }; + + /* Install UART driver for interrupt-driven reads and writes */ + ESP_ERROR_CHECK(uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, + 256, 0, 0, NULL, 0)); + ESP_ERROR_CHECK(uart_param_config(CONFIG_ESP_CONSOLE_UART_NUM, &uart_config)); + + /* Tell VFS to use UART driver */ + uart_vfs_dev_use_driver(CONFIG_ESP_CONSOLE_UART_NUM); +} + +void initialize_console_library(const char *history_path) +{ + /* Initialize the console */ + esp_console_config_t console_config = { + .max_cmdline_args = CONSOLE_MAX_CMDLINE_ARGS, + .max_cmdline_length = CONSOLE_MAX_CMDLINE_LENGTH, +#if CONFIG_LOG_COLORS + .hint_color = atoi(LOG_COLOR_CYAN) +#endif + }; + ESP_ERROR_CHECK(esp_console_init(&console_config)); + + /* Configure linenoise line completion library */ + linenoiseSetMultiLine(1); + + /* Tell linenoise where to get command completions and hints */ + linenoiseSetCompletionCallback(&esp_console_get_completion); + linenoiseSetHintsCallback((linenoiseHintsCallback*) &esp_console_get_hint); + + /* Set command history size */ + linenoiseHistorySetMaxLen(100); + + /* Set command maximum length */ + linenoiseSetMaxLineLen(console_config.max_cmdline_length); + + /* Don't return empty lines */ + linenoiseAllowEmpty(false); + +#if CONFIG_CONSOLE_STORE_HISTORY + /* Load command history from filesystem */ + if (history_path) { + linenoiseHistoryLoad(history_path); + } +#endif + + /* Figure out if the terminal supports escape sequences */ + int probe_status = linenoiseProbe(); + if (probe_status) { /* zero indicates success */ + ESP_LOGW(TAG, "Your terminal application does not support escape sequences.\n" + "Line editing and history features are disabled.\n" + "On Windows, try using Windows Terminal or Putty instead."); + linenoiseSetDumbMode(1); + } +} + +char *setup_prompt(const char *prompt_str) +{ + const char *prompt_temp = "esp>"; + if (prompt_str) { + prompt_temp = prompt_str; + } + + /* Set command line prompt */ + if (linenoiseIsDumbMode()) { + /* Since the terminal doesn't support escape sequences, + * don't use color codes in the prompt */ + snprintf(prompt, CONSOLE_PROMPT_MAX_LEN - 1, "%s ", prompt_temp); + } else { + snprintf(prompt, CONSOLE_PROMPT_MAX_LEN - 1, LOG_COLOR_I "%s " LOG_RESET_COLOR, prompt_temp); + } + + return prompt; +} diff --git a/examples/storage/nvs/nvs_console/main/console_settings.h b/examples/storage/nvs/nvs_console/main/console_settings.h new file mode 100644 index 0000000000..bc70373648 --- /dev/null +++ b/examples/storage/nvs/nvs_console/main/console_settings.h @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Initialize console peripheral type + * + * Console peripheral is based on sdkconfig settings + * + * UART CONFIG_ESP_CONSOLE_UART_DEFAULT + * USB_OTG CONFIG_ESP_CONSOLE_USB_CDC + * USB_SERIAL_JTAG CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG + */ +void initialize_console_peripheral(void); + +/** + * @brief Initialize linenoise and esp console + * + * This function initialize linenoise library and esp_console component, + * also checks if the terminal supports escape sequences + * + * @param history_path Path to store command history + */ +void initialize_console_library(const char *history_path); + +/** + * @brief Initialize console prompt + * + * This function adds color code to the prompt (if the console supports escape sequences) + * + * @param prompt_str Prompt in form of string eg esp32s3> + * + * @return + * - pointer to initialized prompt + */ +char *setup_prompt(const char *prompt_str); + +#ifdef __cplusplus +} +#endif diff --git a/examples/storage/nvs/nvs_console/main/nvs_console_main.c b/examples/storage/nvs/nvs_console/main/nvs_console_main.c new file mode 100644 index 0000000000..d84eade4ec --- /dev/null +++ b/examples/storage/nvs/nvs_console/main/nvs_console_main.c @@ -0,0 +1,133 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include "esp_system.h" +#include "esp_log.h" +#include "esp_console.h" +#include "esp_vfs_fat.h" +#include "linenoise/linenoise.h" +#include "argtable3/argtable3.h" +#include "nvs.h" +#include "nvs_flash.h" +#include "cmd_nvs.h" +#include "cmd_system.h" +#include "console_settings.h" + +static const char* TAG = "nvs_console"; +#define PROMPT_STR "nvs" + +/* Console command history can be stored to and loaded from a file. + * The easiest way to do this is to use FATFS filesystem on top of + * wear_levelling library. + */ +#if CONFIG_CONSOLE_STORE_HISTORY +#define MOUNT_PATH "/data" +#define HISTORY_PATH MOUNT_PATH "/history.txt" + +static void initialize_filesystem(void) +{ + static wl_handle_t wl_handle; + const esp_vfs_fat_mount_config_t mount_config = { + .max_files = 4, + .format_if_mount_failed = true + }; + esp_err_t err = esp_vfs_fat_spiflash_mount_rw_wl(MOUNT_PATH, "storage", &mount_config, &wl_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to mount FATFS (%s)", esp_err_to_name(err)); + return; + } +} +#else +#define HISTORY_PATH NULL +#endif // CONFIG_CONSOLE_STORE_HISTORY + +static void initialize_nvs(void) +{ + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + err = nvs_flash_init(); + } + ESP_ERROR_CHECK(err); +} + +void app_main(void) +{ + initialize_nvs(); + +#if CONFIG_CONSOLE_STORE_HISTORY + initialize_filesystem(); + ESP_LOGI(TAG, "Command history enabled"); +#else + ESP_LOGI(TAG, "Command history disabled"); +#endif + + /* Initialize console peripheral and library */ + initialize_console_peripheral(); + initialize_console_library(HISTORY_PATH); + + /* Register commands */ + esp_console_register_help_command(); + register_system_common(); + register_nvs(); + + /* Set up prompt */ + const char* prompt = setup_prompt(PROMPT_STR ">"); + + /* Clear screen */ + printf("\033[2J\033[1;1H"); + + printf("\n" + "NVS Console Example\n" + "-------------------\n" + "Type 'help' to get the list of commands.\n" + "Use UP/DOWN arrows to navigate through command history.\n" + "Press TAB when typing command name to auto-complete.\n" + "Press Ctrl+C to exit the console.\n\n"); + + if (linenoiseIsDumbMode()) { + printf("\n" + "Your terminal application does not support escape sequences.\n" + "Line editing and history features are disabled.\n" + "On Windows, try using Windows Terminal or Putty instead.\n\n"); + } + + /* Main loop */ + while(true) { + char* line = linenoise(prompt); + if (line == NULL) { + continue; + } + + /* Add the command to the history if not empty */ + if (strlen(line) > 0) { + linenoiseHistoryAdd(line); +#if CONFIG_CONSOLE_STORE_HISTORY + linenoiseHistorySave(HISTORY_PATH); +#endif + } + + /* Try to run the command */ + int ret; + esp_err_t err = esp_console_run(line, &ret); + if (err == ESP_ERR_NOT_FOUND) { + printf("Unrecognized command\n"); + } else if (err == ESP_ERR_INVALID_ARG) { + // command was empty + } else if (err == ESP_OK && ret != ESP_OK) { + printf("Command returned non-zero error code: 0x%x (%s)\n", ret, esp_err_to_name(ret)); + } else if (err != ESP_OK) { + printf("Internal error: %s\n", esp_err_to_name(err)); + } + + linenoiseFree(line); + } + + ESP_LOGE(TAG, "Error or end-of-input, terminating console"); + esp_console_deinit(); +} diff --git a/examples/storage/nvs/nvs_console/nvs_data.csv b/examples/storage/nvs/nvs_console/nvs_data.csv new file mode 100644 index 0000000000..2afa398975 --- /dev/null +++ b/examples/storage/nvs/nvs_console/nvs_data.csv @@ -0,0 +1,15 @@ +# Sample csv file +key,type,encoding,value +storage_1,namespace,, +u8_key,data,u8,255 +i8_key,data,i8,-128 +u16_key,data,u16,65535 + +storage_2,namespace,, +u32_key,data,u32,4294967295 +i32_key,data,i32,-2147483648 +str_key,data,string,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. +Fusce quis risus justo. +Suspendisse egestas in nisi sit amet auctor. +Pellentesque rhoncus dictum sodales. +In justo erat, viverra at interdum eget, interdum vel dui." diff --git a/examples/storage/nvs/nvs_console/pytest_nvs_console.py b/examples/storage/nvs/nvs_console/pytest_nvs_console.py new file mode 100644 index 0000000000..8469d891cd --- /dev/null +++ b/examples/storage/nvs/nvs_console/pytest_nvs_console.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +from time import sleep + +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize + + +@pytest.mark.generic +@idf_parametrize('target', ['esp32', 'esp32c3'], indirect=['target']) +def test_nvs_console(dut: Dut) -> None: + # Wait until the console prompt appears + dut.expect('nvs> ') + + # Write CLI command "version" + dut.write('version') + sleep(0.5) + + # Check if following strings are present in the "version" command output + dut.expect('IDF Version') + dut.expect('Chip info')