From 62d12976088fdc3355206af284954185270442a0 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 19 Jan 2026 23:28:52 -0600 Subject: [PATCH 01/20] feat(remote_debug): Add new RemoteDebug component --- components/remote_debug/CMakeLists.txt | 5 + .../remote_debug/example/CMakeLists.txt | 22 + components/remote_debug/example/README.md | 139 +++++ .../remote_debug/example/main/CMakeLists.txt | 3 + .../example/main/Kconfig.projbuild | 204 +++++++ .../example/main/remote_debug_example.cpp | 116 ++++ .../remote_debug/example/partitions.csv | 5 + .../remote_debug/example/sdkconfig.defaults | 27 + .../example/sdkconfig.defaults.esp32s3 | 3 + components/remote_debug/idf_component.yml | 20 + .../remote_debug/include/remote_debug.hpp | 157 ++++++ components/remote_debug/src/remote_debug.cpp | 523 ++++++++++++++++++ 12 files changed, 1224 insertions(+) create mode 100644 components/remote_debug/CMakeLists.txt create mode 100644 components/remote_debug/example/CMakeLists.txt create mode 100644 components/remote_debug/example/README.md create mode 100644 components/remote_debug/example/main/CMakeLists.txt create mode 100644 components/remote_debug/example/main/Kconfig.projbuild create mode 100644 components/remote_debug/example/main/remote_debug_example.cpp create mode 100644 components/remote_debug/example/partitions.csv create mode 100644 components/remote_debug/example/sdkconfig.defaults create mode 100644 components/remote_debug/example/sdkconfig.defaults.esp32s3 create mode 100644 components/remote_debug/idf_component.yml create mode 100644 components/remote_debug/include/remote_debug.hpp create mode 100644 components/remote_debug/src/remote_debug.cpp diff --git a/components/remote_debug/CMakeLists.txt b/components/remote_debug/CMakeLists.txt new file mode 100644 index 000000000..8966a196a --- /dev/null +++ b/components/remote_debug/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + REQUIRES esp_http_server driver adc base_component task +) diff --git a/components/remote_debug/example/CMakeLists.txt b/components/remote_debug/example/CMakeLists.txt new file mode 100644 index 000000000..e3a87b18c --- /dev/null +++ b/components/remote_debug/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# 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.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py remote_debug task nvs_flash esp_wifi esp_event esp_netif" + CACHE STRING + "List of components to include" + ) + +project(remote_debug_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/remote_debug/example/README.md b/components/remote_debug/example/README.md new file mode 100644 index 000000000..cb6dfe258 --- /dev/null +++ b/components/remote_debug/example/README.md @@ -0,0 +1,139 @@ +# Remote Debug Example + +Web-based remote debugging interface for GPIO control and real-time ADC monitoring. + +## How to Use + +### Hardware Setup + +Connect GPIOs and ADC channels you want to monitor/control. Default pins: +- GPIOs: 2, 4, 16, 17 (configurable) +- ADCs: 36, 39 (configurable) + +Connect LEDs, sensors, or other peripherals to these pins for testing. + +### Configure the Project + +```bash +idf.py menuconfig +``` + +Navigate to `Remote Debug Example Configuration`: +- Set WiFi SSID and password +- Configure server port (default: 8080) +- Set number of GPIOs to expose (1-10) +- Configure which GPIO pins to use +- Set number of ADC channels (0-8) +- Configure which ADC pins to monitor +- Set ADC sample rate and buffer size + +### Build and Flash + +```bash +idf.py build flash monitor +``` + +### Access the Interface + +1. Device connects to your WiFi network +2. Check serial monitor for IP address +3. Open browser to `http://:8080` +4. Use the web interface to control GPIOs and monitor ADCs + +## Features + +### GPIO Control + +- **Read**: Get current state of GPIO pins +- **Write**: Set GPIO pins HIGH or LOW +- **Toggle**: Flip GPIO state +- **Visual indicators**: Shows current state in real-time + +### ADC Monitoring + +- **Real-time plotting**: Live graph of ADC values +- **Multiple channels**: Monitor up to 8 channels simultaneously +- **Configurable sample rate**: 1-1000 Hz +- **Voltage display**: Shows current values in volts +- **Time-series data**: Scrolling graph with configurable buffer + +### JSON API + +For programmatic access: +``` +GET /api/gpio - List all GPIOs and states +GET /api/gpio/ - Read specific GPIO +POST /api/gpio/ - Write GPIO (body: {"state": 1}) +GET /api/adc - Get current ADC values +GET /api/adc/data - Get ADC plot data (all samples) +``` + +## Example Output + +``` +I (380) Remote Debug Example: Starting Remote Debug Example +I (385) Remote Debug Example: Connecting to WiFi SSID: MyWiFi +I (2450) Remote Debug Example: Got IP: 192.168.1.105 +I (2451) Remote Debug Example: Connected to WiFi! IP: 192.168.1.105 +I (2456) Remote Debug Example: Initialized 2 ADC channels +I (2461) Remote Debug Example: Remote Debug Server started! +I (2462) Remote Debug Example: Open browser to: http://192.168.1.105:8080 +I (2463) Remote Debug Example: GPIO pins available: 4 +I (2464) Remote Debug Example: ADC channels available: 2 +``` + +## Use Cases + +### Development & Testing +- Toggle GPIOs to control relays, LEDs, motors +- Monitor sensor values in real-time +- Debug analog circuits +- Test peripheral connections + +### Remote Monitoring +- Monitor battery voltage +- Track temperature sensors +- Log environmental data +- Remote equipment status + +### Interactive Demos +- Control devices from web browser +- Live sensor visualization +- Educational demonstrations +- Prototyping and proof-of-concept + +## Web Interface Features + +- **Clean, modern UI**: Responsive design for desktop and mobile +- **Real-time updates**: ADC plots update automatically +- **Color-coded states**: Visual feedback for GPIO states +- **Labeled pins**: Easy identification of each GPIO/ADC +- **Toggle buttons**: Quick GPIO control +- **Zoom/pan**: Interactive ADC plots + +## Customization + +### Adding Custom Controls + +Modify the HTML/JavaScript in `remote_debug.cpp` to add: +- Custom widgets +- Additional data visualization +- Multi-GPIO patterns +- PWM control +- I2C/SPI device control + +### Integration + +The component can be integrated into any application that needs remote debugging capabilities. Just initialize with your GPIO/ADC configuration and start the server. + +## Troubleshooting + +- **Can't connect**: Check WiFi credentials and verify IP address +- **No ADC readings**: Verify GPIO numbers support ADC on your ESP32 variant +- **GPIO not responding**: Check pin isn't used by other peripherals +- **Slow updates**: Reduce ADC sample rate or buffer size +- **Port conflict**: Change server port if 8080 is in use + +## Security Note + +This is a development/debugging tool. For production use, add authentication and use HTTPS. The current implementation has no access control. diff --git a/components/remote_debug/example/main/CMakeLists.txt b/components/remote_debug/example/main/CMakeLists.txt new file mode 100644 index 000000000..ab2e00da8 --- /dev/null +++ b/components/remote_debug/example/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS "." + PRIV_REQUIRES remote_debug wifi nvs) diff --git a/components/remote_debug/example/main/Kconfig.projbuild b/components/remote_debug/example/main/Kconfig.projbuild new file mode 100644 index 000000000..c11c5262b --- /dev/null +++ b/components/remote_debug/example/main/Kconfig.projbuild @@ -0,0 +1,204 @@ +menu "Remote Debug Example Configuration" + + config REMOTE_DEBUG_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + + config REMOTE_DEBUG_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + + config REMOTE_DEBUG_SERVER_PORT + int "Debug Server Port" + default 8080 + range 1 65535 + help + HTTP port for the remote debug web interface. + + menu "GPIO Configuration" + + config REMOTE_DEBUG_NUM_GPIOS + int "Number of GPIOs to expose" + default 4 + range 1 10 + help + Number of GPIO pins to make available for remote control. + + config REMOTE_DEBUG_GPIO_0 + int "GPIO Pin 0" + default 2 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 1 + help + First GPIO pin to control. + + config REMOTE_DEBUG_GPIO_1 + int "GPIO Pin 1" + default 4 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 2 + help + Second GPIO pin to control. + + config REMOTE_DEBUG_GPIO_2 + int "GPIO Pin 2" + default 16 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 3 + help + Third GPIO pin to control. + + config REMOTE_DEBUG_GPIO_3 + int "GPIO Pin 3" + default 17 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 4 + help + Fourth GPIO pin to control. + + config REMOTE_DEBUG_GPIO_4 + int "GPIO Pin 4" + default 18 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 5 + help + Fifth GPIO pin to control. + + config REMOTE_DEBUG_GPIO_5 + int "GPIO Pin 5" + default 19 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 6 + help + Sixth GPIO pin to control. + + config REMOTE_DEBUG_GPIO_6 + int "GPIO Pin 6" + default 21 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 7 + help + Seventh GPIO pin to control. + + config REMOTE_DEBUG_GPIO_7 + int "GPIO Pin 7" + default 22 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 8 + help + Eighth GPIO pin to control. + + config REMOTE_DEBUG_GPIO_8 + int "GPIO Pin 8" + default 23 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 9 + help + Ninth GPIO pin to control. + + config REMOTE_DEBUG_GPIO_9 + int "GPIO Pin 9" + default 25 + range 0 48 + depends on REMOTE_DEBUG_NUM_GPIOS >= 10 + help + Tenth GPIO pin to control. + + endmenu + + menu "ADC Configuration" + + config REMOTE_DEBUG_NUM_ADCS + int "Number of ADC channels to monitor" + default 2 + range 0 8 + help + Number of ADC channels to monitor and plot. + + config REMOTE_DEBUG_ADC_0 + int "ADC Channel 0" + default 36 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 1 + help + First ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_1 + int "ADC Channel 1" + default 39 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 2 + help + Second ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_2 + int "ADC Channel 2" + default 34 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 3 + help + Third ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_3 + int "ADC Channel 3" + default 35 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 4 + help + Fourth ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_4 + int "ADC Channel 4" + default 32 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 5 + help + Fifth ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_5 + int "ADC Channel 5" + default 33 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 6 + help + Sixth ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_6 + int "ADC Channel 6" + default 25 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 7 + help + Seventh ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_7 + int "ADC Channel 7" + default 26 + range 0 48 + depends on REMOTE_DEBUG_NUM_ADCS >= 8 + help + Eighth ADC pin to monitor (GPIO number). + + config REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ + int "ADC Sample Rate (Hz)" + default 100 + range 1 1000 + depends on REMOTE_DEBUG_NUM_ADCS > 0 + help + How often to sample ADC values (samples per second). + + config REMOTE_DEBUG_ADC_BUFFER_SIZE + int "ADC Buffer Size" + default 500 + range 10 10000 + depends on REMOTE_DEBUG_NUM_ADCS > 0 + help + Number of samples to keep in buffer for plotting. + + endmenu + +endmenu diff --git a/components/remote_debug/example/main/remote_debug_example.cpp b/components/remote_debug/example/main/remote_debug_example.cpp new file mode 100644 index 000000000..ea86faf64 --- /dev/null +++ b/components/remote_debug/example/main/remote_debug_example.cpp @@ -0,0 +1,116 @@ +#include +#include +#include + +#include "logger.hpp" +#include "nvs.hpp" +#include "remote_debug.hpp" +#include "wifi_sta.hpp" + +using namespace std::chrono_literals; + +extern "C" void app_main(void) { + espp::Logger logger({.tag = "Remote Debug Example", .level = espp::Logger::Verbosity::INFO}); + + logger.info("Starting Remote Debug Example"); + +#if CONFIG_ESP32_WIFI_NVS_ENABLED + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); +#endif + + //! [remote debug example] + + // Connect to WiFi using espp WifiSta + logger.info("Connecting to WiFi SSID: {}", CONFIG_REMOTE_DEBUG_WIFI_SSID); + + espp::WifiSta wifi_sta({.ssid = CONFIG_REMOTE_DEBUG_WIFI_SSID, + .password = CONFIG_REMOTE_DEBUG_WIFI_PASSWORD, + .num_connect_retries = 5, + .on_connected = [&logger]() { logger.info("WiFi connected!"); }, + .on_disconnected = [&logger]() { logger.warn("WiFi disconnected!"); }, + .on_got_ip = + [&logger](ip_event_got_ip_t *event) { + logger.info("got IP: {}.{}.{}.{}", IP2STR(&event->ip_info.ip)); + }}); + + // Wait for connection + while (!wifi_sta.is_connected()) { + std::this_thread::sleep_for(100ms); + } + logger.info("WiFi connected successfully"); + + // Build GPIO list from menuconfig + std::vector gpios; +#if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 1 + gpios.push_back( + {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_0), .mode = GPIO_MODE_OUTPUT}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 2 + gpios.push_back( + {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_1), .mode = GPIO_MODE_OUTPUT}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 3 + gpios.push_back( + {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_2), .mode = GPIO_MODE_OUTPUT}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 4 + gpios.push_back( + {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_3), .mode = GPIO_MODE_OUTPUT}); +#endif + + // Build ADC list from menuconfig +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 1 + int adc_sample_rate_hz = CONFIG_REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ; + size_t adc_buffer_size = CONFIG_REMOTE_DEBUG_ADC_BUFFER_SIZE; +#else + int adc_sample_rate_hz = 1; // Default to 1 Hz if no ADCs configured + size_t adc_buffer_size = 1; // Default to buffer size of 1 if no ADCs configured +#endif + + std::vector adcs; +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 1 + adcs.push_back( + {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_0)}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 2 + adcs.push_back( + {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_1)}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 3 + adcs.push_back( + {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_2)}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 4 + adcs.push_back( + {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_3)}); +#endif + + // Configure remote debug + espp::RemoteDebug::Config config{ + .gpios = gpios, + .adcs = adcs, + .server_port = static_cast(CONFIG_REMOTE_DEBUG_SERVER_PORT), + .adc_sample_rate = std::chrono::milliseconds(1000 / adc_sample_rate_hz), + .adc_history_size = adc_buffer_size, + .log_level = espp::Logger::Verbosity::INFO}; + + espp::RemoteDebug remote_debug(config); + remote_debug.start(); + + logger.info("Remote Debug Server started on port {}!", CONFIG_REMOTE_DEBUG_SERVER_PORT); + logger.info("GPIO pins available: {}", gpios.size()); + logger.info("ADC channels available: {}", adcs.size()); + + // Keep running + while (true) { + std::this_thread::sleep_for(1s); + } + + //! [remote debug example] +} diff --git a/components/remote_debug/example/partitions.csv b/components/remote_debug/example/partitions.csv new file mode 100644 index 000000000..c4217ab9e --- /dev/null +++ b/components/remote_debug/example/partitions.csv @@ -0,0 +1,5 @@ +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1500K, diff --git a/components/remote_debug/example/sdkconfig.defaults b/components/remote_debug/example/sdkconfig.defaults new file mode 100644 index 000000000..504883aa7 --- /dev/null +++ b/components/remote_debug/example/sdkconfig.defaults @@ -0,0 +1,27 @@ +# ESP32-specific +CONFIG_IDF_TARGET="esp32s3" + +# Flash size +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="4MB" + +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y + +# Default settings +CONFIG_REMOTE_DEBUG_WIFI_SSID="myssid" +CONFIG_REMOTE_DEBUG_WIFI_PASSWORD="mypassword" +CONFIG_REMOTE_DEBUG_SERVER_PORT=8080 +CONFIG_REMOTE_DEBUG_NUM_GPIOS=4 +CONFIG_REMOTE_DEBUG_GPIO_0=16 +CONFIG_REMOTE_DEBUG_GPIO_1=35 +CONFIG_REMOTE_DEBUG_GPIO_2=36 +CONFIG_REMOTE_DEBUG_GPIO_3=37 +CONFIG_REMOTE_DEBUG_NUM_ADCS=2 +CONFIG_REMOTE_DEBUG_ADC_0=7 +CONFIG_REMOTE_DEBUG_ADC_1=8 +CONFIG_REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ=100 +CONFIG_REMOTE_DEBUG_ADC_BUFFER_SIZE=500 diff --git a/components/remote_debug/example/sdkconfig.defaults.esp32s3 b/components/remote_debug/example/sdkconfig.defaults.esp32s3 new file mode 100644 index 000000000..eaee743c2 --- /dev/null +++ b/components/remote_debug/example/sdkconfig.defaults.esp32s3 @@ -0,0 +1,3 @@ +# on the ESP32S3, which has native USB, we need to set the console so that the +# CLI can be configured correctly: +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y diff --git a/components/remote_debug/idf_component.yml b/components/remote_debug/idf_component.yml new file mode 100644 index 000000000..25f427d0e --- /dev/null +++ b/components/remote_debug/idf_component.yml @@ -0,0 +1,20 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "Remote debugging server with GPIO and ADC control" +url: "https://github.com/esp-cpp/espp/tree/main/components/remote_debug" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/remote_debug.html" +tags: + - cpp + - Debug + - GPIO + - ADC +dependencies: + idf: + version: '>=5.0' + espp/base_component: + path: ../base_component + espp/oneshot_adc: + path: ../oneshot_adc diff --git a/components/remote_debug/include/remote_debug.hpp b/components/remote_debug/include/remote_debug.hpp new file mode 100644 index 000000000..39b733526 --- /dev/null +++ b/components/remote_debug/include/remote_debug.hpp @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "base_component.hpp" +#include "oneshot_adc.hpp" + +namespace espp { +/** + * @brief Remote Debug Component + * + * Provides a web-based interface for remote GPIO control and ADC monitoring. + * Allows real-time control of GPIO pins and plotting of ADC values. + * + * Features: + * - GPIO control (set high/low, read state, configure mode) + * - ADC value reading and real-time plotting + * - WebSocket support for live data streaming + * - Configurable sampling rates + * - Mobile-friendly responsive interface + * + * \section remote_debug_ex1 Remote Debug Example + * \snippet remote_debug_example.cpp remote debug example + */ +class RemoteDebug : public BaseComponent { +public: + /** + * @brief GPIO configuration + */ + struct GpioConfig { + gpio_num_t pin; ///< GPIO pin number + gpio_mode_t mode{GPIO_MODE_OUTPUT}; ///< Pin mode (input/output) + std::string label{""}; ///< Optional label for UI + }; + + /** + * @brief ADC configuration + */ + struct AdcConfig { + adc_unit_t unit; ///< ADC unit (ADC_UNIT_1 or ADC_UNIT_2) + adc_channel_t channel; ///< ADC channel + adc_atten_t atten{ADC_ATTEN_DB_12}; ///< Attenuation (affects voltage range) + std::string label{""}; ///< Optional label for UI + }; + + /** + * @brief Configuration for remote debug + */ + struct Config { + std::vector gpios; ///< GPIO pins to expose + std::vector adcs; ///< ADC channels to monitor + uint16_t server_port{8080}; ///< HTTP server port + std::chrono::milliseconds adc_sample_rate{100}; ///< ADC sampling interval + size_t adc_history_size{1000}; ///< Number of ADC samples to keep + Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity + }; + + /** + * @brief Construct remote debug interface + * @param config Configuration structure + */ + explicit RemoteDebug(const Config &config); + + /** + * @brief Destructor + */ + ~RemoteDebug(); + + /** + * @brief Start the debug server + * @return true if started successfully + */ + bool start(); + + /** + * @brief Stop the debug server + */ + void stop(); + + /** + * @brief Check if server is running + * @return true if active + */ + bool is_active() const { return is_active_; } + + /** + * @brief Set GPIO output level + * @param pin GPIO pin number + * @param level Level (0=low, 1=high) + * @return true if successful + */ + bool set_gpio(gpio_num_t pin, int level); + + /** + * @brief Read GPIO input level + * @param pin GPIO pin number + * @return Level (0 or 1), or -1 on error + */ + int get_gpio(gpio_num_t pin); + + /** + * @brief Read ADC value + * @param unit ADC unit + * @param channel ADC channel + * @return Raw ADC value, or -1 on error + */ + int read_adc(adc_unit_t unit, adc_channel_t channel); + +protected: + void init(const Config &config); + bool start_server(); + void stop_server(); + void adc_sampling_task(); + + // HTTP handlers + static esp_err_t root_handler(httpd_req_t *req); + static esp_err_t gpio_get_handler(httpd_req_t *req); + static esp_err_t gpio_set_handler(httpd_req_t *req); + static esp_err_t adc_get_handler(httpd_req_t *req); + static esp_err_t adc_data_handler(httpd_req_t *req); + + std::string generate_html() const; + std::string get_gpio_state_json() const; + std::string get_adc_data_json() const; + + Config config_; + httpd_handle_t server_{nullptr}; + adc_oneshot_unit_handle_t adc1_handle_{nullptr}; + adc_oneshot_unit_handle_t adc2_handle_{nullptr}; + + std::map gpio_map_; + std::map, AdcConfig> adc_map_; + + // ADC data storage + struct AdcData { + std::vector values; + std::vector timestamps; + size_t write_index{0}; + }; + std::map, AdcData> adc_data_; + std::mutex adc_mutex_; + + std::atomic is_active_{false}; + std::atomic sampling_active_{false}; + std::unique_ptr sampling_thread_; +}; +} // namespace espp diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp new file mode 100644 index 000000000..acb0c177b --- /dev/null +++ b/components/remote_debug/src/remote_debug.cpp @@ -0,0 +1,523 @@ +#include "remote_debug.hpp" + +#include +#include +#include + +using namespace espp; + +RemoteDebug::RemoteDebug(const Config &config) + : BaseComponent("RemoteDebug", config.log_level) { + init(config); +} + +RemoteDebug::~RemoteDebug() { stop(); } + +void RemoteDebug::init(const Config &config) { + config_ = config; + + // Initialize GPIO + for (const auto &gpio : config_.gpios) { + gpio_config_t io_conf = {}; + io_conf.pin_bit_mask = (1ULL << gpio.pin); + io_conf.mode = gpio.mode; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.intr_type = GPIO_INTR_DISABLE; + gpio_config(&io_conf); + gpio_map_[gpio.pin] = gpio; + } + + // Initialize ADC + bool need_adc1 = false, need_adc2 = false; + for (const auto &adc : config_.adcs) { + if (adc.unit == ADC_UNIT_1) + need_adc1 = true; + if (adc.unit == ADC_UNIT_2) + need_adc2 = true; + adc_map_[{adc.unit, adc.channel}] = adc; + } + + if (need_adc1) { + adc_oneshot_unit_init_cfg_t adc1_config = {.unit_id = ADC_UNIT_1}; + adc_oneshot_new_unit(&adc1_config, &adc1_handle_); + } + + if (need_adc2) { + adc_oneshot_unit_init_cfg_t adc2_config = {.unit_id = ADC_UNIT_2}; + adc_oneshot_new_unit(&adc2_config, &adc2_handle_); + } + + // Configure ADC channels + for (const auto &[key, adc] : adc_map_) { + adc_oneshot_chan_cfg_t chan_config = {.atten = adc.atten, .bitwidth = ADC_BITWIDTH_DEFAULT}; + auto handle = (adc.unit == ADC_UNIT_1) ? adc1_handle_ : adc2_handle_; + if (handle) { + adc_oneshot_config_channel(handle, adc.channel, &chan_config); + } + + // Initialize data storage + adc_data_[key].values.resize(config_.adc_history_size, 0); + adc_data_[key].timestamps.resize(config_.adc_history_size, 0); + } + + logger_.info("RemoteDebug initialized with {} GPIOs and {} ADCs", config_.gpios.size(), + config_.adcs.size()); +} + +bool RemoteDebug::start() { + if (is_active_) { + logger_.warn("Already running"); + return true; + } + + if (!start_server()) { + return false; + } + + // Start ADC sampling task if we have ADCs + if (!adc_map_.empty()) { + sampling_active_ = true; + sampling_thread_ = std::make_unique([this]() { adc_sampling_task(); }); + } + + is_active_ = true; + logger_.info("RemoteDebug started on port {}", config_.server_port); + return true; +} + +void RemoteDebug::stop() { + if (!is_active_) { + return; + } + + sampling_active_ = false; + if (sampling_thread_ && sampling_thread_->joinable()) { + sampling_thread_->join(); + } + + stop_server(); + is_active_ = false; + logger_.info("RemoteDebug stopped"); +} + +bool RemoteDebug::set_gpio(gpio_num_t pin, int level) { + if (gpio_map_.find(pin) == gpio_map_.end()) { + logger_.error("GPIO {} not configured", static_cast(pin)); + return false; + } + gpio_set_level(pin, level); + return true; +} + +int RemoteDebug::get_gpio(gpio_num_t pin) { + if (gpio_map_.find(pin) == gpio_map_.end()) { + logger_.error("GPIO {} not configured", static_cast(pin)); + return -1; + } + return gpio_get_level(pin); +} + +int RemoteDebug::read_adc(adc_unit_t unit, adc_channel_t channel) { + auto key = std::make_pair(unit, channel); + if (adc_map_.find(key) == adc_map_.end()) { + return -1; + } + + int raw_value = 0; + auto handle = (unit == ADC_UNIT_1) ? adc1_handle_ : adc2_handle_; + if (handle && adc_oneshot_read(handle, channel, &raw_value) == ESP_OK) { + return raw_value; + } + return -1; +} + +void RemoteDebug::adc_sampling_task() { + while (sampling_active_) { + auto now = esp_timer_get_time(); + + std::lock_guard lock(adc_mutex_); + for (auto &[key, data] : adc_data_) { + int value = read_adc(key.first, key.second); + if (value >= 0) { + data.values[data.write_index] = value; + data.timestamps[data.write_index] = now; + data.write_index = (data.write_index + 1) % config_.adc_history_size; + } + } + + std::this_thread::sleep_for(config_.adc_sample_rate); + } +} + +bool RemoteDebug::start_server() { + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = config_.server_port; + config.max_uri_handlers = 8; + config.stack_size = 8192; + + if (httpd_start(&server_, &config) != ESP_OK) { + logger_.error("Failed to start HTTP server"); + return false; + } + + // Register URI handlers + httpd_uri_t root_uri = { + .uri = "/", .method = HTTP_GET, .handler = root_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &root_uri); + + httpd_uri_t gpio_get_uri = { + .uri = "/api/gpio/get", .method = HTTP_GET, .handler = gpio_get_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &gpio_get_uri); + + httpd_uri_t gpio_set_uri = { + .uri = "/api/gpio/set", .method = HTTP_POST, .handler = gpio_set_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &gpio_set_uri); + + httpd_uri_t adc_get_uri = { + .uri = "/api/adc/get", .method = HTTP_GET, .handler = adc_get_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &adc_get_uri); + + httpd_uri_t adc_data_uri = { + .uri = "/api/adc/data", .method = HTTP_GET, .handler = adc_data_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &adc_data_uri); + + return true; +} + +void RemoteDebug::stop_server() { + if (server_) { + httpd_stop(server_); + server_ = nullptr; + } +} + +esp_err_t RemoteDebug::root_handler(httpd_req_t *req) { + auto *self = static_cast(req->user_ctx); + auto html = self->generate_html(); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, html.c_str(), html.length()); + return ESP_OK; +} + +esp_err_t RemoteDebug::gpio_get_handler(httpd_req_t *req) { + auto *self = static_cast(req->user_ctx); + auto json = self->get_gpio_state_json(); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json.c_str(), json.length()); + return ESP_OK; +} + +esp_err_t RemoteDebug::gpio_set_handler(httpd_req_t *req) { + auto *self = static_cast(req->user_ctx); + + // Parse POST data + char buf[100]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + buf[ret] = '\0'; + + // Parse pin=X&level=Y + int pin = -1, level = -1; + sscanf(buf, "pin=%d&level=%d", &pin, &level); + + if (pin >= 0 && (level == 0 || level == 1)) { + self->set_gpio(static_cast(pin), level); + httpd_resp_send(req, "OK", 2); + } else { + httpd_resp_send_500(req); + } + + return ESP_OK; +} + +esp_err_t RemoteDebug::adc_get_handler(httpd_req_t *req) { + auto *self = static_cast(req->user_ctx); + + // Parse query string for unit and channel + char query[100]; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { + int unit = -1, channel = -1; + char unit_str[16], channel_str[16]; + if (httpd_query_key_value(query, "unit", unit_str, sizeof(unit_str)) == ESP_OK) { + unit = atoi(unit_str); + } + if (httpd_query_key_value(query, "channel", channel_str, sizeof(channel_str)) == ESP_OK) { + channel = atoi(channel_str); + } + + if (unit >= 0 && channel >= 0) { + int value = + self->read_adc(static_cast(unit), static_cast(channel)); + char resp[32]; + snprintf(resp, sizeof(resp), "{\"value\":%d}", value); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, resp, strlen(resp)); + return ESP_OK; + } + } + + httpd_resp_send_500(req); + return ESP_FAIL; +} + +esp_err_t RemoteDebug::adc_data_handler(httpd_req_t *req) { + auto *self = static_cast(req->user_ctx); + auto json = self->get_adc_data_json(); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json.c_str(), json.length()); + return ESP_OK; +} + +std::string RemoteDebug::generate_html() const { + std::stringstream ss; + ss << R"(Remote Debug + + +
+

🔧 Remote Debug Interface

)"; + + // GPIO controls + ss << "

GPIO Control

"; + ss << "
"; + ss << "
Pin
Controls
State
"; + ss << "
"; + for (const auto &[pin, cfg] : gpio_map_) { + ss << "
"; + ss << "" << cfg.label << " (GPIO " << pin << ")"; + ss << "
"; + ss << ""; + ss << ""; + ss << "
"; + ss << "?"; + ss << "
"; + } + ss << "
"; + + // ADC display + if (!adc_map_.empty()) { + ss << "

ADC Monitoring

"; + for (const auto &[key, cfg] : adc_map_) { + ss << "
"; + ss << "" << cfg.label << " (Unit:" << key.first + << " Ch:" << key.second << ")"; + ss << "?"; + ss << "
"; + } + ss << ""; + ss << "
"; + } + + // JavaScript + ss << R"(
)"; + + return ss.str(); +} + +std::string RemoteDebug::get_gpio_state_json() const { + std::stringstream ss; + ss << "{"; + bool first = true; + for (const auto &[pin, cfg] : gpio_map_) { + if (!first) + ss << ","; + ss << "\"" << pin << "\":" << gpio_get_level(pin); + first = false; + } + ss << "}"; + return ss.str(); +} + +std::string RemoteDebug::get_adc_data_json() const { + std::lock_guard lock(const_cast(adc_mutex_)); + std::stringstream ss; + ss << "{"; + bool first = true; + for (const auto &[key, data] : adc_data_) { + if (!first) + ss << ","; + auto idx = (data.write_index > 0) ? (data.write_index - 1) : (config_.adc_history_size - 1); + int raw_value = data.values[idx]; + // Convert raw ADC to millivolts + float voltage_mv = 0.0f; + // if (adcs_.count(key) > 0) { + // voltage_mv = adcs_.at(key)->read_mv(); + // } + ss << "\"" << key.first << "_" << key.second << "\":{"; + ss << "\"current\":" << raw_value << ","; + ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage_mv << "}"; + first = false; + } + ss << "}"; + return ss.str(); +} From cb58bfc29a881c4dc74d3b9c8584d309ee9ca845 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 18:20:35 -0600 Subject: [PATCH 02/20] update kconfig to default to saved ssid; update example / code to better handle input/output and lables for adc and io; update debug to use espp adc --- .../example/main/Kconfig.projbuild | 6 +- .../example/main/remote_debug_example.cpp | 44 ++- .../remote_debug/include/remote_debug.hpp | 68 +++- components/remote_debug/src/remote_debug.cpp | 360 +++++++++++++----- 4 files changed, 339 insertions(+), 139 deletions(-) diff --git a/components/remote_debug/example/main/Kconfig.projbuild b/components/remote_debug/example/main/Kconfig.projbuild index c11c5262b..31985d19b 100644 --- a/components/remote_debug/example/main/Kconfig.projbuild +++ b/components/remote_debug/example/main/Kconfig.projbuild @@ -2,13 +2,13 @@ menu "Remote Debug Example Configuration" config REMOTE_DEBUG_WIFI_SSID string "WiFi SSID" - default "myssid" + default "" help - SSID (network name) for the example to connect to. + SSID (network name) for the example to connect to. If left blank, it will connect to the last used SSID. config REMOTE_DEBUG_WIFI_PASSWORD string "WiFi Password" - default "mypassword" + default "" help WiFi password (WPA or WPA2) for the example to use. diff --git a/components/remote_debug/example/main/remote_debug_example.cpp b/components/remote_debug/example/main/remote_debug_example.cpp index ea86faf64..d70f1452b 100644 --- a/components/remote_debug/example/main/remote_debug_example.cpp +++ b/components/remote_debug/example/main/remote_debug_example.cpp @@ -48,20 +48,24 @@ extern "C" void app_main(void) { // Build GPIO list from menuconfig std::vector gpios; #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 1 - gpios.push_back( - {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_0), .mode = GPIO_MODE_OUTPUT}); + gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_0), + .mode = GPIO_MODE_INPUT, + .label = "LED"}); #endif #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 2 - gpios.push_back( - {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_1), .mode = GPIO_MODE_OUTPUT}); + gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_1), + .mode = GPIO_MODE_INPUT, + .label = "BUZZER"}); #endif #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 3 - gpios.push_back( - {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_2), .mode = GPIO_MODE_OUTPUT}); + gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_2), + .mode = GPIO_MODE_INPUT, + .label = "MOTOR"}); #endif #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 4 - gpios.push_back( - {.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_3), .mode = GPIO_MODE_OUTPUT}); + gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_3), + .mode = GPIO_MODE_INPUT, + .label = "FAN"}); #endif // Build ADC list from menuconfig @@ -73,30 +77,32 @@ extern "C" void app_main(void) { size_t adc_buffer_size = 1; // Default to buffer size of 1 if no ADCs configured #endif - std::vector adcs; + std::vector adc1_channels; #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 1 - adcs.push_back( - {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_0)}); + adc1_channels.push_back( + {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_0), .label = "A0"}); #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 2 - adcs.push_back( - {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_1)}); + adc1_channels.push_back( + {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_1), .label = "A1"}); #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 3 - adcs.push_back( - {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_2)}); + adc1_channels.push_back( + {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_2), .label = "A2"}); #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 4 - adcs.push_back( - {.unit = ADC_UNIT_1, .channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_3)}); + adc1_channels.push_back( + {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_3), .label = "A3"}); #endif // Configure remote debug espp::RemoteDebug::Config config{ .gpios = gpios, - .adcs = adcs, + .adc1_channels = adc1_channels, + .adc2_channels = {}, .server_port = static_cast(CONFIG_REMOTE_DEBUG_SERVER_PORT), .adc_sample_rate = std::chrono::milliseconds(1000 / adc_sample_rate_hz), + .gpio_update_rate = std::chrono::milliseconds(100), .adc_history_size = adc_buffer_size, .log_level = espp::Logger::Verbosity::INFO}; @@ -105,7 +111,7 @@ extern "C" void app_main(void) { logger.info("Remote Debug Server started on port {}!", CONFIG_REMOTE_DEBUG_SERVER_PORT); logger.info("GPIO pins available: {}", gpios.size()); - logger.info("ADC channels available: {}", adcs.size()); + logger.info("ADC channels available: {}", adc1_channels.size()); // Keep running while (true) { diff --git a/components/remote_debug/include/remote_debug.hpp b/components/remote_debug/include/remote_debug.hpp index 39b733526..df702b862 100644 --- a/components/remote_debug/include/remote_debug.hpp +++ b/components/remote_debug/include/remote_debug.hpp @@ -46,8 +46,7 @@ class RemoteDebug : public BaseComponent { /** * @brief ADC configuration */ - struct AdcConfig { - adc_unit_t unit; ///< ADC unit (ADC_UNIT_1 or ADC_UNIT_2) + struct AdcChannelConfig { adc_channel_t channel; ///< ADC channel adc_atten_t atten{ADC_ATTEN_DB_12}; ///< Attenuation (affects voltage range) std::string label{""}; ///< Optional label for UI @@ -58,9 +57,11 @@ class RemoteDebug : public BaseComponent { */ struct Config { std::vector gpios; ///< GPIO pins to expose - std::vector adcs; ///< ADC channels to monitor + std::vector adc1_channels; ///< ADC1 channels to monitor + std::vector adc2_channels; ///< ADC2 channels to monitor uint16_t server_port{8080}; ///< HTTP server port std::chrono::milliseconds adc_sample_rate{100}; ///< ADC sampling interval + std::chrono::milliseconds gpio_update_rate{100}; ///< GPIO state update interval size_t adc_history_size{1000}; ///< Number of ADC samples to keep Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity }; @@ -102,31 +103,32 @@ class RemoteDebug : public BaseComponent { bool set_gpio(gpio_num_t pin, int level); /** - * @brief Read GPIO input level + * @brief Read GPIO level * @param pin GPIO pin number * @return Level (0 or 1), or -1 on error */ int get_gpio(gpio_num_t pin); /** - * @brief Read ADC value - * @param unit ADC unit - * @param channel ADC channel - * @return Raw ADC value, or -1 on error + * @brief Configure GPIO mode + * @param pin GPIO pin number + * @param mode GPIO mode (input/output) + * @return true if successful */ - int read_adc(adc_unit_t unit, adc_channel_t channel); + bool configure_gpio(gpio_num_t pin, gpio_mode_t mode); protected: void init(const Config &config); bool start_server(); void stop_server(); void adc_sampling_task(); + void gpio_update_task(); // HTTP handlers static esp_err_t root_handler(httpd_req_t *req); static esp_err_t gpio_get_handler(httpd_req_t *req); static esp_err_t gpio_set_handler(httpd_req_t *req); - static esp_err_t adc_get_handler(httpd_req_t *req); + static esp_err_t gpio_config_handler(httpd_req_t *req); static esp_err_t adc_data_handler(httpd_req_t *req); std::string generate_html() const; @@ -135,23 +137,61 @@ class RemoteDebug : public BaseComponent { Config config_; httpd_handle_t server_{nullptr}; - adc_oneshot_unit_handle_t adc1_handle_{nullptr}; - adc_oneshot_unit_handle_t adc2_handle_{nullptr}; + + std::unique_ptr adc1_; + std::unique_ptr adc2_; std::map gpio_map_; - std::map, AdcConfig> adc_map_; + std::map gpio_state_; // Cached GPIO states + std::mutex gpio_mutex_; // ADC data storage struct AdcData { std::vector values; std::vector timestamps; size_t write_index{0}; + adc_channel_t channel; + std::string label; }; - std::map, AdcData> adc_data_; + std::vector adc1_data_; + std::vector adc2_data_; std::mutex adc_mutex_; std::atomic is_active_{false}; std::atomic sampling_active_{false}; std::unique_ptr sampling_thread_; + std::unique_ptr gpio_thread_; }; } // namespace espp + +// for libfmt formatting of gpio_mode_t +template <> struct fmt::formatter : fmt::formatter { + template auto format(const gpio_mode_t &mode, FormatContext &ctx) const { + std::string mode_str; + switch (mode) { + case GPIO_MODE_INPUT: + mode_str = "INPUT"; + break; + case GPIO_MODE_OUTPUT: + mode_str = "OUTPUT"; + break; + case GPIO_MODE_INPUT_OUTPUT: + mode_str = "INPUT_OUTPUT"; + break; + case GPIO_MODE_DISABLE: + mode_str = "DISABLE"; + break; + default: + mode_str = "UNKNOWN"; + break; + } + return fmt::formatter::format(mode_str, ctx); + } +}; + +// for libfmt formatting of gpio_num_t +template <> struct fmt::formatter : fmt::formatter { + template auto format(const gpio_num_t &pin, FormatContext &ctx) const { + return fmt::formatter::format(static_cast(pin), ctx); + } +}; diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index acb0c177b..fac128629 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -16,53 +16,64 @@ RemoteDebug::~RemoteDebug() { stop(); } void RemoteDebug::init(const Config &config) { config_ = config; - // Initialize GPIO + // Initialize GPIO - default to input mode for (const auto &gpio : config_.gpios) { gpio_config_t io_conf = {}; io_conf.pin_bit_mask = (1ULL << gpio.pin); - io_conf.mode = gpio.mode; + io_conf.mode = GPIO_MODE_INPUT; // Default to input io_conf.pull_up_en = GPIO_PULLUP_DISABLE; io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; io_conf.intr_type = GPIO_INTR_DISABLE; gpio_config(&io_conf); - gpio_map_[gpio.pin] = gpio; - } - // Initialize ADC - bool need_adc1 = false, need_adc2 = false; - for (const auto &adc : config_.adcs) { - if (adc.unit == ADC_UNIT_1) - need_adc1 = true; - if (adc.unit == ADC_UNIT_2) - need_adc2 = true; - adc_map_[{adc.unit, adc.channel}] = adc; - } + // Store the GPIO config with input mode + GpioConfig gpio_copy = gpio; + gpio_copy.mode = GPIO_MODE_INPUT; + gpio_map_[gpio.pin] = gpio_copy; + gpio_state_[gpio.pin] = gpio_get_level(gpio.pin); - if (need_adc1) { - adc_oneshot_unit_init_cfg_t adc1_config = {.unit_id = ADC_UNIT_1}; - adc_oneshot_new_unit(&adc1_config, &adc1_handle_); + logger_.debug("Configured GPIO {} as input, initial state: {}", gpio.pin, + gpio_state_[gpio.pin]); } - if (need_adc2) { - adc_oneshot_unit_init_cfg_t adc2_config = {.unit_id = ADC_UNIT_2}; - adc_oneshot_new_unit(&adc2_config, &adc2_handle_); + // Initialize ADC1 + if (!config_.adc1_channels.empty()) { + std::vector adc1_configs; + for (const auto &ch : config_.adc1_channels) { + adc1_configs.push_back({.unit = ADC_UNIT_1, .channel = ch.channel, .attenuation = ch.atten}); + + // Initialize data storage + AdcData data; + data.values.resize(config_.adc_history_size, 0); + data.timestamps.resize(config_.adc_history_size, 0); + data.channel = ch.channel; + data.label = ch.label; + adc1_data_.push_back(std::move(data)); + } + adc1_ = std::make_unique(OneshotAdc::Config{ + .unit = ADC_UNIT_1, .channels = adc1_configs, .log_level = config_.log_level}); } - // Configure ADC channels - for (const auto &[key, adc] : adc_map_) { - adc_oneshot_chan_cfg_t chan_config = {.atten = adc.atten, .bitwidth = ADC_BITWIDTH_DEFAULT}; - auto handle = (adc.unit == ADC_UNIT_1) ? adc1_handle_ : adc2_handle_; - if (handle) { - adc_oneshot_config_channel(handle, adc.channel, &chan_config); + // Initialize ADC2 + if (!config_.adc2_channels.empty()) { + std::vector adc2_configs; + for (const auto &ch : config_.adc2_channels) { + adc2_configs.push_back({.unit = ADC_UNIT_2, .channel = ch.channel, .attenuation = ch.atten}); + + // Initialize data storage + AdcData data; + data.values.resize(config_.adc_history_size, 0); + data.timestamps.resize(config_.adc_history_size, 0); + data.channel = ch.channel; + data.label = ch.label; + adc2_data_.push_back(std::move(data)); } - - // Initialize data storage - adc_data_[key].values.resize(config_.adc_history_size, 0); - adc_data_[key].timestamps.resize(config_.adc_history_size, 0); + adc2_ = std::make_unique(OneshotAdc::Config{ + .unit = ADC_UNIT_2, .channels = adc2_configs, .log_level = config_.log_level}); } - logger_.info("RemoteDebug initialized with {} GPIOs and {} ADCs", config_.gpios.size(), - config_.adcs.size()); + logger_.info("RemoteDebug initialized with {} GPIOs, {} ADC1 channels, {} ADC2 channels", + config_.gpios.size(), config_.adc1_channels.size(), config_.adc2_channels.size()); } bool RemoteDebug::start() { @@ -76,11 +87,16 @@ bool RemoteDebug::start() { } // Start ADC sampling task if we have ADCs - if (!adc_map_.empty()) { + if (adc1_ || adc2_) { sampling_active_ = true; sampling_thread_ = std::make_unique([this]() { adc_sampling_task(); }); } + // Start GPIO update task + if (!gpio_map_.empty()) { + gpio_thread_ = std::make_unique([this]() { gpio_update_task(); }); + } + is_active_ = true; logger_.info("RemoteDebug started on port {}", config_.server_port); return true; @@ -95,6 +111,9 @@ void RemoteDebug::stop() { if (sampling_thread_ && sampling_thread_->joinable()) { sampling_thread_->join(); } + if (gpio_thread_ && gpio_thread_->joinable()) { + gpio_thread_->join(); + } stop_server(); is_active_ = false; @@ -102,34 +121,66 @@ void RemoteDebug::stop() { } bool RemoteDebug::set_gpio(gpio_num_t pin, int level) { + std::lock_guard lock(gpio_mutex_); if (gpio_map_.find(pin) == gpio_map_.end()) { logger_.error("GPIO {} not configured", static_cast(pin)); return false; } + + // Only set if it's an output + if (gpio_map_[pin].mode != GPIO_MODE_OUTPUT && gpio_map_[pin].mode != GPIO_MODE_OUTPUT_OD && + gpio_map_[pin].mode != GPIO_MODE_INPUT_OUTPUT && + gpio_map_[pin].mode != GPIO_MODE_INPUT_OUTPUT_OD) { + logger_.error("GPIO {} is not configured as output", static_cast(pin)); + return false; + } + gpio_set_level(pin, level); + gpio_state_[pin] = level; return true; } int RemoteDebug::get_gpio(gpio_num_t pin) { + std::lock_guard lock(gpio_mutex_); if (gpio_map_.find(pin) == gpio_map_.end()) { logger_.error("GPIO {} not configured", static_cast(pin)); return -1; } - return gpio_get_level(pin); + return gpio_state_[pin]; } -int RemoteDebug::read_adc(adc_unit_t unit, adc_channel_t channel) { - auto key = std::make_pair(unit, channel); - if (adc_map_.find(key) == adc_map_.end()) { - return -1; +bool RemoteDebug::configure_gpio(gpio_num_t pin, gpio_mode_t mode) { + std::lock_guard lock(gpio_mutex_); + if (gpio_map_.find(pin) == gpio_map_.end()) { + logger_.error("GPIO {} not configured", static_cast(pin)); + return false; } - int raw_value = 0; - auto handle = (unit == ADC_UNIT_1) ? adc1_handle_ : adc2_handle_; - if (handle && adc_oneshot_read(handle, channel, &raw_value) == ESP_OK) { - return raw_value; + logger_.info("Configuring GPIO {} to mode {}", static_cast(pin), mode); + + gpio_config_t io_conf = {}; + io_conf.pin_bit_mask = (1ULL << pin); + io_conf.mode = mode; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.intr_type = GPIO_INTR_DISABLE; + gpio_config(&io_conf); + + gpio_map_[pin].mode = mode; + gpio_state_[pin] = gpio_get_level(pin); + return true; +} + +void RemoteDebug::gpio_update_task() { + while (sampling_active_) { + { + std::lock_guard lock(gpio_mutex_); + for (auto &[pin, config] : gpio_map_) { + gpio_state_[pin] = gpio_get_level(pin); + } + } + std::this_thread::sleep_for(config_.gpio_update_rate); } - return -1; } void RemoteDebug::adc_sampling_task() { @@ -137,10 +188,24 @@ void RemoteDebug::adc_sampling_task() { auto now = esp_timer_get_time(); std::lock_guard lock(adc_mutex_); - for (auto &[key, data] : adc_data_) { - int value = read_adc(key.first, key.second); - if (value >= 0) { - data.values[data.write_index] = value; + + // Sample ADC1 + if (adc1_) { + auto values = adc1_->read_all_mv(); + for (size_t i = 0; i < values.size() && i < adc1_data_.size(); i++) { + auto &data = adc1_data_[i]; + data.values[data.write_index] = values[i]; + data.timestamps[data.write_index] = now; + data.write_index = (data.write_index + 1) % config_.adc_history_size; + } + } + + // Sample ADC2 + if (adc2_) { + auto values = adc2_->read_all_mv(); + for (size_t i = 0; i < values.size() && i < adc2_data_.size(); i++) { + auto &data = adc2_data_[i]; + data.values[data.write_index] = values[i]; data.timestamps[data.write_index] = now; data.write_index = (data.write_index + 1) % config_.adc_history_size; } @@ -174,9 +239,11 @@ bool RemoteDebug::start_server() { .uri = "/api/gpio/set", .method = HTTP_POST, .handler = gpio_set_handler, .user_ctx = this}; httpd_register_uri_handler(server_, &gpio_set_uri); - httpd_uri_t adc_get_uri = { - .uri = "/api/adc/get", .method = HTTP_GET, .handler = adc_get_handler, .user_ctx = this}; - httpd_register_uri_handler(server_, &adc_get_uri); + httpd_uri_t gpio_config_uri = {.uri = "/api/gpio/config", + .method = HTTP_POST, + .handler = gpio_config_handler, + .user_ctx = this}; + httpd_register_uri_handler(server_, &gpio_config_uri); httpd_uri_t adc_data_uri = { .uri = "/api/adc/data", .method = HTTP_GET, .handler = adc_data_handler, .user_ctx = this}; @@ -234,34 +301,30 @@ esp_err_t RemoteDebug::gpio_set_handler(httpd_req_t *req) { return ESP_OK; } -esp_err_t RemoteDebug::adc_get_handler(httpd_req_t *req) { +esp_err_t RemoteDebug::gpio_config_handler(httpd_req_t *req) { auto *self = static_cast(req->user_ctx); - // Parse query string for unit and channel - char query[100]; - if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) { - int unit = -1, channel = -1; - char unit_str[16], channel_str[16]; - if (httpd_query_key_value(query, "unit", unit_str, sizeof(unit_str)) == ESP_OK) { - unit = atoi(unit_str); - } - if (httpd_query_key_value(query, "channel", channel_str, sizeof(channel_str)) == ESP_OK) { - channel = atoi(channel_str); - } + // Parse POST data + char buf[100]; + int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + buf[ret] = '\0'; - if (unit >= 0 && channel >= 0) { - int value = - self->read_adc(static_cast(unit), static_cast(channel)); - char resp[32]; - snprintf(resp, sizeof(resp), "{\"value\":%d}", value); - httpd_resp_set_type(req, "application/json"); - httpd_resp_send(req, resp, strlen(resp)); - return ESP_OK; - } + // Parse pin=X&mode=Y + int pin = -1, mode = -1; + sscanf(buf, "pin=%d&mode=%d", &pin, &mode); + + if (pin >= 0 && mode >= 0 && mode <= 6) { + self->configure_gpio(static_cast(pin), static_cast(mode)); + httpd_resp_send(req, "OK", 2); + } else { + httpd_resp_send_500(req); } - httpd_resp_send_500(req); - return ESP_FAIL; + return ESP_OK; } esp_err_t RemoteDebug::adc_data_handler(httpd_req_t *req) { @@ -308,16 +371,28 @@ button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0. // GPIO controls ss << "

GPIO Control

"; - ss << "
"; - ss << "
Pin
Controls
Mode
Controls
State
"; ss << "
"; for (const auto &[pin, cfg] : gpio_map_) { - ss << "
"; - ss << "" << cfg.label << " (GPIO " << pin << ")"; - ss << "
"; + ss << "" + << (cfg.label.empty() ? "GPIO " + std::to_string(pin) : cfg.label) << " (GPIO " << pin + << ")"; + ss << ""; + ss << "
"; ss << ""; ss << ""; ss << "
"; @@ -327,13 +402,26 @@ button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0. ss << "
"; // ADC display - if (!adc_map_.empty()) { + if (!adc1_data_.empty() || !adc2_data_.empty()) { ss << "

ADC Monitoring

"; - for (const auto &[key, cfg] : adc_map_) { + for (const auto &data : adc1_data_) { ss << "
"; - ss << "" << cfg.label << " (Unit:" << key.first - << " Ch:" << key.second << ")"; - ss << "?"; + ss << "" + << (data.label.empty() + ? ("ADC1_CH" + std::to_string(static_cast(data.channel))) + : data.label + " (ADC1_CH" + std::to_string(static_cast(data.channel)) + ")") + << ""; + ss << "?"; + ss << "
"; + } + for (const auto &data : adc2_data_) { + ss << "
"; + ss << "" + << (data.label.empty() + ? ("ADC2_CH" + std::to_string(static_cast(data.channel))) + : data.label + " (ADC2_CH" + std::to_string(static_cast(data.channel)) + ")") + << ""; + ss << "?"; ss << "
"; } ss << ""; @@ -346,6 +434,16 @@ const adcHistory = {}; const colors = ['#667eea', '#764ba2', '#48c774', '#f14668', '#ffdd57', '#3298dc']; let colorIndex = 0; +function setMode(pin, mode) { + fetch('/api/gpio/config', {method: 'POST', body: 'pin=' + pin + '&mode=' + mode}) + .then(r => r.text()) + .then(() => { + updateControlsVisibility(); + updateGpio(); + }) + .catch(err => console.error('Failed to set mode:', err)); +} + function setGpio(pin, level) { fetch('/api/gpio/set', {method: 'POST', body: 'pin=' + pin + '&level=' + level}) .then(r => r.text()) @@ -367,23 +465,44 @@ function updateGpio() { }); } +function updateControlsVisibility() { + )" + << "["; + bool first = true; + for (const auto &[pin, cfg] : gpio_map_) { + if (!first) + ss << ","; + ss << pin; + first = false; + } + ss << R"(].forEach(pin => { + const modeSelect = document.getElementById('mode' + pin); + const controls = document.getElementById('controls' + pin); + if (modeSelect && controls) { + // Show controls for input_output (3), hide for input (1) + controls.style.visibility = (modeSelect.value == '3') ? 'visible' : 'hidden'; + } + }); +} + function updateAdc() { fetch('/api/adc/data') .then(r => r.json()) .then(data => { const now = Date.now(); for (let key in data) { + // key format is "1_8" for ADC1 channel 8 const elem = document.getElementById('adc' + key); if (elem) { const volts = (data[key].voltage / 1000.0).toFixed(3); - elem.innerText = volts + ' V (' + data[key].current + ' raw)'; + elem.innerText = volts + ' V'; } if (!adcHistory[key]) { - adcHistory[key] = { values: [], times: [], color: colors[colorIndex++ % colors.length] }; + adcHistory[key] = { values: [], times: [], color: colors[colorIndex++ % colors.length], label: data[key].label }; } - adcHistory[key].values.push(data[key].current); + adcHistory[key].values.push(data[key].voltage / 1000.0); // Store in volts adcHistory[key].times.push(now); // Keep last 100 samples @@ -411,7 +530,7 @@ function drawChart() { const chartWidth = canvas.width - 2 * padding; const chartHeight = canvas.height - 2 * padding; - // Find min/max across all series + // Find min/max across all series (values are in volts) let minVal = Infinity, maxVal = -Infinity; for (let key in adcHistory) { const vals = adcHistory[key].values; @@ -435,10 +554,10 @@ function drawChart() { ctx.lineTo(padding + chartWidth, y); ctx.stroke(); - const val = Math.round(maxVal - (range * i / 4)); + const val = (maxVal - (range * i / 4)).toFixed(2); ctx.fillStyle = '#666'; ctx.font = '12px Arial'; - ctx.fillText(val, 5, y + 4); + ctx.fillText(val + 'V', 5, y + 4); } // Draw each ADC line @@ -466,32 +585,38 @@ function drawChart() { // Draw legend let legendY = padding + 10; for (let key in adcHistory) { + const label = adcHistory[key].label || ('ADC ' + key); ctx.fillStyle = adcHistory[key].color; - ctx.fillRect(padding + chartWidth - 100, legendY, 15, 15); + ctx.fillRect(padding + chartWidth - 150, legendY, 15, 15); ctx.fillStyle = '#333'; ctx.font = '12px Arial'; - ctx.fillText('ADC ' + key, padding + chartWidth - 80, legendY + 12); + ctx.fillText(label, padding + chartWidth - 130, legendY + 12); legendY += 20; } } -setInterval(updateGpio, 1000); -setInterval(updateAdc, 200); +// Initialize on page load +updateControlsVisibility(); updateGpio(); updateAdc(); + +// Update periodically +setInterval(updateGpio, 500); +setInterval(updateAdc, 200);
)"; return ss.str(); } std::string RemoteDebug::get_gpio_state_json() const { + std::lock_guard lock(const_cast(gpio_mutex_)); std::stringstream ss; ss << "{"; bool first = true; for (const auto &[pin, cfg] : gpio_map_) { if (!first) ss << ","; - ss << "\"" << pin << "\":" << gpio_get_level(pin); + ss << "\"" << static_cast(pin) << "\":" << gpio_state_.at(pin); first = false; } ss << "}"; @@ -502,22 +627,51 @@ std::string RemoteDebug::get_adc_data_json() const { std::lock_guard lock(const_cast(adc_mutex_)); std::stringstream ss; ss << "{"; + bool first = true; - for (const auto &[key, data] : adc_data_) { + + // ADC1 data + for (size_t i = 0; i < adc1_data_.size(); i++) { + const auto &data = adc1_data_[i]; if (!first) ss << ","; + auto idx = (data.write_index > 0) ? (data.write_index - 1) : (config_.adc_history_size - 1); - int raw_value = data.values[idx]; - // Convert raw ADC to millivolts - float voltage_mv = 0.0f; - // if (adcs_.count(key) > 0) { - // voltage_mv = adcs_.at(key)->read_mv(); - // } - ss << "\"" << key.first << "_" << key.second << "\":{"; - ss << "\"current\":" << raw_value << ","; - ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage_mv << "}"; + int voltage_mv = data.values[idx]; + + // Key format: "1_5" for ADC1 channel 5 + ss << "\"1_" << static_cast(data.channel) << "\":{"; + ss << "\"voltage\":" << voltage_mv << ","; + ss << "\"current\":" << voltage_mv << ","; + ss << "\"label\":\"" + << (data.label.empty() ? ("ADC1_CH" + std::to_string(static_cast(data.channel))) + : data.label) + << "\""; + ss << "}"; first = false; } + + // ADC2 data + for (size_t i = 0; i < adc2_data_.size(); i++) { + const auto &data = adc2_data_[i]; + if (!first) + ss << ","; + + auto idx = (data.write_index > 0) ? (data.write_index - 1) : (config_.adc_history_size - 1); + int voltage_mv = data.values[idx]; + + // Key format: "2_5" for ADC2 channel 5 + ss << "\"2_" << static_cast(data.channel) << "\":{"; + ss << "\"voltage\":" << voltage_mv << ","; + ss << "\"current\":" << voltage_mv << ","; + ss << "\"label\":\"" + << (data.label.empty() ? ("ADC2_CH" + std::to_string(static_cast(data.channel))) + : data.label) + << "\""; + ss << "}"; + first = false; + } + ss << "}"; return ss.str(); } From 3d4658275dc3136da35b2db41643d9be3833eac0 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Sun, 25 Jan 2026 22:52:00 -0600 Subject: [PATCH 03/20] update to allow labels in kconfig --- .../example/main/Kconfig.projbuild | 48 +++++++++++++++++++ .../example/main/remote_debug_example.cpp | 17 +++---- .../remote_debug/include/remote_debug.hpp | 1 + components/remote_debug/src/remote_debug.cpp | 5 +- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/components/remote_debug/example/main/Kconfig.projbuild b/components/remote_debug/example/main/Kconfig.projbuild index 31985d19b..5932139ad 100644 --- a/components/remote_debug/example/main/Kconfig.projbuild +++ b/components/remote_debug/example/main/Kconfig.projbuild @@ -1,5 +1,11 @@ menu "Remote Debug Example Configuration" + config REMOTE_DEBUG_DEVICE_NAME + string "Device Name" + default "ESP32 Device" + help + Name of the device shown in the web interface title. + config REMOTE_DEBUG_WIFI_SSID string "WiFi SSID" default "" @@ -36,6 +42,13 @@ menu "Remote Debug Example Configuration" help First GPIO pin to control. + config REMOTE_DEBUG_GPIO_0_LABEL + string "GPIO Pin 0 Label" + default "GPIO 2" + depends on REMOTE_DEBUG_NUM_GPIOS >= 1 + help + Label for first GPIO pin. + config REMOTE_DEBUG_GPIO_1 int "GPIO Pin 1" default 4 @@ -44,6 +57,13 @@ menu "Remote Debug Example Configuration" help Second GPIO pin to control. + config REMOTE_DEBUG_GPIO_1_LABEL + string "GPIO Pin 1 Label" + default "GPIO 4" + depends on REMOTE_DEBUG_NUM_GPIOS >= 2 + help + Label for second GPIO pin. + config REMOTE_DEBUG_GPIO_2 int "GPIO Pin 2" default 16 @@ -52,6 +72,13 @@ menu "Remote Debug Example Configuration" help Third GPIO pin to control. + config REMOTE_DEBUG_GPIO_2_LABEL + string "GPIO Pin 2 Label" + default "GPIO 16" + depends on REMOTE_DEBUG_NUM_GPIOS >= 3 + help + Label for third GPIO pin. + config REMOTE_DEBUG_GPIO_3 int "GPIO Pin 3" default 17 @@ -60,6 +87,13 @@ menu "Remote Debug Example Configuration" help Fourth GPIO pin to control. + config REMOTE_DEBUG_GPIO_3_LABEL + string "GPIO Pin 3 Label" + default "GPIO 17" + depends on REMOTE_DEBUG_NUM_GPIOS >= 4 + help + Label for fourth GPIO pin. + config REMOTE_DEBUG_GPIO_4 int "GPIO Pin 4" default 18 @@ -127,6 +161,13 @@ menu "Remote Debug Example Configuration" help First ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_0_LABEL + string "ADC Channel 0 Label" + default "ADC 36" + depends on REMOTE_DEBUG_NUM_ADCS >= 1 + help + Label for first ADC channel. + config REMOTE_DEBUG_ADC_1 int "ADC Channel 1" default 39 @@ -135,6 +176,13 @@ menu "Remote Debug Example Configuration" help Second ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_1_LABEL + string "ADC Channel 1 Label" + default "ADC 39" + depends on REMOTE_DEBUG_NUM_ADCS >= 2 + help + Label for second ADC channel. + config REMOTE_DEBUG_ADC_2 int "ADC Channel 2" default 34 diff --git a/components/remote_debug/example/main/remote_debug_example.cpp b/components/remote_debug/example/main/remote_debug_example.cpp index d70f1452b..32e997def 100644 --- a/components/remote_debug/example/main/remote_debug_example.cpp +++ b/components/remote_debug/example/main/remote_debug_example.cpp @@ -50,22 +50,22 @@ extern "C" void app_main(void) { #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 1 gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_0), .mode = GPIO_MODE_INPUT, - .label = "LED"}); + .label = CONFIG_REMOTE_DEBUG_GPIO_0_LABEL}); #endif #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 2 gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_1), .mode = GPIO_MODE_INPUT, - .label = "BUZZER"}); + .label = CONFIG_REMOTE_DEBUG_GPIO_1_LABEL}); #endif #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 3 gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_2), .mode = GPIO_MODE_INPUT, - .label = "MOTOR"}); + .label = CONFIG_REMOTE_DEBUG_GPIO_2_LABEL}); #endif #if CONFIG_REMOTE_DEBUG_NUM_GPIOS >= 4 gpios.push_back({.pin = static_cast(CONFIG_REMOTE_DEBUG_GPIO_3), .mode = GPIO_MODE_INPUT, - .label = "FAN"}); + .label = CONFIG_REMOTE_DEBUG_GPIO_3_LABEL}); #endif // Build ADC list from menuconfig @@ -79,12 +79,12 @@ extern "C" void app_main(void) { std::vector adc1_channels; #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 1 - adc1_channels.push_back( - {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_0), .label = "A0"}); + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_0), + .label = CONFIG_REMOTE_DEBUG_ADC_0_LABEL}); #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 2 - adc1_channels.push_back( - {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_1), .label = "A1"}); + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_1), + .label = CONFIG_REMOTE_DEBUG_ADC_1_LABEL}); #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 3 adc1_channels.push_back( @@ -97,6 +97,7 @@ extern "C" void app_main(void) { // Configure remote debug espp::RemoteDebug::Config config{ + .device_name = CONFIG_REMOTE_DEBUG_DEVICE_NAME, .gpios = gpios, .adc1_channels = adc1_channels, .adc2_channels = {}, diff --git a/components/remote_debug/include/remote_debug.hpp b/components/remote_debug/include/remote_debug.hpp index df702b862..422a9db97 100644 --- a/components/remote_debug/include/remote_debug.hpp +++ b/components/remote_debug/include/remote_debug.hpp @@ -56,6 +56,7 @@ class RemoteDebug : public BaseComponent { * @brief Configuration for remote debug */ struct Config { + std::string device_name{"ESP32 Device"}; ///< Device name shown in UI title std::vector gpios; ///< GPIO pins to expose std::vector adc1_channels; ///< ADC1 channels to monitor std::vector adc2_channels; ///< ADC2 channels to monitor diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index fac128629..6f8c880be 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -337,7 +337,7 @@ esp_err_t RemoteDebug::adc_data_handler(httpd_req_t *req) { std::string RemoteDebug::generate_html() const { std::stringstream ss; - ss << R"(Remote Debug + ss << R"(Remote Debug - )" << config_.device_name << R"(
-

🔧 Remote Debug Interface

)"; +

🔧 )" + << config_.device_name << R"(

)"; // GPIO controls ss << "

GPIO Control

"; From 6688c8495847b53f2005550ab042aaf93d180ccc Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 26 Jan 2026 22:03:53 -0600 Subject: [PATCH 04/20] Working remote logging with colorization :) --- components/remote_debug/CMakeLists.txt | 2 +- .../remote_debug/example/CMakeLists.txt | 2 +- .../remote_debug/example/main/CMakeLists.txt | 2 +- .../example/main/remote_debug_example.cpp | 31 +++ .../remote_debug/example/partitions.csv | 10 +- .../remote_debug/example/sdkconfig.defaults | 3 + .../remote_debug/include/remote_debug.hpp | 28 ++- components/remote_debug/src/remote_debug.cpp | 202 ++++++++++++++++++ 8 files changed, 264 insertions(+), 16 deletions(-) diff --git a/components/remote_debug/CMakeLists.txt b/components/remote_debug/CMakeLists.txt index 8966a196a..a25c1c3b1 100644 --- a/components/remote_debug/CMakeLists.txt +++ b/components/remote_debug/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES esp_http_server driver adc base_component task + REQUIRES esp_http_server driver adc base_component task file_system ) diff --git a/components/remote_debug/example/CMakeLists.txt b/components/remote_debug/example/CMakeLists.txt index e3a87b18c..068170289 100644 --- a/components/remote_debug/example/CMakeLists.txt +++ b/components/remote_debug/example/CMakeLists.txt @@ -12,7 +12,7 @@ set(EXTRA_COMPONENT_DIRS set( COMPONENTS - "main esptool_py remote_debug task nvs_flash esp_wifi esp_event esp_netif" + "main esptool_py remote_debug task timer nvs_flash wifi" CACHE STRING "List of components to include" ) diff --git a/components/remote_debug/example/main/CMakeLists.txt b/components/remote_debug/example/main/CMakeLists.txt index ab2e00da8..052f65ed2 100644 --- a/components/remote_debug/example/main/CMakeLists.txt +++ b/components/remote_debug/example/main/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRC_DIRS "." INCLUDE_DIRS "." - PRIV_REQUIRES remote_debug wifi nvs) + PRIV_REQUIRES remote_debug wifi nvs file_system timer) diff --git a/components/remote_debug/example/main/remote_debug_example.cpp b/components/remote_debug/example/main/remote_debug_example.cpp index 32e997def..07f5fa479 100644 --- a/components/remote_debug/example/main/remote_debug_example.cpp +++ b/components/remote_debug/example/main/remote_debug_example.cpp @@ -2,9 +2,11 @@ #include #include +#include "file_system.hpp" #include "logger.hpp" #include "nvs.hpp" #include "remote_debug.hpp" +#include "timer.hpp" #include "wifi_sta.hpp" using namespace std::chrono_literals; @@ -24,6 +26,13 @@ extern "C" void app_main(void) { ESP_ERROR_CHECK(ret); #endif + // Initialize filesystem + logger.info("Initializing filesystem..."); + auto &fs = espp::FileSystem::get(); + logger.info("Filesystem mounted at: {}", fs.get_mount_point()); + logger.info("Total space: {}", fs.human_readable(fs.get_total_space())); + logger.info("Free space: {}", fs.human_readable(fs.get_free_space())); + //! [remote debug example] // Connect to WiFi using espp WifiSta @@ -105,6 +114,7 @@ extern "C" void app_main(void) { .adc_sample_rate = std::chrono::milliseconds(1000 / adc_sample_rate_hz), .gpio_update_rate = std::chrono::milliseconds(100), .adc_history_size = adc_buffer_size, + .enable_log_capture = true, .log_level = espp::Logger::Verbosity::INFO}; espp::RemoteDebug remote_debug(config); @@ -114,6 +124,27 @@ extern "C" void app_main(void) { logger.info("GPIO pins available: {}", gpios.size()); logger.info("ADC channels available: {}", adc1_channels.size()); + std::this_thread::sleep_for(2s); + + // Create a timer to periodically generate log messages + int counter = 0; + auto timer = espp::Timer(espp::Timer::Config{.name = "Log Timer", + .period = 2s, + .callback = + [&logger, &counter]() { + logger.info("Timer tick #{}", counter++); + if (counter % 3 == 0) { + logger.warn("Warning message every 3 ticks"); + } + if (counter % 5 == 0) { + logger.error("Error message every 5 ticks"); + } + return false; // don't stop + }, + .stack_size_bytes = 6192}); + + logger.info("Timer started - generating log messages every 2 seconds"); + // Keep running while (true) { std::this_thread::sleep_for(1s); diff --git a/components/remote_debug/example/partitions.csv b/components/remote_debug/example/partitions.csv index c4217ab9e..419600340 100644 --- a/components/remote_debug/example/partitions.csv +++ b/components/remote_debug/example/partitions.csv @@ -1,5 +1,5 @@ -# ESP-IDF Partition Table -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x6000, -phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1500K, +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x6000 +phy_init, data, phy, 0xf000, 0x1000 +factory, app, factory, 0x10000, 2M +littlefs, data, littlefs, , 1M diff --git a/components/remote_debug/example/sdkconfig.defaults b/components/remote_debug/example/sdkconfig.defaults index 504883aa7..0e3541e7b 100644 --- a/components/remote_debug/example/sdkconfig.defaults +++ b/components/remote_debug/example/sdkconfig.defaults @@ -1,6 +1,9 @@ # ESP32-specific CONFIG_IDF_TARGET="esp32s3" +# set the main task stack size to 8k +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + # Flash size CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_FLASHSIZE="4MB" diff --git a/components/remote_debug/include/remote_debug.hpp b/components/remote_debug/include/remote_debug.hpp index 422a9db97..816c850b5 100644 --- a/components/remote_debug/include/remote_debug.hpp +++ b/components/remote_debug/include/remote_debug.hpp @@ -56,14 +56,18 @@ class RemoteDebug : public BaseComponent { * @brief Configuration for remote debug */ struct Config { - std::string device_name{"ESP32 Device"}; ///< Device name shown in UI title - std::vector gpios; ///< GPIO pins to expose - std::vector adc1_channels; ///< ADC1 channels to monitor - std::vector adc2_channels; ///< ADC2 channels to monitor - uint16_t server_port{8080}; ///< HTTP server port - std::chrono::milliseconds adc_sample_rate{100}; ///< ADC sampling interval - std::chrono::milliseconds gpio_update_rate{100}; ///< GPIO state update interval - size_t adc_history_size{1000}; ///< Number of ADC samples to keep + std::string device_name{"ESP32 Device"}; ///< Device name shown in UI title + std::vector gpios; ///< GPIO pins to expose + std::vector adc1_channels; ///< ADC1 channels to monitor + std::vector adc2_channels; ///< ADC2 channels to monitor + uint16_t server_port{8080}; ///< HTTP server port + std::chrono::milliseconds adc_sample_rate{100}; ///< ADC sampling interval + std::chrono::milliseconds gpio_update_rate{100}; ///< GPIO state update interval + size_t adc_history_size{1000}; ///< Number of ADC samples to keep + bool enable_log_capture{false}; ///< Enable stdout redirection to file + std::string log_file_path{ + "debug.log"}; ///< Path to log file. Will be appended to espp::FileSystem::get_root_path(). + size_t max_log_size{100000}; ///< Maximum log file size in bytes Logger::Verbosity log_level{Logger::Verbosity::WARN}; ///< Log verbosity }; @@ -131,10 +135,14 @@ class RemoteDebug : public BaseComponent { static esp_err_t gpio_set_handler(httpd_req_t *req); static esp_err_t gpio_config_handler(httpd_req_t *req); static esp_err_t adc_data_handler(httpd_req_t *req); + static esp_err_t logs_handler(httpd_req_t *req); std::string generate_html() const; std::string get_gpio_state_json() const; std::string get_adc_data_json() const; + std::string get_logs() const; + void setup_log_redirection(); + void cleanup_log_redirection(); Config config_; httpd_handle_t server_{nullptr}; @@ -162,6 +170,10 @@ class RemoteDebug : public BaseComponent { std::atomic sampling_active_{false}; std::unique_ptr sampling_thread_; std::unique_ptr gpio_thread_; + + // Log redirection + FILE *log_file_{nullptr}; + FILE *original_stdout_{nullptr}; }; } // namespace espp diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index 6f8c880be..25c7efa71 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -1,9 +1,13 @@ #include "remote_debug.hpp" #include +#include +#include #include #include +#include "file_system.hpp" + using namespace espp; RemoteDebug::RemoteDebug(const Config &config) @@ -97,6 +101,11 @@ bool RemoteDebug::start() { gpio_thread_ = std::make_unique([this]() { gpio_update_task(); }); } + // Setup log redirection if enabled + if (config_.enable_log_capture) { + setup_log_redirection(); + } + is_active_ = true; logger_.info("RemoteDebug started on port {}", config_.server_port); return true; @@ -115,6 +124,7 @@ void RemoteDebug::stop() { gpio_thread_->join(); } + cleanup_log_redirection(); stop_server(); is_active_ = false; logger_.info("RemoteDebug stopped"); @@ -249,6 +259,10 @@ bool RemoteDebug::start_server() { .uri = "/api/adc/data", .method = HTTP_GET, .handler = adc_data_handler, .user_ctx = this}; httpd_register_uri_handler(server_, &adc_data_uri); + httpd_uri_t logs_uri = { + .uri = "/api/logs", .method = HTTP_GET, .handler = logs_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &logs_uri); + return true; } @@ -429,6 +443,16 @@ button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0. ss << "
"; } + // Log viewer + if (config_.enable_log_capture) { + ss << R"(
+

Console Logs

+
+
Loading logs...
+
+
)"; + } + // JavaScript ss << R"(
)"; return ss.str(); @@ -676,3 +772,109 @@ std::string RemoteDebug::get_adc_data_json() const { ss << "}"; return ss.str(); } + +void RemoteDebug::setup_log_redirection() { + if (!config_.enable_log_capture) { + logger_.debug("Log capture not enabled"); + return; + } + + auto &fs = FileSystem::get(); + auto log_path = fs.get_root_path() / config_.log_file_path; + + logger_.info("Attempting to redirect stdout to: {}", log_path); + + // Save original stdout before redirecting + original_stdout_ = stdout; + + // Redirect stdout using freopen + log_file_ = freopen(log_path.string().c_str(), "w", stdout); + if (!log_file_) { + logger_.error("Failed to redirect stdout to log file: {} (errno: {})", log_path, + strerror(errno)); + return; + } + + // remove file buffer so logs are written immediately + setvbuf(log_file_, nullptr, _IONBF, 0); + + // Write test messages directly to stdout (which is now the file) + fmt::print("=== Log capture started at {} s ===\n", Logger::get_time()); + fmt::print("Remote Debug log file: {}\n", log_path); + fflush(stdout); +} + +void RemoteDebug::cleanup_log_redirection() { + if (log_file_) { + printf("=== Log capture stopped ===\n"); + fflush(log_file_); + + // Restore original stdout + if (original_stdout_) { + freopen("/dev/null", "w", stdout); // dummy call to reset stdout + *stdout = *original_stdout_; // restore the FILE structure + original_stdout_ = nullptr; + } + + // Don't close log_file_ since it's the same as stdout after freopen + log_file_ = nullptr; + } +} + +std::string RemoteDebug::get_logs() const { + if (!config_.enable_log_capture) { + return "Log capture not enabled"; + } + + // Flush stdout to ensure all logs are written + if (log_file_) { + fflush(log_file_); + } + + auto &fs = FileSystem::get(); + auto log_path = fs.get_root_path() / config_.log_file_path; + + // Open a separate file handle for reading + FILE *read_file = fopen(log_path.string().c_str(), "r"); + if (!read_file) { + return fmt::format("Failed to open log file for reading (errno: {})", strerror(errno)); + } + + // Get file size + fseek(read_file, 0, SEEK_END); + long file_size = ftell(read_file); + + if (file_size <= 0) { + fclose(read_file); + return "Log file is empty"; + } + + // Limit to max_log_size and seek to start of content we want to read + long read_size = std::min(file_size, static_cast(config_.max_log_size)); + long start_pos = file_size - read_size; + fseek(read_file, start_pos, SEEK_SET); + + // Read content + std::string content; + content.resize(read_size); + size_t bytes_read = fread(&content[0], 1, read_size, read_file); + content.resize(bytes_read); + fclose(read_file); + + if (bytes_read == 0) { + return fmt::format("Failed to read log file (errno: {}) (read_size: {})", strerror(errno), + read_size); + } + + return content; +} + +esp_err_t RemoteDebug::logs_handler(httpd_req_t *req) { + RemoteDebug *self = static_cast(req->user_ctx); + + std::string logs = self->get_logs(); + + httpd_resp_set_type(req, "text/plain"); + httpd_resp_send(req, logs.c_str(), logs.length()); + return ESP_OK; +} From 61587ff28d39adf20a7c08cd22818ca38c269399 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 26 Jan 2026 22:35:01 -0600 Subject: [PATCH 05/20] finalizing the component --- components/remote_debug/README.md | 69 +++++++++++++++++++ components/remote_debug/example/README.md | 11 ++- .../example/main/Kconfig.projbuild | 60 ++++++++++++++++ .../example/main/remote_debug_example.cpp | 47 +++++++++---- .../remote_debug/example/sdkconfig.defaults | 13 +++- components/remote_debug/src/remote_debug.cpp | 10 +++ 6 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 components/remote_debug/README.md diff --git a/components/remote_debug/README.md b/components/remote_debug/README.md new file mode 100644 index 000000000..e65675ea0 --- /dev/null +++ b/components/remote_debug/README.md @@ -0,0 +1,69 @@ +# Remote Debug + +Web-based remote debugging interface providing GPIO control, real-time ADC monitoring, and optional console log viewing over HTTP. + +## Features + +- **GPIO Control**: Configure pins as input/output, read states, control outputs via web interface +- **ADC Monitoring**: Real-time visualization of up to 8 ADC channels with configurable sample rates +- **Console Log Viewer**: Optional stdout redirection to web-viewable log with ANSI color support +- **Clean API**: RESTful JSON endpoints for programmatic access +- **Responsive UI**: Modern web interface that works on desktop and mobile + +## Usage + +```cpp +#include "remote_debug.hpp" + +// Configure GPIOs to expose +std::vector gpios = { + {.pin = 2, .label = "LED"}, + {.pin = 4, .label = "Button"} +}; + +// Configure ADC channels to monitor +std::vector adc1_channels = { + {.channel = ADC1_CHANNEL_0, .atten = ADC_ATTEN_DB_12, .label = "Battery Voltage"} +}; + +// Create remote debug server +espp::RemoteDebug::Config config{ + .server_address = "0.0.0.0", + .server_port = 8080, + .title = "My Device Debug", + .gpios = gpios, + .adc1_channels = adc1_channels, + .adc_sample_rate = 100.0f, // Hz + .adc_buffer_size = 1000, + .enable_logging = true, // Enable console log viewer + .log_buffer_size = 4096, + .log_level = espp::Logger::Verbosity::INFO +}; + +espp::RemoteDebug debug(config); +debug.start(); + +// Access at http://:8080 +``` + +## Console Logging + +When `enable_logging` is true, the component redirects `stdout` to a file that can be viewed in the web interface. The log viewer supports ANSI color codes for styled output. + +**Important**: For real-time log updates, you must enable LittleFS file flushing: + +``` +CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y +``` + +Set this in your `sdkconfig.defaults` or via `idf.py menuconfig` → Component config → LittleFS. + +Without this setting, logs will only appear after the file buffer is full or the file is closed. + +## API + +See the [Remote Debug API documentation](https://esp-cpp.github.io/espp/html/classespp_1_1_remote_debug.html) for detailed information. + +## Example + +See the [remote_debug example](example/README.md) for a complete working example with WiFi connection, GPIO control, ADC monitoring, and console log viewing. diff --git a/components/remote_debug/example/README.md b/components/remote_debug/example/README.md index cb6dfe258..c53a59661 100644 --- a/components/remote_debug/example/README.md +++ b/components/remote_debug/example/README.md @@ -22,10 +22,17 @@ Navigate to `Remote Debug Example Configuration`: - Set WiFi SSID and password - Configure server port (default: 8080) - Set number of GPIOs to expose (1-10) -- Configure which GPIO pins to use +- Configure which GPIO pins to use and their labels - Set number of ADC channels (0-8) -- Configure which ADC pins to monitor +- Configure which ADC pins to monitor and their labels - Set ADC sample rate and buffer size +- Enable/disable console log viewer +- Set log buffer size + +**Important for Console Logging**: If you enable the console log viewer, you must also enable: +- Component config → LittleFS → `CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y` + +This ensures logs appear in real-time on the web interface. Without this setting, logs will only show after the buffer fills or the file closes. ### Build and Flash diff --git a/components/remote_debug/example/main/Kconfig.projbuild b/components/remote_debug/example/main/Kconfig.projbuild index 5932139ad..639578f6e 100644 --- a/components/remote_debug/example/main/Kconfig.projbuild +++ b/components/remote_debug/example/main/Kconfig.projbuild @@ -191,6 +191,13 @@ menu "Remote Debug Example Configuration" help Third ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_2_LABEL + string "ADC Channel 2 Label" + default "ADC 34" + depends on REMOTE_DEBUG_NUM_ADCS >= 3 + help + Label for third ADC channel. + config REMOTE_DEBUG_ADC_3 int "ADC Channel 3" default 35 @@ -199,6 +206,13 @@ menu "Remote Debug Example Configuration" help Fourth ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_3_LABEL + string "ADC Channel 3 Label" + default "ADC 35" + depends on REMOTE_DEBUG_NUM_ADCS >= 4 + help + Label for fourth ADC channel. + config REMOTE_DEBUG_ADC_4 int "ADC Channel 4" default 32 @@ -207,6 +221,13 @@ menu "Remote Debug Example Configuration" help Fifth ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_4_LABEL + string "ADC Channel 4 Label" + default "ADC 32" + depends on REMOTE_DEBUG_NUM_ADCS >= 5 + help + Label for fifth ADC channel. + config REMOTE_DEBUG_ADC_5 int "ADC Channel 5" default 33 @@ -215,6 +236,13 @@ menu "Remote Debug Example Configuration" help Sixth ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_5_LABEL + string "ADC Channel 5 Label" + default "ADC 33" + depends on REMOTE_DEBUG_NUM_ADCS >= 6 + help + Label for sixth ADC channel. + config REMOTE_DEBUG_ADC_6 int "ADC Channel 6" default 25 @@ -223,6 +251,13 @@ menu "Remote Debug Example Configuration" help Seventh ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_6_LABEL + string "ADC Channel 6 Label" + default "ADC 25" + depends on REMOTE_DEBUG_NUM_ADCS >= 7 + help + Label for seventh ADC channel. + config REMOTE_DEBUG_ADC_7 int "ADC Channel 7" default 26 @@ -231,6 +266,13 @@ menu "Remote Debug Example Configuration" help Eighth ADC pin to monitor (GPIO number). + config REMOTE_DEBUG_ADC_7_LABEL + string "ADC Channel 7 Label" + default "ADC 26" + depends on REMOTE_DEBUG_NUM_ADCS >= 8 + help + Label for eighth ADC channel. + config REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ int "ADC Sample Rate (Hz)" default 100 @@ -249,4 +291,22 @@ menu "Remote Debug Example Configuration" endmenu + menu "Log Configuration" + + config REMOTE_DEBUG_ENABLE_LOGS + bool "Enable Log Capture" + default y + help + Enable capturing stdout to a log file that can be viewed remotely. + + config REMOTE_DEBUG_LOG_BUFFER_SIZE + int "Log Buffer Size (bytes)" + default 8192 + range 1024 65536 + depends on REMOTE_DEBUG_ENABLE_LOGS + help + Maximum size of the log buffer/file in bytes. When full, old logs will be overwritten. + + endmenu + endmenu diff --git a/components/remote_debug/example/main/remote_debug_example.cpp b/components/remote_debug/example/main/remote_debug_example.cpp index 07f5fa479..f59d83817 100644 --- a/components/remote_debug/example/main/remote_debug_example.cpp +++ b/components/remote_debug/example/main/remote_debug_example.cpp @@ -96,26 +96,43 @@ extern "C" void app_main(void) { .label = CONFIG_REMOTE_DEBUG_ADC_1_LABEL}); #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 3 - adc1_channels.push_back( - {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_2), .label = "A2"}); + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_2), + .label = CONFIG_REMOTE_DEBUG_ADC_2_LABEL}); #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 4 - adc1_channels.push_back( - {.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_3), .label = "A3"}); + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_3), + .label = CONFIG_REMOTE_DEBUG_ADC_3_LABEL}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 5 + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_4), + .label = CONFIG_REMOTE_DEBUG_ADC_4_LABEL}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 6 + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_5), + .label = CONFIG_REMOTE_DEBUG_ADC_5_LABEL}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 7 + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_6), + .label = CONFIG_REMOTE_DEBUG_ADC_6_LABEL}); +#endif +#if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 8 + adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_7), + .label = CONFIG_REMOTE_DEBUG_ADC_7_LABEL}); #endif // Configure remote debug - espp::RemoteDebug::Config config{ - .device_name = CONFIG_REMOTE_DEBUG_DEVICE_NAME, - .gpios = gpios, - .adc1_channels = adc1_channels, - .adc2_channels = {}, - .server_port = static_cast(CONFIG_REMOTE_DEBUG_SERVER_PORT), - .adc_sample_rate = std::chrono::milliseconds(1000 / adc_sample_rate_hz), - .gpio_update_rate = std::chrono::milliseconds(100), - .adc_history_size = adc_buffer_size, - .enable_log_capture = true, - .log_level = espp::Logger::Verbosity::INFO}; + espp::RemoteDebug::Config config { + .device_name = CONFIG_REMOTE_DEBUG_DEVICE_NAME, .gpios = gpios, .adc1_channels = adc1_channels, + .adc2_channels = {}, .server_port = static_cast(CONFIG_REMOTE_DEBUG_SERVER_PORT), + .adc_sample_rate = std::chrono::milliseconds(1000 / adc_sample_rate_hz), + .gpio_update_rate = std::chrono::milliseconds(100), .adc_history_size = adc_buffer_size, +#if CONFIG_REMOTE_DEBUG_ENABLE_LOGS + .enable_log_capture = true, .max_log_size = CONFIG_REMOTE_DEBUG_LOG_BUFFER_SIZE, +#else + .enable_log_capture = false, +#endif + .log_level = espp::Logger::Verbosity::INFO + }; espp::RemoteDebug remote_debug(config); remote_debug.start(); diff --git a/components/remote_debug/example/sdkconfig.defaults b/components/remote_debug/example/sdkconfig.defaults index 0e3541e7b..966587288 100644 --- a/components/remote_debug/example/sdkconfig.defaults +++ b/components/remote_debug/example/sdkconfig.defaults @@ -1,6 +1,13 @@ # ESP32-specific CONFIG_IDF_TARGET="esp32s3" +# set cpu speed to 240MHz +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240 + +# FreeRTOS tick rate to 1000Hz +CONFIG_FREERTOS_HZ=1000 + # set the main task stack size to 8k CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 @@ -14,9 +21,11 @@ CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" CONFIG_PARTITION_TABLE_OFFSET=0x8000 CONFIG_PARTITION_TABLE_MD5=y +# littlefs settings +CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y +CONFIG_LITTLEFS_MMAP_PARTITION=y + # Default settings -CONFIG_REMOTE_DEBUG_WIFI_SSID="myssid" -CONFIG_REMOTE_DEBUG_WIFI_PASSWORD="mypassword" CONFIG_REMOTE_DEBUG_SERVER_PORT=8080 CONFIG_REMOTE_DEBUG_NUM_GPIOS=4 CONFIG_REMOTE_DEBUG_GPIO_0=16 diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index 25c7efa71..1add67aa5 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -779,10 +779,20 @@ void RemoteDebug::setup_log_redirection() { return; } +#ifndef CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE + logger_.warn("**************************************************************"); + logger_.warn("WARNING: CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not enabled!"); + logger_.warn("Logs will not appear in real-time on the web interface."); + logger_.warn("Enable this option in menuconfig:"); + logger_.warn(" Component config -> LittleFS -> Flush file every write"); + logger_.warn("**************************************************************"); +#endif + auto &fs = FileSystem::get(); auto log_path = fs.get_root_path() / config_.log_file_path; logger_.info("Attempting to redirect stdout to: {}", log_path); + logger_.info("Log buffer size: {} bytes", config_.max_log_size); // Save original stdout before redirecting original_stdout_ = stdout; From ee1bf8b3e8d295f69076e2b93fed4468ab0907f9 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 26 Jan 2026 22:37:02 -0600 Subject: [PATCH 06/20] cleanup --- components/remote_debug/example/sdkconfig.defaults | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/components/remote_debug/example/sdkconfig.defaults b/components/remote_debug/example/sdkconfig.defaults index 966587288..ab2f4d832 100644 --- a/components/remote_debug/example/sdkconfig.defaults +++ b/components/remote_debug/example/sdkconfig.defaults @@ -27,13 +27,16 @@ CONFIG_LITTLEFS_MMAP_PARTITION=y # Default settings CONFIG_REMOTE_DEBUG_SERVER_PORT=8080 -CONFIG_REMOTE_DEBUG_NUM_GPIOS=4 -CONFIG_REMOTE_DEBUG_GPIO_0=16 -CONFIG_REMOTE_DEBUG_GPIO_1=35 -CONFIG_REMOTE_DEBUG_GPIO_2=36 -CONFIG_REMOTE_DEBUG_GPIO_3=37 +CONFIG_REMOTE_DEBUG_NUM_GPIOS=2 +CONFIG_REMOTE_DEBUG_GPIO_0=17 +CONFIG_REMOTE_DEBUG_GPIO_0_LABEL="LED" +CONFIG_REMOTE_DEBUG_GPIO_1=18 +CONFIG_REMOTE_DEBUG_GPIO_1_LABEL="Button" CONFIG_REMOTE_DEBUG_NUM_ADCS=2 CONFIG_REMOTE_DEBUG_ADC_0=7 +CONFIG_REMOTE_DEBUG_ADC_0_LABEL="Joy LX" CONFIG_REMOTE_DEBUG_ADC_1=8 +CONFIG_REMOTE_DEBUG_ADC_1_LABEL="Joy LY" CONFIG_REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ=100 CONFIG_REMOTE_DEBUG_ADC_BUFFER_SIZE=500 +CONFIG_REMOTE_DEBUG_ENABLE_LOGS=y From a7cf22343e48e31a2a5491511b5ba472d17fe939 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 26 Jan 2026 23:19:33 -0600 Subject: [PATCH 07/20] wip better analog plotting --- components/remote_debug/CMakeLists.txt | 2 +- components/remote_debug/README.md | 21 +- components/remote_debug/idf_component.yml | 2 + .../remote_debug/include/remote_debug.hpp | 21 +- components/remote_debug/src/remote_debug.cpp | 233 +++++++++++++----- 5 files changed, 203 insertions(+), 76 deletions(-) diff --git a/components/remote_debug/CMakeLists.txt b/components/remote_debug/CMakeLists.txt index a25c1c3b1..90858ffb6 100644 --- a/components/remote_debug/CMakeLists.txt +++ b/components/remote_debug/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES esp_http_server driver adc base_component task file_system + REQUIRES esp_http_server driver adc base_component task timer file_system ) diff --git a/components/remote_debug/README.md b/components/remote_debug/README.md index e65675ea0..8d3f56eb2 100644 --- a/components/remote_debug/README.md +++ b/components/remote_debug/README.md @@ -1,14 +1,31 @@ # Remote Debug -Web-based remote debugging interface providing GPIO control, real-time ADC monitoring, and optional console log viewing over HTTP. +Web-based remote debugging interface providing GPIO control, real-time ADC monitoring, and optional console log viewing over HTTP. Uses espp::Timer for efficient, configurable periodic updates. ## Features - **GPIO Control**: Configure pins as input/output, read states, control outputs via web interface -- **ADC Monitoring**: Real-time visualization of up to 8 ADC channels with configurable sample rates +- **ADC Monitoring**: Real-time visualization of ADC channels with configurable sample rates and batching - **Console Log Viewer**: Optional stdout redirection to web-viewable log with ANSI color support +- **Efficient Updates**: Uses espp::Timer with configurable priority and stack size for optimal performance - **Clean API**: RESTful JSON endpoints for programmatic access - **Responsive UI**: Modern web interface that works on desktop and mobile +- **Multi-client Support**: Optimized for multiple concurrent clients through batched updates + +## Performance Considerations + +The remote debug component has been optimized for efficiency: + +- Uses `espp::Timer` instead of raw threads for precise, lightweight periodic updates +- Configurable task priority and stack size for both GPIO and ADC sampling +- Batched ADC data updates reduce HTTP overhead +- Single consolidated update endpoint minimizes request count +- Efficient JSON generation for minimal processing overhead + +For best performance with multiple clients: +- Increase `adc_batch_size` to reduce update frequency +- Adjust `adc_sample_rate` and `gpio_update_rate` based on actual needs +- Configure appropriate `task_priority` and `task_stack_size` for your application ## Usage diff --git a/components/remote_debug/idf_component.yml b/components/remote_debug/idf_component.yml index 25f427d0e..41a49af5d 100644 --- a/components/remote_debug/idf_component.yml +++ b/components/remote_debug/idf_component.yml @@ -18,3 +18,5 @@ dependencies: path: ../base_component espp/oneshot_adc: path: ../oneshot_adc + espp/timer: + path: ../timer diff --git a/components/remote_debug/include/remote_debug.hpp b/components/remote_debug/include/remote_debug.hpp index 816c850b5..783d32a91 100644 --- a/components/remote_debug/include/remote_debug.hpp +++ b/components/remote_debug/include/remote_debug.hpp @@ -3,10 +3,10 @@ #include #include #include -#include #include #include #include +#include #include #include @@ -14,6 +14,7 @@ #include "base_component.hpp" #include "oneshot_adc.hpp" +#include "timer.hpp" namespace espp { /** @@ -64,6 +65,9 @@ class RemoteDebug : public BaseComponent { std::chrono::milliseconds adc_sample_rate{100}; ///< ADC sampling interval std::chrono::milliseconds gpio_update_rate{100}; ///< GPIO state update interval size_t adc_history_size{1000}; ///< Number of ADC samples to keep + size_t adc_batch_size{10}; ///< Number of ADC samples to send per update + size_t task_priority{5}; ///< Priority for update tasks + size_t task_stack_size{4096}; ///< Stack size for update tasks bool enable_log_capture{false}; ///< Enable stdout redirection to file std::string log_file_path{ "debug.log"}; ///< Path to log file. Will be appended to espp::FileSystem::get_root_path(). @@ -150,15 +154,16 @@ class RemoteDebug : public BaseComponent { std::unique_ptr adc1_; std::unique_ptr adc2_; - std::map gpio_map_; - std::map gpio_state_; // Cached GPIO states + std::unordered_map gpio_map_; + std::unordered_map gpio_state_; // Cached GPIO states std::mutex gpio_mutex_; - // ADC data storage + // ADC data storage - ring buffer implementation struct AdcData { - std::vector values; + std::vector values; std::vector timestamps; - size_t write_index{0}; + size_t write_index{0}; // Current write position in ring buffer + size_t count{0}; // Number of valid samples (up to buffer size) adc_channel_t channel; std::string label; }; @@ -168,8 +173,8 @@ class RemoteDebug : public BaseComponent { std::atomic is_active_{false}; std::atomic sampling_active_{false}; - std::unique_ptr sampling_thread_; - std::unique_ptr gpio_thread_; + std::unique_ptr adc_timer_; + std::unique_ptr gpio_timer_; // Log redirection FILE *log_file_{nullptr}; diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index 1add67aa5..242fe41b5 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -40,8 +40,13 @@ void RemoteDebug::init(const Config &config) { gpio_state_[gpio.pin]); } + logger_.debug("Configured {} GPIOs", config_.gpios.size()); + + logger_.info("ADC history size: {}", config_.adc_history_size); + // Initialize ADC1 if (!config_.adc1_channels.empty()) { + logger_.info("Initializing ADC1 with {} channels", config_.adc1_channels.size()); std::vector adc1_configs; for (const auto &ch : config_.adc1_channels) { adc1_configs.push_back({.unit = ADC_UNIT_1, .channel = ch.channel, .attenuation = ch.atten}); @@ -60,6 +65,7 @@ void RemoteDebug::init(const Config &config) { // Initialize ADC2 if (!config_.adc2_channels.empty()) { + logger_.info("Initializing ADC2 with {} channels", config_.adc2_channels.size()); std::vector adc2_configs; for (const auto &ch : config_.adc2_channels) { adc2_configs.push_back({.unit = ADC_UNIT_2, .channel = ch.channel, .attenuation = ch.atten}); @@ -90,15 +96,38 @@ bool RemoteDebug::start() { return false; } - // Start ADC sampling task if we have ADCs + // Start ADC sampling timer if we have ADCs if (adc1_ || adc2_) { + logger_.info("Starting ADC sampling timer with period {} ms", config_.adc_sample_rate.count()); sampling_active_ = true; - sampling_thread_ = std::make_unique([this]() { adc_sampling_task(); }); - } - - // Start GPIO update task + adc_timer_ = std::make_unique(Timer::Config{.name = "adc_timer", + .period = config_.adc_sample_rate, + .callback = + [this]() { + adc_sampling_task(); + return false; + }, + .auto_start = false, + .stack_size_bytes = config_.task_stack_size, + .priority = config_.task_priority, + .log_level = config_.log_level}); + adc_timer_->start(); + } + + // Start GPIO update timer if (!gpio_map_.empty()) { - gpio_thread_ = std::make_unique([this]() { gpio_update_task(); }); + gpio_timer_ = std::make_unique(Timer::Config{.name = "gpio_timer", + .period = config_.gpio_update_rate, + .callback = + [this]() { + gpio_update_task(); + return false; + }, + .auto_start = false, + .stack_size_bytes = config_.task_stack_size, + .priority = config_.task_priority, + .log_level = config_.log_level}); + gpio_timer_->start(); } // Setup log redirection if enabled @@ -117,11 +146,15 @@ void RemoteDebug::stop() { } sampling_active_ = false; - if (sampling_thread_ && sampling_thread_->joinable()) { - sampling_thread_->join(); + + // Stop timers + if (adc_timer_) { + adc_timer_->stop(); + adc_timer_.reset(); } - if (gpio_thread_ && gpio_thread_->joinable()) { - gpio_thread_->join(); + if (gpio_timer_) { + gpio_timer_->stop(); + gpio_timer_.reset(); } cleanup_log_redirection(); @@ -182,46 +215,45 @@ bool RemoteDebug::configure_gpio(gpio_num_t pin, gpio_mode_t mode) { } void RemoteDebug::gpio_update_task() { - while (sampling_active_) { - { - std::lock_guard lock(gpio_mutex_); - for (auto &[pin, config] : gpio_map_) { - gpio_state_[pin] = gpio_get_level(pin); - } - } - std::this_thread::sleep_for(config_.gpio_update_rate); + std::lock_guard lock(gpio_mutex_); + for (auto &[pin, config] : gpio_map_) { + gpio_state_[pin] = gpio_get_level(pin); } } void RemoteDebug::adc_sampling_task() { - while (sampling_active_) { - auto now = esp_timer_get_time(); - - std::lock_guard lock(adc_mutex_); - - // Sample ADC1 - if (adc1_) { - auto values = adc1_->read_all_mv(); - for (size_t i = 0; i < values.size() && i < adc1_data_.size(); i++) { - auto &data = adc1_data_[i]; - data.values[data.write_index] = values[i]; - data.timestamps[data.write_index] = now; - data.write_index = (data.write_index + 1) % config_.adc_history_size; + auto now = esp_timer_get_time(); + + std::lock_guard lock(adc_mutex_); + + // Sample ADC1 + if (adc1_) { + auto values = adc1_->read_all_mv(); + for (size_t i = 0; i < values.size() && i < adc1_data_.size(); i++) { + auto &data = adc1_data_[i]; + // Convert mV to V + data.values[data.write_index] = values[i] / 1000.0f; + data.timestamps[data.write_index] = now; + data.write_index = (data.write_index + 1) % config_.adc_history_size; + if (data.count < config_.adc_history_size) { + data.count++; } } + } - // Sample ADC2 - if (adc2_) { - auto values = adc2_->read_all_mv(); - for (size_t i = 0; i < values.size() && i < adc2_data_.size(); i++) { - auto &data = adc2_data_[i]; - data.values[data.write_index] = values[i]; - data.timestamps[data.write_index] = now; - data.write_index = (data.write_index + 1) % config_.adc_history_size; + // Sample ADC2 + if (adc2_) { + auto values = adc2_->read_all_mv(); + for (size_t i = 0; i < values.size() && i < adc2_data_.size(); i++) { + auto &data = adc2_data_[i]; + // Convert mV to V + data.values[data.write_index] = values[i] / 1000.0f; + data.timestamps[data.write_index] = now; + data.write_index = (data.write_index + 1) % config_.adc_history_size; + if (data.count < config_.adc_history_size) { + data.count++; } } - - std::this_thread::sleep_for(config_.adc_sample_rate); } } @@ -514,26 +546,37 @@ function updateAdc() { fetch('/api/adc/data') .then(r => r.json()) .then(data => { - const now = Date.now(); for (let key in data) { // key format is "1_8" for ADC1 channel 8 const elem = document.getElementById('adc' + key); if (elem) { - const volts = (data[key].voltage / 1000.0).toFixed(3); + const volts = data[key].voltage.toFixed(3); elem.innerText = volts + ' V'; } if (!adcHistory[key]) { - adcHistory[key] = { values: [], times: [], color: colors[colorIndex++ % colors.length], label: data[key].label }; + adcHistory[key] = { + values: [], + color: colors[colorIndex++ % colors.length], + label: data[key].label, + lastIndex: 0 + }; } - adcHistory[key].values.push(data[key].voltage / 1000.0); // Store in volts - adcHistory[key].times.push(now); - - // Keep last 100 samples - if (adcHistory[key].values.length > 100) { - adcHistory[key].values.shift(); - adcHistory[key].times.shift(); + // Get batched history data + const history = data[key].history; + if (history && history.values && history.values.length > 0) { + // Append new values from the batch + for (let i = 0; i < history.values.length; i++) { + adcHistory[key].values.push(history.values[i]); + } + + // Keep only the most recent samples (limit client-side storage) + const maxSamples = 1000; + if (adcHistory[key].values.length > maxSamples) { + const excess = adcHistory[key].values.length - maxSamples; + adcHistory[key].values.splice(0, excess); + } } } drawChart(); @@ -733,18 +776,48 @@ std::string RemoteDebug::get_adc_data_json() const { if (!first) ss << ","; - auto idx = (data.write_index > 0) ? (data.write_index - 1) : (config_.adc_history_size - 1); - int voltage_mv = data.values[idx]; + // Get the most recent value + size_t read_idx = (data.write_index > 0) ? (data.write_index - 1) : (data.count - 1); + float voltage = (data.count > 0) ? data.values[read_idx] : 0.0f; // Key format: "1_5" for ADC1 channel 5 ss << "\"1_" << static_cast(data.channel) << "\":{"; - ss << "\"voltage\":" << voltage_mv << ","; - ss << "\"current\":" << voltage_mv << ","; + ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage << ","; + ss << "\"current\":" << voltage << ","; ss << "\"label\":\"" - << (data.label.empty() ? ("ADC1_CH" + std::to_string(static_cast(data.channel))) + << (data.label.empty() ? ("ADC 1_" + std::to_string(static_cast(data.channel))) : data.label) - << "\""; - ss << "}"; + << "\","; + + // Send batched data for plotting (last N samples) + ss << "\"history\":{"; + ss << "\"count\":" << data.count << ","; + ss << "\"write_index\":" << data.write_index << ","; + ss << "\"values\":["; + + // Send the most recent batch_size samples + size_t num_samples = std::min(config_.adc_batch_size, data.count); + for (size_t j = 0; j < num_samples; j++) { + if (j > 0) + ss << ","; + // Calculate index going backwards from write_index + size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % + config_.adc_history_size; + ss << std::fixed << std::setprecision(3) << data.values[idx]; + } + ss << "],"; + + ss << "\"timestamps\":["; + for (size_t j = 0; j < num_samples; j++) { + if (j > 0) + ss << ","; + size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % + config_.adc_history_size; + ss << data.timestamps[idx]; + } + ss << "]"; + ss << "}"; // close history + ss << "}"; // close channel first = false; } @@ -754,18 +827,48 @@ std::string RemoteDebug::get_adc_data_json() const { if (!first) ss << ","; - auto idx = (data.write_index > 0) ? (data.write_index - 1) : (config_.adc_history_size - 1); - int voltage_mv = data.values[idx]; + // Get the most recent value + size_t read_idx = (data.write_index > 0) ? (data.write_index - 1) : (data.count - 1); + float voltage = (data.count > 0) ? data.values[read_idx] : 0.0f; // Key format: "2_5" for ADC2 channel 5 ss << "\"2_" << static_cast(data.channel) << "\":{"; - ss << "\"voltage\":" << voltage_mv << ","; - ss << "\"current\":" << voltage_mv << ","; + ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage << ","; + ss << "\"current\":" << voltage << ","; ss << "\"label\":\"" - << (data.label.empty() ? ("ADC2_CH" + std::to_string(static_cast(data.channel))) + << (data.label.empty() ? ("ADC 2_" + std::to_string(static_cast(data.channel))) : data.label) - << "\""; - ss << "}"; + << "\","; + + // Send batched data for plotting (last N samples) + ss << "\"history\":{"; + ss << "\"count\":" << data.count << ","; + ss << "\"write_index\":" << data.write_index << ","; + ss << "\"values\":["; + + // Send the most recent batch_size samples + size_t num_samples = std::min(config_.adc_batch_size, data.count); + for (size_t j = 0; j < num_samples; j++) { + if (j > 0) + ss << ","; + // Calculate index going backwards from write_index + size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % + config_.adc_history_size; + ss << std::fixed << std::setprecision(3) << data.values[idx]; + } + ss << "],"; + + ss << "\"timestamps\":["; + for (size_t j = 0; j < num_samples; j++) { + if (j > 0) + ss << ","; + size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % + config_.adc_history_size; + ss << data.timestamps[idx]; + } + ss << "]"; + ss << "}"; // close history + ss << "}"; // close channel first = false; } From f019d1b7cb808043f3d572b7fd4fdb0189cfb6a4 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 26 Jan 2026 23:48:10 -0600 Subject: [PATCH 08/20] improve behavior / performance --- components/remote_debug/src/remote_debug.cpp | 152 ++++++++++--------- 1 file changed, 83 insertions(+), 69 deletions(-) diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index 242fe41b5..1bc971448 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -42,7 +42,14 @@ void RemoteDebug::init(const Config &config) { logger_.debug("Configured {} GPIOs", config_.gpios.size()); - logger_.info("ADC history size: {}", config_.adc_history_size); + // Calculate actual history size based on sample rate + // adc_history_size is the number of samples we want to keep + // Sample rate determines how many samples per second + // So actual time = adc_history_size / samples_per_second + float samples_per_second = 1000.0f / config_.adc_sample_rate.count(); + float actual_history_seconds = config_.adc_history_size / samples_per_second; + logger_.info("ADC history: {} samples at {} Hz = {:.2f} seconds", config_.adc_history_size, + samples_per_second, actual_history_seconds); // Initialize ADC1 if (!config_.adc1_channels.empty()) { @@ -547,35 +554,32 @@ function updateAdc() { .then(r => r.json()) .then(data => { for (let key in data) { - // key format is "1_8" for ADC1 channel 8 - const elem = document.getElementById('adc' + key); + const adcData = data[key]; + const unit = adcData.unit; + const channel = adcData.channel; + + // Update the ADC value display using unit and channel + const elem = document.getElementById(`adc${unit}_${channel}`); if (elem) { - const volts = data[key].voltage.toFixed(3); + const volts = adcData.voltage.toFixed(3); elem.innerText = volts + ' V'; } if (!adcHistory[key]) { adcHistory[key] = { - values: [], + data: [], // Store [timestamp, value] pairs color: colors[colorIndex++ % colors.length], - label: data[key].label, - lastIndex: 0 + label: adcData.label }; } - // Get batched history data - const history = data[key].history; - if (history && history.values && history.values.length > 0) { - // Append new values from the batch + // Get batched history data - replace the entire dataset + const history = adcData.history; + if (history && history.values && history.timestamps && history.values.length > 0) { + // Replace the entire dataset with fresh data from the ring buffer + adcHistory[key].data = []; for (let i = 0; i < history.values.length; i++) { - adcHistory[key].values.push(history.values[i]); - } - - // Keep only the most recent samples (limit client-side storage) - const maxSamples = 1000; - if (adcHistory[key].values.length > maxSamples) { - const excess = adcHistory[key].values.length - maxSamples; - adcHistory[key].values.splice(0, excess); + adcHistory[key].data.push([history.timestamps[i], history.values[i]]); } } } @@ -601,10 +605,12 @@ function drawChart() { // Find min/max across all series (values are in volts) let minVal = Infinity, maxVal = -Infinity; for (let key in adcHistory) { - const vals = adcHistory[key].values; - if (vals.length > 0) { - minVal = Math.min(minVal, ...vals); - maxVal = Math.max(maxVal, ...vals); + const data = adcHistory[key].data; + if (data.length > 0) { + for (const [_, value] of data) { + minVal = Math.min(minVal, value); + maxVal = Math.max(maxVal, value); + } } } @@ -628,18 +634,30 @@ function drawChart() { ctx.fillText(val + 'V', 5, y + 4); } + // Find time range across all data + let minTime = Infinity, maxTime = -Infinity; + for (let key in adcHistory) { + const data = adcHistory[key].data; + if (data.length > 0) { + minTime = Math.min(minTime, data[0][0]); + maxTime = Math.max(maxTime, data[data.length - 1][0]); + } + } + const timeRange = maxTime - minTime || 1; + // Draw each ADC line for (let key in adcHistory) { const history = adcHistory[key]; - if (history.values.length < 2) continue; + if (history.data.length < 2) continue; ctx.strokeStyle = history.color; ctx.lineWidth = 2; ctx.beginPath(); - for (let i = 0; i < history.values.length; i++) { - const x = padding + (chartWidth * i / (history.values.length - 1)); - const y = padding + chartHeight - ((history.values[i] - minVal) / range * chartHeight); + for (let i = 0; i < history.data.length; i++) { + const [timestamp, value] = history.data[i]; + const x = padding + (chartWidth * (timestamp - minTime) / timeRange); + const y = padding + chartHeight - ((value - minVal) / range * chartHeight); if (i === 0) { ctx.moveTo(x, y); @@ -669,8 +687,8 @@ updateGpio(); updateAdc(); // Update periodically -setInterval(updateGpio, 500); -setInterval(updateAdc, 200); +setInterval(updateGpio, 200); +setInterval(updateAdc, 100); // Update logs if enabled )"; @@ -770,49 +788,47 @@ std::string RemoteDebug::get_adc_data_json() const { bool first = true; - // ADC1 data + // Process ADC1 channels for (size_t i = 0; i < adc1_data_.size(); i++) { const auto &data = adc1_data_[i]; if (!first) ss << ","; // Get the most recent value - size_t read_idx = (data.write_index > 0) ? (data.write_index - 1) : (data.count - 1); - float voltage = (data.count > 0) ? data.values[read_idx] : 0.0f; + float voltage = 0.0f; + if (data.count > 0) { + size_t latest_idx = (data.write_index + data.values.size() - 1) % data.values.size(); + voltage = data.values[latest_idx]; + } - // Key format: "1_5" for ADC1 channel 5 - ss << "\"1_" << static_cast(data.channel) << "\":{"; + ss << "\"" << data.label << "\":{"; ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage << ","; ss << "\"current\":" << voltage << ","; - ss << "\"label\":\"" - << (data.label.empty() ? ("ADC 1_" + std::to_string(static_cast(data.channel))) - : data.label) - << "\","; + ss << "\"label\":\"" << data.label << "\","; + ss << "\"unit\":1,"; + ss << "\"channel\":" << static_cast(data.channel) << ","; - // Send batched data for plotting (last N samples) + // Send all buffered data for plotting ss << "\"history\":{"; ss << "\"count\":" << data.count << ","; - ss << "\"write_index\":" << data.write_index << ","; ss << "\"values\":["; - // Send the most recent batch_size samples - size_t num_samples = std::min(config_.adc_batch_size, data.count); - for (size_t j = 0; j < num_samples; j++) { + // Send data in chronological order (oldest to newest) + const size_t count = data.count; + const size_t capacity = data.values.size(); + for (size_t j = 0; j < count; j++) { if (j > 0) ss << ","; - // Calculate index going backwards from write_index - size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % - config_.adc_history_size; + size_t idx = (data.write_index + capacity - count + j) % capacity; ss << std::fixed << std::setprecision(3) << data.values[idx]; } ss << "],"; ss << "\"timestamps\":["; - for (size_t j = 0; j < num_samples; j++) { + for (size_t j = 0; j < count; j++) { if (j > 0) ss << ","; - size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % - config_.adc_history_size; + size_t idx = (data.write_index + capacity - count + j) % capacity; ss << data.timestamps[idx]; } ss << "]"; @@ -821,49 +837,47 @@ std::string RemoteDebug::get_adc_data_json() const { first = false; } - // ADC2 data + // Process ADC2 channels for (size_t i = 0; i < adc2_data_.size(); i++) { const auto &data = adc2_data_[i]; if (!first) ss << ","; // Get the most recent value - size_t read_idx = (data.write_index > 0) ? (data.write_index - 1) : (data.count - 1); - float voltage = (data.count > 0) ? data.values[read_idx] : 0.0f; + float voltage = 0.0f; + if (data.count > 0) { + size_t latest_idx = (data.write_index + data.values.size() - 1) % data.values.size(); + voltage = data.values[latest_idx]; + } - // Key format: "2_5" for ADC2 channel 5 - ss << "\"2_" << static_cast(data.channel) << "\":{"; + ss << "\"" << data.label << "\":{"; ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage << ","; ss << "\"current\":" << voltage << ","; - ss << "\"label\":\"" - << (data.label.empty() ? ("ADC 2_" + std::to_string(static_cast(data.channel))) - : data.label) - << "\","; + ss << "\"label\":\"" << data.label << "\","; + ss << "\"unit\":2,"; + ss << "\"channel\":" << static_cast(data.channel) << ","; - // Send batched data for plotting (last N samples) + // Send all buffered data for plotting ss << "\"history\":{"; ss << "\"count\":" << data.count << ","; - ss << "\"write_index\":" << data.write_index << ","; ss << "\"values\":["; - // Send the most recent batch_size samples - size_t num_samples = std::min(config_.adc_batch_size, data.count); - for (size_t j = 0; j < num_samples; j++) { + // Send data in chronological order (oldest to newest) + const size_t count = data.count; + const size_t capacity = data.values.size(); + for (size_t j = 0; j < count; j++) { if (j > 0) ss << ","; - // Calculate index going backwards from write_index - size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % - config_.adc_history_size; + size_t idx = (data.write_index + capacity - count + j) % capacity; ss << std::fixed << std::setprecision(3) << data.values[idx]; } ss << "],"; ss << "\"timestamps\":["; - for (size_t j = 0; j < num_samples; j++) { + for (size_t j = 0; j < count; j++) { if (j > 0) ss << ","; - size_t idx = (data.write_index + config_.adc_history_size - num_samples + j) % - config_.adc_history_size; + size_t idx = (data.write_index + capacity - count + j) % capacity; ss << data.timestamps[idx]; } ss << "]"; From 958bbf7aa23402416ba974b98e4ce18f28cca707 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 26 Jan 2026 23:59:21 -0600 Subject: [PATCH 09/20] minor updates for good common config --- components/remote_debug/example/main/Kconfig.projbuild | 6 +++--- .../remote_debug/example/main/remote_debug_example.cpp | 5 +++++ components/remote_debug/example/sdkconfig.defaults | 5 +---- components/remote_debug/include/remote_debug.hpp | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/components/remote_debug/example/main/Kconfig.projbuild b/components/remote_debug/example/main/Kconfig.projbuild index 639578f6e..c3ca87368 100644 --- a/components/remote_debug/example/main/Kconfig.projbuild +++ b/components/remote_debug/example/main/Kconfig.projbuild @@ -275,7 +275,7 @@ menu "Remote Debug Example Configuration" config REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ int "ADC Sample Rate (Hz)" - default 100 + default 25 range 1 1000 depends on REMOTE_DEBUG_NUM_ADCS > 0 help @@ -283,7 +283,7 @@ menu "Remote Debug Example Configuration" config REMOTE_DEBUG_ADC_BUFFER_SIZE int "ADC Buffer Size" - default 500 + default 100 range 10 10000 depends on REMOTE_DEBUG_NUM_ADCS > 0 help @@ -301,7 +301,7 @@ menu "Remote Debug Example Configuration" config REMOTE_DEBUG_LOG_BUFFER_SIZE int "Log Buffer Size (bytes)" - default 8192 + default 2048 range 1024 65536 depends on REMOTE_DEBUG_ENABLE_LOGS help diff --git a/components/remote_debug/example/main/remote_debug_example.cpp b/components/remote_debug/example/main/remote_debug_example.cpp index f59d83817..64b60fc68 100644 --- a/components/remote_debug/example/main/remote_debug_example.cpp +++ b/components/remote_debug/example/main/remote_debug_example.cpp @@ -78,7 +78,9 @@ extern "C" void app_main(void) { #endif // Build ADC list from menuconfig + size_t task_stack_size = 4096; #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 1 + task_stack_size += 2048; int adc_sample_rate_hz = CONFIG_REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ; size_t adc_buffer_size = CONFIG_REMOTE_DEBUG_ADC_BUFFER_SIZE; #else @@ -102,6 +104,7 @@ extern "C" void app_main(void) { #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 4 adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_3), .label = CONFIG_REMOTE_DEBUG_ADC_3_LABEL}); + task_stack_size += 2048; #endif #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 5 adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_4), @@ -118,6 +121,7 @@ extern "C" void app_main(void) { #if CONFIG_REMOTE_DEBUG_NUM_ADCS >= 8 adc1_channels.push_back({.channel = static_cast(CONFIG_REMOTE_DEBUG_ADC_7), .label = CONFIG_REMOTE_DEBUG_ADC_7_LABEL}); + task_stack_size += 2048; #endif // Configure remote debug @@ -126,6 +130,7 @@ extern "C" void app_main(void) { .adc2_channels = {}, .server_port = static_cast(CONFIG_REMOTE_DEBUG_SERVER_PORT), .adc_sample_rate = std::chrono::milliseconds(1000 / adc_sample_rate_hz), .gpio_update_rate = std::chrono::milliseconds(100), .adc_history_size = adc_buffer_size, + .adc_batch_size = adc_buffer_size / 3, .task_priority = 5, .task_stack_size = task_stack_size, #if CONFIG_REMOTE_DEBUG_ENABLE_LOGS .enable_log_capture = true, .max_log_size = CONFIG_REMOTE_DEBUG_LOG_BUFFER_SIZE, #else diff --git a/components/remote_debug/example/sdkconfig.defaults b/components/remote_debug/example/sdkconfig.defaults index ab2f4d832..88a7400fe 100644 --- a/components/remote_debug/example/sdkconfig.defaults +++ b/components/remote_debug/example/sdkconfig.defaults @@ -26,7 +26,7 @@ CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y CONFIG_LITTLEFS_MMAP_PARTITION=y # Default settings -CONFIG_REMOTE_DEBUG_SERVER_PORT=8080 +CONFIG_REMOTE_DEBUG_DEVICE_NAME="Remote Debug Example" CONFIG_REMOTE_DEBUG_NUM_GPIOS=2 CONFIG_REMOTE_DEBUG_GPIO_0=17 CONFIG_REMOTE_DEBUG_GPIO_0_LABEL="LED" @@ -37,6 +37,3 @@ CONFIG_REMOTE_DEBUG_ADC_0=7 CONFIG_REMOTE_DEBUG_ADC_0_LABEL="Joy LX" CONFIG_REMOTE_DEBUG_ADC_1=8 CONFIG_REMOTE_DEBUG_ADC_1_LABEL="Joy LY" -CONFIG_REMOTE_DEBUG_ADC_SAMPLE_RATE_HZ=100 -CONFIG_REMOTE_DEBUG_ADC_BUFFER_SIZE=500 -CONFIG_REMOTE_DEBUG_ENABLE_LOGS=y diff --git a/components/remote_debug/include/remote_debug.hpp b/components/remote_debug/include/remote_debug.hpp index 783d32a91..fa534340f 100644 --- a/components/remote_debug/include/remote_debug.hpp +++ b/components/remote_debug/include/remote_debug.hpp @@ -64,7 +64,7 @@ class RemoteDebug : public BaseComponent { uint16_t server_port{8080}; ///< HTTP server port std::chrono::milliseconds adc_sample_rate{100}; ///< ADC sampling interval std::chrono::milliseconds gpio_update_rate{100}; ///< GPIO state update interval - size_t adc_history_size{1000}; ///< Number of ADC samples to keep + size_t adc_history_size{20}; ///< Number of ADC samples to keep size_t adc_batch_size{10}; ///< Number of ADC samples to send per update size_t task_priority{5}; ///< Priority for update tasks size_t task_stack_size{4096}; ///< Stack size for update tasks From 0f61fef224f0620525bf34906cc209dfda2c3e48 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 00:03:40 -0600 Subject: [PATCH 10/20] clean up component --- components/remote_debug/CMakeLists.txt | 2 +- components/remote_debug/idf_component.yml | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/components/remote_debug/CMakeLists.txt b/components/remote_debug/CMakeLists.txt index 90858ffb6..bd6b97c6b 100644 --- a/components/remote_debug/CMakeLists.txt +++ b/components/remote_debug/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES esp_http_server driver adc base_component task timer file_system + REQUIRES esp_http_server driver adc base_component file_system timer ) diff --git a/components/remote_debug/idf_component.yml b/components/remote_debug/idf_component.yml index 41a49af5d..8d38d0153 100644 --- a/components/remote_debug/idf_component.yml +++ b/components/remote_debug/idf_component.yml @@ -6,17 +6,19 @@ repository: "git://github.com/esp-cpp/espp.git" maintainers: - William Emfinger documentation: "https://esp-cpp.github.io/espp/remote_debug.html" +examples: + - path: example tags: - cpp - Debug - GPIO - ADC + - HTTP + - WebServer dependencies: idf: version: '>=5.0' - espp/base_component: - path: ../base_component - espp/oneshot_adc: - path: ../oneshot_adc - espp/timer: - path: ../timer + espp/adc: '>=1.0' + espp/base_component: '>=1.0' + espp/file_system: '>=1.0' + espp/timer: '>=1.0' From 8a1f1e17e5831cfbf9f77a2f91a6298d4a620479 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 00:05:37 -0600 Subject: [PATCH 11/20] update ci and doc --- .github/workflows/build.yml | 2 + .github/workflows/upload_components.yml | 1 + doc/Doxyfile | 2 + doc/en/remote_debug.rst | 7 ++ doc/en/remote_debug_example.md | 90 +++++++++++++++++++++++++ 5 files changed, 102 insertions(+) create mode 100644 doc/en/remote_debug.rst create mode 100644 doc/en/remote_debug_example.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1bb607c4..faf17f65e 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -165,6 +165,8 @@ jobs: target: esp32s3 - path: 'components/qwiicnes/example' target: esp32 + - path: 'components/remote_debug/example' + target: esp32s3 - path: 'components/rmt/example' target: esp32s3 - path: 'components/rtsp/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index f0d27c8e9..fae93e79a 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -103,6 +103,7 @@ jobs: components/qmi8658 components/qtpy components/qwiicnes + components/remote_debug components/rmt components/rtsp components/runqueue diff --git a/doc/Doxyfile b/doc/Doxyfile index 6c00a5180..2e2f77a9c 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -132,6 +132,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/pcf85063/example/main/pcf85063_example.cpp \ $(PROJECT_PATH)/components/pid/example/main/pid_example.cpp \ $(PROJECT_PATH)/components/provisioning/example/main/provisioning_example.cpp \ + $(PROJECT_PATH)/components/remote_debug/example/main/remote_debug_example.cpp \ $(PROJECT_PATH)/components/qtpy/example/main/qtpy_example.cpp \ $(PROJECT_PATH)/components/qmi8658/example/main/qmi8658_example.cpp \ $(PROJECT_PATH)/components/qwiicnes/example/main/qwiicnes_example.cpp \ @@ -297,6 +298,7 @@ INPUT = \ $(PROJECT_PATH)/components/pid/include/pid.hpp \ $(PROJECT_PATH)/components/ping/include/ping.hpp \ $(PROJECT_PATH)/components/provisioning/include/provisioning.hpp \ + $(PROJECT_PATH)/components/remote_debug/include/remote_debug.hpp \ $(PROJECT_PATH)/components/qmi8658/include/qmi8658.hpp \ $(PROJECT_PATH)/components/qmi8658/include/qmi8658_detail.hpp \ $(PROJECT_PATH)/components/qtpy/include/qtpy.hpp \ diff --git a/doc/en/remote_debug.rst b/doc/en/remote_debug.rst new file mode 100644 index 000000000..3f1e982a8 --- /dev/null +++ b/doc/en/remote_debug.rst @@ -0,0 +1,7 @@ +Remote Debug +============ + +.. doxygenclass:: espp::RemoteDebug + :members: + :protected-members: + :undoc-members: diff --git a/doc/en/remote_debug_example.md b/doc/en/remote_debug_example.md new file mode 100644 index 000000000..1d7e04f8b --- /dev/null +++ b/doc/en/remote_debug_example.md @@ -0,0 +1,90 @@ +# Remote Debug Example + +Example demonstrating the Remote Debug component for web-based GPIO control, ADC monitoring, and console log viewing. + +## How to use example + +### Hardware Required + +This example can run on any ESP32 development board. To use all features: +- Connect GPIO pins to test inputs/outputs +- Connect analog signals to ADC channels for monitoring +- Ensure WiFi connectivity for web interface access + +### Configure the project + +``` +idf.py menuconfig +``` + +Key configuration options: +- **WiFi Settings**: Configure SSID and password under "Example Configuration" +- **Debug Interface**: Set title, GPIO pins, and ADC channels to expose +- **Logging**: Enable stdout redirection and set buffer size +- **Performance**: Adjust sample rates, batch sizes, task priorities + +**Important**: To enable real-time log viewing, set `CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y` in your sdkconfig. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +[RemoteDebug Example/I][0.123]: Starting Remote Debug Example +[RemoteDebug Example/I][0.456]: Connecting to WiFi: MyWiFiNetwork +[WifiSta/I][1.234]: got ip: 192.168.1.100 +[RemoteDebug Example/I][1.235]: WiFi connected, IP: 192.168.1.100 +[RemoteDebug/I][1.345]: Remote Debug server started on http://192.168.1.100:8080 +[RemoteDebug Example/I][1.346]: Web interface available at: http://192.168.1.100:8080 +[RemoteDebug Example/I][2.000]: Test log message 1 +[RemoteDebug Example/I][3.000]: Test log message 2 +``` + +## Web Interface + +Navigate to the IP address shown in the console output (e.g., `http://192.168.1.100:8080`) to access: + +### GPIO Control +- View all configured GPIOs with custom labels +- Configure each pin as input or output +- Control output states with High/Low buttons +- Real-time state display for all pins + +### ADC Monitoring +- Live display of current voltage values +- Real-time plotting with configurable history +- Multiple channels displayed simultaneously +- Auto-updating graphs + +### Console Logs (if enabled) +- View stdout output remotely +- ANSI color code support for formatted logs +- Auto-scrolling to latest entries +- Configurable buffer size + +## Troubleshooting + +### Logs not updating in real-time +Ensure `CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y` is set in sdkconfig or menuconfig under Component config → LittleFS. + +### Web interface slow with multiple clients +- Increase `adc_batch_size` to reduce update frequency +- Reduce ADC sample rate if high precision isn't needed +- Adjust task priorities if competing with other high-priority tasks + +### GPIO states not updating +- Verify GPIO pins are not used by other peripherals +- Check that direction is set correctly (input vs output) +- Ensure GPIO numbers are valid for your ESP32 variant From 0825d093f444f6257706fff2340103dca1d16739 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 00:07:36 -0600 Subject: [PATCH 12/20] readme: update --- components/remote_debug/README.md | 92 ++++-------- components/remote_debug/example/README.md | 169 ++++++++-------------- 2 files changed, 92 insertions(+), 169 deletions(-) diff --git a/components/remote_debug/README.md b/components/remote_debug/README.md index 8d3f56eb2..cca4039dc 100644 --- a/components/remote_debug/README.md +++ b/components/remote_debug/README.md @@ -1,86 +1,58 @@ # Remote Debug -Web-based remote debugging interface providing GPIO control, real-time ADC monitoring, and optional console log viewing over HTTP. Uses espp::Timer for efficient, configurable periodic updates. +[![Badge](https://components.espressif.com/components/espp/remote_debug/badge.svg)](https://components.espressif.com/components/espp/remote_debug) + +Web-based remote debugging interface providing GPIO control, real-time ADC +monitoring, and optional console log viewing over HTTP. Uses `espp::Timer` for +efficient, configurable periodic updates. ## Features - **GPIO Control**: Configure pins as input/output, read states, control outputs via web interface -- **ADC Monitoring**: Real-time visualization of ADC channels with configurable sample rates and batching +- **ADC Monitoring**: Real-time visualization of ADC channels with configurable sample rates - **Console Log Viewer**: Optional stdout redirection to web-viewable log with ANSI color support -- **Efficient Updates**: Uses espp::Timer with configurable priority and stack size for optimal performance -- **Clean API**: RESTful JSON endpoints for programmatic access +- **Efficient Updates**: Uses `espp::Timer` for optimal performance with configurable priority +- **RESTful API**: JSON endpoints for programmatic access - **Responsive UI**: Modern web interface that works on desktop and mobile - **Multi-client Support**: Optimized for multiple concurrent clients through batched updates -## Performance Considerations +## Performance -The remote debug component has been optimized for efficiency: +The component has been optimized for efficiency: -- Uses `espp::Timer` instead of raw threads for precise, lightweight periodic updates -- Configurable task priority and stack size for both GPIO and ADC sampling +- Uses `espp::Timer` for precise, lightweight periodic updates +- Configurable task priority and stack size - Batched ADC data updates reduce HTTP overhead -- Single consolidated update endpoint minimizes request count -- Efficient JSON generation for minimal processing overhead - -For best performance with multiple clients: -- Increase `adc_batch_size` to reduce update frequency -- Adjust `adc_sample_rate` and `gpio_update_rate` based on actual needs -- Configure appropriate `task_priority` and `task_stack_size` for your application - -## Usage - -```cpp -#include "remote_debug.hpp" - -// Configure GPIOs to expose -std::vector gpios = { - {.pin = 2, .label = "LED"}, - {.pin = 4, .label = "Button"} -}; - -// Configure ADC channels to monitor -std::vector adc1_channels = { - {.channel = ADC1_CHANNEL_0, .atten = ADC_ATTEN_DB_12, .label = "Battery Voltage"} -}; - -// Create remote debug server -espp::RemoteDebug::Config config{ - .server_address = "0.0.0.0", - .server_port = 8080, - .title = "My Device Debug", - .gpios = gpios, - .adc1_channels = adc1_channels, - .adc_sample_rate = 100.0f, // Hz - .adc_buffer_size = 1000, - .enable_logging = true, // Enable console log viewer - .log_buffer_size = 4096, - .log_level = espp::Logger::Verbosity::INFO -}; - -espp::RemoteDebug debug(config); -debug.start(); - -// Access at http://:8080 -``` +- Ring buffer implementation for efficient data management +- Efficient JSON generation minimizes processing overhead + +## Dependencies + +- `espp::Timer` - Periodic task execution +- `espp::Adc` - ADC channel management +- `espp::FileSystem` - LittleFS for optional log storage +- ESP HTTP Server - Web interface hosting ## Console Logging -When `enable_logging` is true, the component redirects `stdout` to a file that can be viewed in the web interface. The log viewer supports ANSI color codes for styled output. +When `enable_logging` is enabled in the config, stdout is redirected to a file +viewable in the web interface. The log viewer supports ANSI color codes. -**Important**: For real-time log updates, you must enable LittleFS file flushing: +**Important**: For real-time log updates, enable LittleFS file flushing: ``` CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y ``` -Set this in your `sdkconfig.defaults` or via `idf.py menuconfig` → Component config → LittleFS. +Set this in your `sdkconfig.defaults` or via `idf.py menuconfig` → Component +config → LittleFS. Without this, logs only appear after the buffer fills. -Without this setting, logs will only appear after the file buffer is full or the file is closed. - -## API +## Example -See the [Remote Debug API documentation](https://esp-cpp.github.io/espp/html/classespp_1_1_remote_debug.html) for detailed information. +See the example in the `example/` folder for a complete demonstration with WiFi +connection, GPIO control, ADC monitoring, and console log viewing. -## Example +## External Resources -See the [remote_debug example](example/README.md) for a complete working example with WiFi connection, GPIO control, ADC monitoring, and console log viewing. +- [ESP-IDF HTTP Server Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/protocols/esp_http_server.html) +- [ADC Continuous Mode](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/adc_continuous.html) diff --git a/components/remote_debug/example/README.md b/components/remote_debug/example/README.md index c53a59661..f79b16a7a 100644 --- a/components/remote_debug/example/README.md +++ b/components/remote_debug/example/README.md @@ -1,146 +1,97 @@ # Remote Debug Example -Web-based remote debugging interface for GPIO control and real-time ADC monitoring. +This example demonstrates the `espp::RemoteDebug` component, providing a +web-based interface for GPIO control, real-time ADC monitoring, and console log +viewing. -## How to Use +## How to use example -### Hardware Setup +### Hardware Required -Connect GPIOs and ADC channels you want to monitor/control. Default pins: -- GPIOs: 2, 4, 16, 17 (configurable) -- ADCs: 36, 39 (configurable) +This example can run on any ESP32 development board. For testing: +- Connect LEDs or other peripherals / inputs to GPIO pins +- Connect analog sensors to ADC-capable pins -Connect LEDs, sensors, or other peripherals to these pins for testing. +### Configure the project -### Configure the Project - -```bash +``` idf.py menuconfig ``` Navigate to `Remote Debug Example Configuration`: -- Set WiFi SSID and password -- Configure server port (default: 8080) -- Set number of GPIOs to expose (1-10) -- Configure which GPIO pins to use and their labels -- Set number of ADC channels (0-8) -- Configure which ADC pins to monitor and their labels -- Set ADC sample rate and buffer size -- Enable/disable console log viewer -- Set log buffer size - -**Important for Console Logging**: If you enable the console log viewer, you must also enable: +- WiFi credentials (SSID and password) +- Server configuration (port, title) +- GPIO configuration (number of pins, pin numbers, labels) +- ADC configuration (channels, attenuation, labels, sample rate, buffer size) +- Console logging (enable/disable, buffer size) + +**Important**: If enabling console logging, you must also set: - Component config → LittleFS → `CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y` -This ensures logs appear in real-time on the web interface. Without this setting, logs will only show after the buffer fills or the file closes. +This ensures real-time log updates on the web interface. ### Build and Flash -```bash -idf.py build flash monitor -``` - -### Access the Interface - -1. Device connects to your WiFi network -2. Check serial monitor for IP address -3. Open browser to `http://:8080` -4. Use the web interface to control GPIOs and monitor ADCs +Build the project and flash it to the board, then run monitor tool to view serial output: -## Features +``` +idf.py -p PORT flash monitor +``` -### GPIO Control +(Replace PORT with the name of the serial port to use.) -- **Read**: Get current state of GPIO pins -- **Write**: Set GPIO pins HIGH or LOW -- **Toggle**: Flip GPIO state -- **Visual indicators**: Shows current state in real-time +(To exit the serial monitor, type ``Ctrl-]``.) -### ADC Monitoring +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. -- **Real-time plotting**: Live graph of ADC values -- **Multiple channels**: Monitor up to 8 channels simultaneously -- **Configurable sample rate**: 1-1000 Hz -- **Voltage display**: Shows current values in volts -- **Time-series data**: Scrolling graph with configurable buffer +### Access the Interface -### JSON API - -For programmatic access: -``` -GET /api/gpio - List all GPIOs and states -GET /api/gpio/ - Read specific GPIO -POST /api/gpio/ - Write GPIO (body: {"state": 1}) -GET /api/adc - Get current ADC values -GET /api/adc/data - Get ADC plot data (all samples) -``` +1. Device connects to configured WiFi network +2. Check serial monitor for assigned IP address +3. Open web browser to `http://:` (default port: 8080) +4. Use the interface to control GPIOs, monitor ADCs, and view console logs ## Example Output ``` I (380) Remote Debug Example: Starting Remote Debug Example -I (385) Remote Debug Example: Connecting to WiFi SSID: MyWiFi -I (2450) Remote Debug Example: Got IP: 192.168.1.105 -I (2451) Remote Debug Example: Connected to WiFi! IP: 192.168.1.105 +I (390) Remote Debug Example: Connecting to WiFi: MyNetwork +I (2450) Remote Debug Example: WiFi connected! IP: 192.168.1.105 I (2456) Remote Debug Example: Initialized 2 ADC channels -I (2461) Remote Debug Example: Remote Debug Server started! -I (2462) Remote Debug Example: Open browser to: http://192.168.1.105:8080 -I (2463) Remote Debug Example: GPIO pins available: 4 -I (2464) Remote Debug Example: ADC channels available: 2 +I (2461) Remote Debug Example: Remote Debug Server started +I (2462) Remote Debug Example: Web interface: http://192.168.1.105:8080 +I (2463) Remote Debug Example: GPIO pins: 4 | ADC channels: 2 ``` -## Use Cases - -### Development & Testing -- Toggle GPIOs to control relays, LEDs, motors -- Monitor sensor values in real-time -- Debug analog circuits -- Test peripheral connections - -### Remote Monitoring -- Monitor battery voltage -- Track temperature sensors -- Log environmental data -- Remote equipment status - -### Interactive Demos -- Control devices from web browser -- Live sensor visualization -- Educational demonstrations -- Prototyping and proof-of-concept - ## Web Interface Features -- **Clean, modern UI**: Responsive design for desktop and mobile -- **Real-time updates**: ADC plots update automatically -- **Color-coded states**: Visual feedback for GPIO states -- **Labeled pins**: Easy identification of each GPIO/ADC -- **Toggle buttons**: Quick GPIO control -- **Zoom/pan**: Interactive ADC plots - -## Customization +- **GPIO Control** + - Configure pins as input or output + - Read current states in real-time + - Set output pins HIGH or LOW + - Visual state indicators -### Adding Custom Controls +- **ADC Monitoring** + - Real-time plotting with automatic updates + - Multiple channels displayed simultaneously + - Voltage display (converted from raw values) + - Configurable sample rate and buffer size -Modify the HTML/JavaScript in `remote_debug.cpp` to add: -- Custom widgets -- Additional data visualization -- Multi-GPIO patterns -- PWM control -- I2C/SPI device control +- **Console Log Viewer** (when enabled) + - Live stdout output + - ANSI color code support + - Auto-scrolling display + - Configurable buffer size -### Integration +## API Endpoints -The component can be integrated into any application that needs remote debugging capabilities. Just initialize with your GPIO/ADC configuration and start the server. +Programmatic access via JSON REST API: -## Troubleshooting - -- **Can't connect**: Check WiFi credentials and verify IP address -- **No ADC readings**: Verify GPIO numbers support ADC on your ESP32 variant -- **GPIO not responding**: Check pin isn't used by other peripherals -- **Slow updates**: Reduce ADC sample rate or buffer size -- **Port conflict**: Change server port if 8080 is in use - -## Security Note - -This is a development/debugging tool. For production use, add authentication and use HTTPS. The current implementation has no access control. +``` +GET /data - Batched update (GPIO states + ADC data) +GET /gpio - List all GPIOs and current states +POST /gpio/direction - Set GPIO direction (input/output) +POST /gpio/set - Set GPIO output state +GET /adc - Get current ADC values +GET /logs - Get console log content (if enabled) +``` From fe4ba809a510bda90f929342e5c3be6c76be89d2 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 17:32:37 -0600 Subject: [PATCH 13/20] fix sa --- components/remote_debug/src/remote_debug.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index 1bc971448..f433ef41a 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -938,8 +938,12 @@ void RemoteDebug::cleanup_log_redirection() { // Restore original stdout if (original_stdout_) { - freopen("/dev/null", "w", stdout); // dummy call to reset stdout - *stdout = *original_stdout_; // restore the FILE structure + auto ret = freopen("/dev/null", "w", stdout); // dummy call to reset stdout + if (!ret) { + logger_.error("Failed to reset stdout (errno: {})", strerror(errno)); + return; + } + *stdout = *original_stdout_; // restore the FILE structure original_stdout_ = nullptr; } From 98389e8804ce8e434c8a570128de14336dea2929 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 17:37:38 -0600 Subject: [PATCH 14/20] readme: update --- components/remote_debug/README.md | 4 ++++ components/remote_debug/example/README.md | 18 +++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/components/remote_debug/README.md b/components/remote_debug/README.md index cca4039dc..891285a3d 100644 --- a/components/remote_debug/README.md +++ b/components/remote_debug/README.md @@ -6,6 +6,10 @@ Web-based remote debugging interface providing GPIO control, real-time ADC monitoring, and optional console log viewing over HTTP. Uses `espp::Timer` for efficient, configurable periodic updates. +https://github.com/user-attachments/assets/ee806c6c-0f6b-4dd0-a5b0-82b80410b5bc + +image + ## Features - **GPIO Control**: Configure pins as input/output, read states, control outputs via web interface diff --git a/components/remote_debug/example/README.md b/components/remote_debug/example/README.md index f79b16a7a..7b03cdae1 100644 --- a/components/remote_debug/example/README.md +++ b/components/remote_debug/example/README.md @@ -4,6 +4,10 @@ This example demonstrates the `espp::RemoteDebug` component, providing a web-based interface for GPIO control, real-time ADC monitoring, and console log viewing. +https://github.com/user-attachments/assets/ee806c6c-0f6b-4dd0-a5b0-82b80410b5bc + +image + ## How to use example ### Hardware Required @@ -53,15 +57,11 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui ## Example Output -``` -I (380) Remote Debug Example: Starting Remote Debug Example -I (390) Remote Debug Example: Connecting to WiFi: MyNetwork -I (2450) Remote Debug Example: WiFi connected! IP: 192.168.1.105 -I (2456) Remote Debug Example: Initialized 2 ADC channels -I (2461) Remote Debug Example: Remote Debug Server started -I (2462) Remote Debug Example: Web interface: http://192.168.1.105:8080 -I (2463) Remote Debug Example: GPIO pins: 4 | ADC channels: 2 -``` +CleanShot 2026-01-27 at 00 13 39@2x + +Screenshots: +CleanShot 2026-01-27 at 00 12 32@2x +CleanShot 2026-01-27 at 00 13 06@2x ## Web Interface Features From e0a1ecf99b97f6dcf60635d054a0689c30a5fb2f Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 22:35:21 -0600 Subject: [PATCH 15/20] address comments and improve behavior --- components/remote_debug/README.md | 13 +- .../example/main/remote_debug_example.cpp | 2 +- .../remote_debug/include/remote_debug.hpp | 11 +- components/remote_debug/src/remote_debug.cpp | 264 ++++++++++++++---- 4 files changed, 227 insertions(+), 63 deletions(-) diff --git a/components/remote_debug/README.md b/components/remote_debug/README.md index 891285a3d..954d83d5a 100644 --- a/components/remote_debug/README.md +++ b/components/remote_debug/README.md @@ -39,7 +39,7 @@ The component has been optimized for efficiency: ## Console Logging -When `enable_logging` is enabled in the config, stdout is redirected to a file +When `enable_log_capture` is enabled in the config, stdout is redirected to a file viewable in the web interface. The log viewer supports ANSI color codes. **Important**: For real-time log updates, enable LittleFS file flushing: @@ -48,6 +48,17 @@ viewable in the web interface. The log viewer supports ANSI color codes. CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y ``` +## API Endpoints + +The component exposes the following HTTP endpoints: + +- `GET /` - Main web interface (HTML page) +- `GET /api/gpio/get` - Get current GPIO states (JSON) +- `POST /api/gpio/set` - Set GPIO output level (params: pin, level) +- `POST /api/gpio/config` - Configure GPIO direction (params: pin, mode) +- `GET /api/adc/data` - Get ADC channel data with history (JSON) +- `GET /api/logs` - Get console log contents (if logging enabled) + Set this in your `sdkconfig.defaults` or via `idf.py menuconfig` → Component config → LittleFS. Without this, logs only appear after the buffer fills. diff --git a/components/remote_debug/example/main/remote_debug_example.cpp b/components/remote_debug/example/main/remote_debug_example.cpp index 64b60fc68..746eb0951 100644 --- a/components/remote_debug/example/main/remote_debug_example.cpp +++ b/components/remote_debug/example/main/remote_debug_example.cpp @@ -130,7 +130,7 @@ extern "C" void app_main(void) { .adc2_channels = {}, .server_port = static_cast(CONFIG_REMOTE_DEBUG_SERVER_PORT), .adc_sample_rate = std::chrono::milliseconds(1000 / adc_sample_rate_hz), .gpio_update_rate = std::chrono::milliseconds(100), .adc_history_size = adc_buffer_size, - .adc_batch_size = adc_buffer_size / 3, .task_priority = 5, .task_stack_size = task_stack_size, + .task_priority = 5, .task_stack_size = task_stack_size, #if CONFIG_REMOTE_DEBUG_ENABLE_LOGS .enable_log_capture = true, .max_log_size = CONFIG_REMOTE_DEBUG_LOG_BUFFER_SIZE, #else diff --git a/components/remote_debug/include/remote_debug.hpp b/components/remote_debug/include/remote_debug.hpp index fa534340f..366e57409 100644 --- a/components/remote_debug/include/remote_debug.hpp +++ b/components/remote_debug/include/remote_debug.hpp @@ -26,7 +26,6 @@ namespace espp { * Features: * - GPIO control (set high/low, read state, configure mode) * - ADC value reading and real-time plotting - * - WebSocket support for live data streaming * - Configurable sampling rates * - Mobile-friendly responsive interface * @@ -65,7 +64,6 @@ class RemoteDebug : public BaseComponent { std::chrono::milliseconds adc_sample_rate{100}; ///< ADC sampling interval std::chrono::milliseconds gpio_update_rate{100}; ///< GPIO state update interval size_t adc_history_size{20}; ///< Number of ADC samples to keep - size_t adc_batch_size{10}; ///< Number of ADC samples to send per update size_t task_priority{5}; ///< Priority for update tasks size_t task_stack_size{4096}; ///< Stack size for update tasks bool enable_log_capture{false}; ///< Enable stdout redirection to file @@ -140,12 +138,15 @@ class RemoteDebug : public BaseComponent { static esp_err_t gpio_config_handler(httpd_req_t *req); static esp_err_t adc_data_handler(httpd_req_t *req); static esp_err_t logs_handler(httpd_req_t *req); + static esp_err_t start_log_handler(httpd_req_t *req); + static esp_err_t stop_log_handler(httpd_req_t *req); std::string generate_html() const; std::string get_gpio_state_json() const; std::string get_adc_data_json() const; std::string get_logs() const; void setup_log_redirection(); + void stop_log_redirection(); void cleanup_log_redirection(); Config config_; @@ -156,7 +157,7 @@ class RemoteDebug : public BaseComponent { std::unordered_map gpio_map_; std::unordered_map gpio_state_; // Cached GPIO states - std::mutex gpio_mutex_; + mutable std::mutex gpio_mutex_; // ADC data storage - ring buffer implementation struct AdcData { @@ -169,7 +170,7 @@ class RemoteDebug : public BaseComponent { }; std::vector adc1_data_; std::vector adc2_data_; - std::mutex adc_mutex_; + mutable std::mutex adc_mutex_; std::atomic is_active_{false}; std::atomic sampling_active_{false}; @@ -178,7 +179,7 @@ class RemoteDebug : public BaseComponent { // Log redirection FILE *log_file_{nullptr}; - FILE *original_stdout_{nullptr}; + mutable std::mutex log_mutex_; }; } // namespace espp diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index f433ef41a..006b33ef0 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -5,11 +5,80 @@ #include #include #include +#include #include "file_system.hpp" using namespace espp; +namespace { +// Helper function to escape JSON strings +std::string json_escape(const std::string &str) { + std::string escaped; + escaped.reserve(str.size()); + for (char c : str) { + switch (c) { + case '\"': + escaped += "\\\""; + break; + case '\\': + escaped += "\\\\"; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + case '\b': + escaped += "\\b"; + break; + case '\f': + escaped += "\\f"; + break; + default: + escaped += c; + } + } + return escaped; +} + +// Helper to read full HTTP request body +bool read_request_body(httpd_req_t *req, std::string &buffer, size_t max_size = 4096) { + size_t content_len = req->content_len; + + if (content_len == 0) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Empty request body"); + return false; + } + + if (content_len > max_size) { + httpd_resp_send_err(req, HTTPD_413_CONTENT_TOO_LARGE, "Request too large"); + return false; + } + + buffer.resize(content_len); + size_t received = 0; + + while (received < content_len) { + int ret = httpd_req_recv(req, &buffer[received], content_len - received); + if (ret < 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + continue; + } + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read request"); + return false; + } + received += ret; + } + + return true; +} +} // anonymous namespace + RemoteDebug::RemoteDebug(const Config &config) : BaseComponent("RemoteDebug", config.log_level) { init(config); @@ -20,23 +89,34 @@ RemoteDebug::~RemoteDebug() { stop(); } void RemoteDebug::init(const Config &config) { config_ = config; - // Initialize GPIO - default to input mode + // Initialize GPIO with configured mode for (const auto &gpio : config_.gpios) { + gpio_mode_t init_mode = gpio.mode; + + // Promote OUTPUT to INPUT_OUTPUT for bidirectional capability + if (init_mode == GPIO_MODE_OUTPUT) { + init_mode = GPIO_MODE_INPUT_OUTPUT; + logger_.debug("Promoting GPIO {} from OUTPUT to INPUT_OUTPUT", gpio.pin); + } else if (init_mode == GPIO_MODE_OUTPUT_OD) { + init_mode = GPIO_MODE_INPUT_OUTPUT_OD; + logger_.debug("Promoting GPIO {} from OUTPUT_OD to INPUT_OUTPUT_OD", gpio.pin); + } + gpio_config_t io_conf = {}; io_conf.pin_bit_mask = (1ULL << gpio.pin); - io_conf.mode = GPIO_MODE_INPUT; // Default to input + io_conf.mode = init_mode; io_conf.pull_up_en = GPIO_PULLUP_DISABLE; io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; io_conf.intr_type = GPIO_INTR_DISABLE; gpio_config(&io_conf); - // Store the GPIO config with input mode + // Store the GPIO config GpioConfig gpio_copy = gpio; - gpio_copy.mode = GPIO_MODE_INPUT; + gpio_copy.mode = init_mode; gpio_map_[gpio.pin] = gpio_copy; gpio_state_[gpio.pin] = gpio_get_level(gpio.pin); - logger_.debug("Configured GPIO {} as input, initial state: {}", gpio.pin, + logger_.debug("Configured GPIO {} as mode {}, initial state: {}", gpio.pin, init_mode, gpio_state_[gpio.pin]); } @@ -137,6 +217,17 @@ bool RemoteDebug::start() { gpio_timer_->start(); } + // Register log control endpoints + httpd_uri_t start_log_uri = {.uri = "/api/logs/start", + .method = HTTP_POST, + .handler = start_log_handler, + .user_ctx = this}; + httpd_register_uri_handler(server_, &start_log_uri); + + httpd_uri_t stop_log_uri = { + .uri = "/api/logs/stop", .method = HTTP_POST, .handler = stop_log_handler, .user_ctx = this}; + httpd_register_uri_handler(server_, &stop_log_uri); + // Setup log redirection if enabled if (config_.enable_log_capture) { setup_log_redirection(); @@ -331,52 +422,57 @@ esp_err_t RemoteDebug::gpio_get_handler(httpd_req_t *req) { esp_err_t RemoteDebug::gpio_set_handler(httpd_req_t *req) { auto *self = static_cast(req->user_ctx); - // Parse POST data - char buf[100]; - int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (ret <= 0) { - httpd_resp_send_500(req); + // Read request body properly + std::string body; + if (!read_request_body(req, body)) { return ESP_FAIL; } - buf[ret] = '\0'; // Parse pin=X&level=Y int pin = -1, level = -1; - sscanf(buf, "pin=%d&level=%d", &pin, &level); + sscanf(body.c_str(), "pin=%d&level=%d", &pin, &level); + + if (pin < 0 || (level != 0 && level != 1)) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid pin or level"); + return ESP_FAIL; + } - if (pin >= 0 && (level == 0 || level == 1)) { - self->set_gpio(static_cast(pin), level); - httpd_resp_send(req, "OK", 2); - } else { - httpd_resp_send_500(req); + // Attempt to set GPIO + if (!self->set_gpio(static_cast(pin), level)) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "GPIO not configured or not an output"); + return ESP_FAIL; } + httpd_resp_send(req, "OK", 2); return ESP_OK; } esp_err_t RemoteDebug::gpio_config_handler(httpd_req_t *req) { auto *self = static_cast(req->user_ctx); - // Parse POST data - char buf[100]; - int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); - if (ret <= 0) { - httpd_resp_send_500(req); + // Read request body properly + std::string body; + if (!read_request_body(req, body)) { return ESP_FAIL; } - buf[ret] = '\0'; // Parse pin=X&mode=Y int pin = -1, mode = -1; - sscanf(buf, "pin=%d&mode=%d", &pin, &mode); + sscanf(body.c_str(), "pin=%d&mode=%d", &pin, &mode); - if (pin >= 0 && mode >= 0 && mode <= 6) { - self->configure_gpio(static_cast(pin), static_cast(mode)); - httpd_resp_send(req, "OK", 2); - } else { - httpd_resp_send_500(req); + // Validate mode (ESP-IDF supports modes 0-5) + if (pin < 0 || mode < 0 || mode > 5) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid pin or mode"); + return ESP_FAIL; } + // Attempt to configure GPIO + if (!self->configure_gpio(static_cast(pin), static_cast(mode))) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "GPIO configuration failed"); + return ESP_FAIL; + } + + httpd_resp_send(req, "OK", 2); return ESP_OK; } @@ -486,6 +582,10 @@ button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0. if (config_.enable_log_capture) { ss << R"(

Console Logs

+
+ + +
Loading logs...
@@ -508,6 +608,20 @@ function setMode(pin, mode) { .catch(err => console.error('Failed to set mode:', err)); } +function startLogs() { + fetch('/api/logs/start', {method: 'POST'}) + .then(r => r.text()) + .then(() => console.log('Logging started')) + .catch(err => console.error('Failed to start logs:', err)); +} + +function stopLogs() { + fetch('/api/logs/stop', {method: 'POST'}) + .then(r => r.text()) + .then(() => console.log('Logging stopped')) + .catch(err => console.error('Failed to stop logs:', err)); +} + function setGpio(pin, level) { fetch('/api/gpio/set', {method: 'POST', body: 'pin=' + pin + '&level=' + level}) .then(r => r.text()) @@ -782,7 +896,7 @@ std::string RemoteDebug::get_gpio_state_json() const { } std::string RemoteDebug::get_adc_data_json() const { - std::lock_guard lock(const_cast(adc_mutex_)); + std::lock_guard lock(adc_mutex_); std::stringstream ss; ss << "{"; @@ -794,6 +908,9 @@ std::string RemoteDebug::get_adc_data_json() const { if (!first) ss << ","; + // Use unique key based on unit and channel (not label which can be non-unique) + std::string key = fmt::format("ADC1_{}", static_cast(data.channel)); + // Get the most recent value float voltage = 0.0f; if (data.count > 0) { @@ -801,10 +918,10 @@ std::string RemoteDebug::get_adc_data_json() const { voltage = data.values[latest_idx]; } - ss << "\"" << data.label << "\":{"; + ss << "\"" << key << "\":{"; ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage << ","; ss << "\"current\":" << voltage << ","; - ss << "\"label\":\"" << data.label << "\","; + ss << "\"label\":\"" << json_escape(data.label) << "\","; ss << "\"unit\":1,"; ss << "\"channel\":" << static_cast(data.channel) << ","; @@ -843,6 +960,9 @@ std::string RemoteDebug::get_adc_data_json() const { if (!first) ss << ","; + // Use unique key based on unit and channel (not label which can be non-unique) + std::string key = fmt::format("ADC2_{}", static_cast(data.channel)); + // Get the most recent value float voltage = 0.0f; if (data.count > 0) { @@ -850,10 +970,10 @@ std::string RemoteDebug::get_adc_data_json() const { voltage = data.values[latest_idx]; } - ss << "\"" << data.label << "\":{"; + ss << "\"" << key << "\":{"; ss << "\"voltage\":" << std::fixed << std::setprecision(3) << voltage << ","; ss << "\"current\":" << voltage << ","; - ss << "\"label\":\"" << data.label << "\","; + ss << "\"label\":\"" << json_escape(data.label) << "\","; ss << "\"unit\":2,"; ss << "\"channel\":" << static_cast(data.channel) << ","; @@ -896,6 +1016,11 @@ void RemoteDebug::setup_log_redirection() { return; } + if (log_file_) { + logger_.warn("Stdout already redirected to log file"); + return; + } + #ifndef CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE logger_.warn("**************************************************************"); logger_.warn("WARNING: CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not enabled!"); @@ -911,47 +1036,48 @@ void RemoteDebug::setup_log_redirection() { logger_.info("Attempting to redirect stdout to: {}", log_path); logger_.info("Log buffer size: {} bytes", config_.max_log_size); - // Save original stdout before redirecting - original_stdout_ = stdout; - // Redirect stdout using freopen - log_file_ = freopen(log_path.string().c_str(), "w", stdout); - if (!log_file_) { + FILE *result = freopen(log_path.string().c_str(), "w", stdout); + if (!result) { logger_.error("Failed to redirect stdout to log file: {} (errno: {})", log_path, strerror(errno)); return; } + log_file_ = result; + // remove file buffer so logs are written immediately setvbuf(log_file_, nullptr, _IONBF, 0); - // Write test messages directly to stdout (which is now the file) + // Write test message directly to stdout (which is now the file) fmt::print("=== Log capture started at {} s ===\n", Logger::get_time()); fmt::print("Remote Debug log file: {}\n", log_path); fflush(stdout); } -void RemoteDebug::cleanup_log_redirection() { - if (log_file_) { - printf("=== Log capture stopped ===\n"); - fflush(log_file_); +void RemoteDebug::stop_log_redirection() { + if (!log_file_) { + logger_.warn("Stdout not redirected to log file"); + return; + } - // Restore original stdout - if (original_stdout_) { - auto ret = freopen("/dev/null", "w", stdout); // dummy call to reset stdout - if (!ret) { - logger_.error("Failed to reset stdout (errno: {})", strerror(errno)); - return; - } - *stdout = *original_stdout_; // restore the FILE structure - original_stdout_ = nullptr; - } + // Write final message + fmt::print("=== Log capture stopped at {} s ===\n", Logger::get_time()); + fflush(stdout); - // Don't close log_file_ since it's the same as stdout after freopen - log_file_ = nullptr; + // Redirect stdout back to console + FILE *result = freopen("/dev/console", "w", stdout); + if (!result) { + // Can't log this error since stdout is broken + return; } + + log_file_ = nullptr; + logger_.info("Successfully restored stdout to console"); } +void RemoteDebug::cleanup_log_redirection() { stop_log_redirection(); } + std::string RemoteDebug::get_logs() const { if (!config_.enable_log_capture) { return "Log capture not enabled"; @@ -1009,3 +1135,29 @@ esp_err_t RemoteDebug::logs_handler(httpd_req_t *req) { httpd_resp_send(req, logs.c_str(), logs.length()); return ESP_OK; } + +esp_err_t RemoteDebug::start_log_handler(httpd_req_t *req) { + RemoteDebug *self = static_cast(req->user_ctx); + + if (!self->config_.enable_log_capture) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Log capture not enabled"); + return ESP_FAIL; + } + + self->setup_log_redirection(); + httpd_resp_sendstr(req, "OK"); + return ESP_OK; +} + +esp_err_t RemoteDebug::stop_log_handler(httpd_req_t *req) { + RemoteDebug *self = static_cast(req->user_ctx); + + if (!self->config_.enable_log_capture) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Log capture not enabled"); + return ESP_FAIL; + } + + self->stop_log_redirection(); + httpd_resp_sendstr(req, "OK"); + return ESP_OK; +} From dd62530a063ad300673a079145d4daa602d5d5f6 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 22:42:34 -0600 Subject: [PATCH 16/20] fix reamdes --- components/remote_debug/README.md | 13 ++++++++----- components/remote_debug/example/README.md | 15 +++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/components/remote_debug/README.md b/components/remote_debug/README.md index 954d83d5a..0903f283a 100644 --- a/components/remote_debug/README.md +++ b/components/remote_debug/README.md @@ -53,11 +53,14 @@ CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y The component exposes the following HTTP endpoints: - `GET /` - Main web interface (HTML page) -- `GET /api/gpio/get` - Get current GPIO states (JSON) -- `POST /api/gpio/set` - Set GPIO output level (params: pin, level) -- `POST /api/gpio/config` - Configure GPIO direction (params: pin, mode) -- `GET /api/adc/data` - Get ADC channel data with history (JSON) -- `GET /api/logs` - Get console log contents (if logging enabled) +- `GET /api/gpio/get` - Get all GPIO states and configurations (JSON) +- `POST /api/gpio/set` - Set GPIO output state (JSON: `{"pin": N, "value": 0|1}`) +- `POST /api/gpio/config` - Configure GPIO direction (JSON: `{"pin": N, "mode": 1|3}`) + - Mode values: `1` = INPUT, `3` = INPUT_OUTPUT (OUTPUT is promoted to INPUT_OUTPUT for safety) +- `GET /api/adc/data` - Get ADC readings and plot data (JSON with ring buffer indices) +- `GET /api/logs` - Get console log contents (text/plain with ANSI colors) +- `POST /api/logs/start` - Start log capture (redirects stdout to file) +- `POST /api/logs/stop` - Stop log capture (restores stdout to /dev/console) Set this in your `sdkconfig.defaults` or via `idf.py menuconfig` → Component config → LittleFS. Without this, logs only appear after the buffer fills. diff --git a/components/remote_debug/example/README.md b/components/remote_debug/example/README.md index 7b03cdae1..06d71309e 100644 --- a/components/remote_debug/example/README.md +++ b/components/remote_debug/example/README.md @@ -88,10 +88,13 @@ Screenshots: Programmatic access via JSON REST API: ``` -GET /data - Batched update (GPIO states + ADC data) -GET /gpio - List all GPIOs and current states -POST /gpio/direction - Set GPIO direction (input/output) -POST /gpio/set - Set GPIO output state -GET /adc - Get current ADC values -GET /logs - Get console log content (if enabled) +GET /api/gpio/get - Get all GPIO states and configurations +POST /api/gpio/set - Set GPIO output state (JSON: {"pin": N, "value": 0|1}) +POST /api/gpio/config - Configure GPIO direction (JSON: {"pin": N, "mode": 1|3}) +GET /api/adc/data - Get ADC readings and plot data +POST /api/logs/start - Start log capture (redirects stdout to file) +POST /api/logs/stop - Stop log capture (restores stdout to /dev/console) +GET /api/logs - Get console log content (text/plain with ANSI colors) ``` + +Note: GPIO mode values are `1` for INPUT and `3` for INPUT_OUTPUT. OUTPUT mode (2) is automatically promoted to INPUT_OUTPUT for safety. From 050afeac757c24e7cb4df84e1c2ab2efdd731e3c Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 22:46:02 -0600 Subject: [PATCH 17/20] fix doc --- doc/Doxyfile | 4 ++-- doc/en/remote_debug.rst | 29 +++++++++++++++++++++++------ doc/en/remote_debug_example.md | 30 +----------------------------- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/doc/Doxyfile b/doc/Doxyfile index 2e2f77a9c..44c99e35f 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -132,10 +132,10 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/pcf85063/example/main/pcf85063_example.cpp \ $(PROJECT_PATH)/components/pid/example/main/pid_example.cpp \ $(PROJECT_PATH)/components/provisioning/example/main/provisioning_example.cpp \ - $(PROJECT_PATH)/components/remote_debug/example/main/remote_debug_example.cpp \ $(PROJECT_PATH)/components/qtpy/example/main/qtpy_example.cpp \ $(PROJECT_PATH)/components/qmi8658/example/main/qmi8658_example.cpp \ $(PROJECT_PATH)/components/qwiicnes/example/main/qwiicnes_example.cpp \ + $(PROJECT_PATH)/components/remote_debug/example/main/remote_debug_example.cpp \ $(PROJECT_PATH)/components/rmt/example/main/rmt_example.cpp \ $(PROJECT_PATH)/components/rtsp/example/main/rtsp_example.cpp \ $(PROJECT_PATH)/components/ping/example/main/ping_example.cpp \ @@ -298,11 +298,11 @@ INPUT = \ $(PROJECT_PATH)/components/pid/include/pid.hpp \ $(PROJECT_PATH)/components/ping/include/ping.hpp \ $(PROJECT_PATH)/components/provisioning/include/provisioning.hpp \ - $(PROJECT_PATH)/components/remote_debug/include/remote_debug.hpp \ $(PROJECT_PATH)/components/qmi8658/include/qmi8658.hpp \ $(PROJECT_PATH)/components/qmi8658/include/qmi8658_detail.hpp \ $(PROJECT_PATH)/components/qtpy/include/qtpy.hpp \ $(PROJECT_PATH)/components/qwiicnes/include/qwiicnes.hpp \ + $(PROJECT_PATH)/components/remote_debug/include/remote_debug.hpp \ $(PROJECT_PATH)/components/rmt/include/rmt.hpp \ $(PROJECT_PATH)/components/rmt/include/rmt_encoder.hpp \ $(PROJECT_PATH)/components/rtsp/include/rtsp_client.hpp \ diff --git a/doc/en/remote_debug.rst b/doc/en/remote_debug.rst index 3f1e982a8..101980a51 100644 --- a/doc/en/remote_debug.rst +++ b/doc/en/remote_debug.rst @@ -1,7 +1,24 @@ -Remote Debug -============ +Remote Debug APIs +***************** -.. doxygenclass:: espp::RemoteDebug - :members: - :protected-members: - :undoc-members: +The `RemoteDebug` class provides a web-based interface for remote debugging and monitoring of ESP32 devices. It offers: + +* **GPIO Control**: Configure pins as input/output and control their states remotely +* **ADC Monitoring**: Real-time voltage monitoring with live graphs and configurable sampling rates +* **Console Log Viewing**: Capture and display stdout output with ANSI color support +* **Multi-Client Support**: Efficient batched updates to support multiple simultaneous web clients + +The component uses the espp WiFi, ADC, and FileSystem components for functionality, and provides a responsive web interface accessible from any browser on the local network. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + remote_debug_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/remote_debug.inc diff --git a/doc/en/remote_debug_example.md b/doc/en/remote_debug_example.md index 1d7e04f8b..a93722839 100644 --- a/doc/en/remote_debug_example.md +++ b/doc/en/remote_debug_example.md @@ -1,33 +1,5 @@ -# Remote Debug Example - -Example demonstrating the Remote Debug component for web-based GPIO control, ADC monitoring, and console log viewing. - -## How to use example - -### Hardware Required - -This example can run on any ESP32 development board. To use all features: -- Connect GPIO pins to test inputs/outputs -- Connect analog signals to ADC channels for monitoring -- Ensure WiFi connectivity for web interface access - -### Configure the project - +```{include} ../../components/remote_debug/example/README.md ``` -idf.py menuconfig -``` - -Key configuration options: -- **WiFi Settings**: Configure SSID and password under "Example Configuration" -- **Debug Interface**: Set title, GPIO pins, and ADC channels to expose -- **Logging**: Enable stdout redirection and set buffer size -- **Performance**: Adjust sample rates, batch sizes, task priorities - -**Important**: To enable real-time log viewing, set `CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y` in your sdkconfig. - -### Build and Flash - -Build the project and flash it to the board, then run monitor tool to view serial output: ``` idf.py -p PORT flash monitor From 02b7a83854965a239f71c0012ba13c6948c19033 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 22:49:12 -0600 Subject: [PATCH 18/20] fix sa --- components/remote_debug/src/remote_debug.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index 006b33ef0..db505821f 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -1074,7 +1074,10 @@ void RemoteDebug::stop_log_redirection() { log_file_ = nullptr; logger_.info("Successfully restored stdout to console"); -} + // we have to suppress the resource leak warning here because freopen returns + // a new FILE* that we don't own and shouldn't fclose since this is just a + // virtual file system +} // cppcheck-suppress resourceLeak void RemoteDebug::cleanup_log_redirection() { stop_log_redirection(); } From 8e911b3b9bc176afe5f8debe1f46dfbad68d54ec Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 22:51:29 -0600 Subject: [PATCH 19/20] comment update --- components/remote_debug/src/remote_debug.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/remote_debug/src/remote_debug.cpp b/components/remote_debug/src/remote_debug.cpp index db505821f..721b8fbf3 100644 --- a/components/remote_debug/src/remote_debug.cpp +++ b/components/remote_debug/src/remote_debug.cpp @@ -1076,7 +1076,9 @@ void RemoteDebug::stop_log_redirection() { logger_.info("Successfully restored stdout to console"); // we have to suppress the resource leak warning here because freopen returns // a new FILE* that we don't own and shouldn't fclose since this is just a - // virtual file system + // virtual file system. If we called fclose here, it would close stdout, which + // would then lead to no console output and potential crashes next time we + // tried to redirect stdout to file. } // cppcheck-suppress resourceLeak void RemoteDebug::cleanup_log_redirection() { stop_log_redirection(); } From 9cc5e98f59964235f7caef348943a9ce6f1c93f6 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 27 Jan 2026 22:55:59 -0600 Subject: [PATCH 20/20] final cleanup --- doc/en/remote_debug_example.md | 60 ---------------------------------- 1 file changed, 60 deletions(-) diff --git a/doc/en/remote_debug_example.md b/doc/en/remote_debug_example.md index a93722839..3e3e04896 100644 --- a/doc/en/remote_debug_example.md +++ b/doc/en/remote_debug_example.md @@ -1,62 +1,2 @@ ```{include} ../../components/remote_debug/example/README.md ``` - -``` -idf.py -p PORT flash monitor -``` - -(Replace PORT with the name of the serial port to use.) - -(To exit the serial monitor, type ``Ctrl-]``.) - -See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. - -## Example Output - -``` -[RemoteDebug Example/I][0.123]: Starting Remote Debug Example -[RemoteDebug Example/I][0.456]: Connecting to WiFi: MyWiFiNetwork -[WifiSta/I][1.234]: got ip: 192.168.1.100 -[RemoteDebug Example/I][1.235]: WiFi connected, IP: 192.168.1.100 -[RemoteDebug/I][1.345]: Remote Debug server started on http://192.168.1.100:8080 -[RemoteDebug Example/I][1.346]: Web interface available at: http://192.168.1.100:8080 -[RemoteDebug Example/I][2.000]: Test log message 1 -[RemoteDebug Example/I][3.000]: Test log message 2 -``` - -## Web Interface - -Navigate to the IP address shown in the console output (e.g., `http://192.168.1.100:8080`) to access: - -### GPIO Control -- View all configured GPIOs with custom labels -- Configure each pin as input or output -- Control output states with High/Low buttons -- Real-time state display for all pins - -### ADC Monitoring -- Live display of current voltage values -- Real-time plotting with configurable history -- Multiple channels displayed simultaneously -- Auto-updating graphs - -### Console Logs (if enabled) -- View stdout output remotely -- ANSI color code support for formatted logs -- Auto-scrolling to latest entries -- Configurable buffer size - -## Troubleshooting - -### Logs not updating in real-time -Ensure `CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE=y` is set in sdkconfig or menuconfig under Component config → LittleFS. - -### Web interface slow with multiple clients -- Increase `adc_batch_size` to reduce update frequency -- Reduce ADC sample rate if high precision isn't needed -- Adjust task priorities if competing with other high-priority tasks - -### GPIO states not updating -- Verify GPIO pins are not used by other peripherals -- Check that direction is set correctly (input vs output) -- Ensure GPIO numbers are valid for your ESP32 variant