diff --git a/.github/workflows/ArduinoBuild.yml b/.github/workflows/ArduinoBuild.yml index 9ad9a0eba..018595a63 100644 --- a/.github/workflows/ArduinoBuild.yml +++ b/.github/workflows/ArduinoBuild.yml @@ -16,8 +16,8 @@ on: - '**ArduinoBuild.yml' pull_request: jobs: - build: - name: Build ESP32 examples + build-stm32: + name: Build Arduino STM32 examples runs-on: ubuntu-latest steps: - name: Checkout @@ -29,21 +29,55 @@ jobs: $GITHUB_WORKSPACE/arduino/libify.sh $HOME/Arduino/libraries/OpenMRNLite $GITHUB_WORKSPACE -f -l rm -f $GITHUB_WORKSPACE/arduino/examples/Stm*/build_opt.h - - name: Compile all STM32 examples - uses: ArminJo/arduino-test-compile@v3.0.0 + - name: Compile STM32 examples + uses: ArminJo/arduino-test-compile@v3 with: platform-url: https://raw.githubusercontent.com/stm32duino/BoardManagerFiles/master/STM32/package_stm_index.json arduino-board-fqbn: STM32:stm32:Nucleo_144:pnum=NUCLEO_F767ZI,upload_method=MassStorage,xserial=generic,usb=CDCgen,xusb=FS,opt=osstd,rtlib=nano sketch-names: Stm32*.ino build-properties: '{ "All": "-DHAL_CAN_MODULE_ENABLED" }' debug-compile: true - - - name: Compile all ESP32 examples - uses: ArminJo/arduino-test-compile@v3.0.0 + + build-esp32: + name: Build Arduino ${{ matrix.target }} examples + runs-on: ubuntu-latest + strategy: + max-parallel: 2 + matrix: + target: [esp32, esp32c3, esp32s2] + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Generate OpenMRNLite library + run: | + mkdir --parents $HOME/Arduino/libraries/OpenMRNLite + $GITHUB_WORKSPACE/arduino/libify.sh $HOME/Arduino/libraries/OpenMRNLite $GITHUB_WORKSPACE -f -l + rm -f $GITHUB_WORKSPACE/arduino/examples/Stm*/build_opt.h + + - name: Compile ESP32 examples + uses: ArminJo/arduino-test-compile@v3 with: platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json arduino-board-fqbn: esp32:esp32:node32s - sketch-names: ESP*.ino + sketch-names: ESP32CanLoadTest.ino,ESP32IOBoard.ino,ESP32SerialBridge.ino,ESP32WifiCanBridge.ino debug-compile: true + if: ${{ matrix.target == 'esp32' }} - + - name: Compile ESP32-C3 examples + uses: ArminJo/arduino-test-compile@v3 + with: + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + arduino-board-fqbn: esp32:esp32:esp32c3 + sketch-names: ESP32C3CanLoadTest.ino,ESP32C3IOBoard.ino + debug-compile: true + if: ${{ matrix.target == 'esp32c3' }} + + - name: Compile ESP32-S2 examples + uses: ArminJo/arduino-test-compile@v3 + with: + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json + arduino-board-fqbn: esp32:esp32:esp32s2 + sketch-names: ESP32S2CanLoadTest.ino,ESP32S2IOBoard.ino + debug-compile: true + if: ${{ matrix.target == 'esp32s2' }} \ No newline at end of file diff --git a/arduino/CDIXMLGenerator.hxx b/arduino/CDIXMLGenerator.hxx new file mode 100644 index 000000000..c548efa74 --- /dev/null +++ b/arduino/CDIXMLGenerator.hxx @@ -0,0 +1,126 @@ +/** \copyright + * Copyright (c) 2018, Balazs Racz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file CDIXMLGenerator.hxx + * + * Standalone CDI XML generation class. + * + * @author Balazs Racz, Mike Dunston + * @date 24 July 2018 + */ + +#ifndef _CDIXMLGENERATOR_HXX_ +#define _CDIXMLGENERATOR_HXX_ + +#include "openlcb/SimpleStack.hxx" +#include "utils/FileUtils.hxx" + +/// Standalone utility class for generating the XML representation of the node +/// configuration structure. This is primarily used in Arduino environments. +class CDIXMLGenerator +{ +public: + /// Creates the XML representation of the configuration structure and saves + /// it to a file on the filesystem. Must be called after SPIFFS.begin() but + /// before calling the {\link create_config_file_if_needed} method. The + /// config file will be re-written whenever there was a change in the + /// contents. It is also necessary to declare the static compiled-in CDI to + /// be empty: + /// ``` + /// namespace openlcb { + /// // This will stop openlcb from exporting the CDI memory space + /// // upon start. + /// extern const char CDI_DATA[] = ""; + /// } // namespace openlcb + /// ``` + /// @param cfg is the global configuration instance (usually called cfg). + /// @param filename is where the xml file can be stored on the + /// filesystem. For example "/spiffs/cdi.xml". + /// @return true if the configuration xml file was modified by this method, + /// false if the file was already up-to-date. + template + static bool create_config_descriptor_xml( + const ConfigDef &config, const char *filename, + openlcb::SimpleStackBase *stack = nullptr) + { + string cdi_string; + ConfigDef cfg(config.offset()); + cfg.config_renderer().render_cdi(&cdi_string); + + cdi_string += '\0'; + + bool need_write = false; + LOG(INFO, "[CDI] Checking %s...", filename); + FILE *ff = fopen(filename, "rb"); + if (!ff) + { + LOG(INFO, "[CDI] File %s does not exist", filename); + need_write = true; + } + else + { + fclose(ff); + string current_str = read_file_to_string(filename); + if (current_str != cdi_string) + { + LOG(INFO, "[CDI] File %s is not up-to-date", filename); + need_write = true; + } + else + { + LOG(INFO, "[CDI] File %s appears up-to-date (len %u vs %u)", + filename, current_str.size(), cdi_string.size()); + } + } + if (need_write) + { + LOG(INFO, "[CDI] Updating %s (len %u)", filename, + cdi_string.size()); + write_string_to_file(filename, cdi_string); + } + + if (stack) + { + LOG(INFO, "[CDI] Registering CDI with stack..."); + // Creates list of event IDs for factory reset. + auto *v = new vector(); + cfg.handle_events([v](unsigned o) { v->push_back(o); }); + v->push_back(0); + stack->set_event_offsets(v); + // We leak v because it has to stay alive for the entire lifetime + // of the stack. + + // Add the file memory space to the stack. + openlcb::MemorySpace *space = + new openlcb::ROFileMemorySpace(filename); + stack->memory_config_handler()->registry()->insert( + stack->node(), openlcb::MemoryConfigDefs::SPACE_CDI, space); + } + return need_write; + } +}; + +#endif // _CDIXMLGENERATOR_HXX_ \ No newline at end of file diff --git a/arduino/OpenMRNLite.h b/arduino/OpenMRNLite.h index e46c3265c..b7c22bf29 100644 --- a/arduino/OpenMRNLite.h +++ b/arduino/OpenMRNLite.h @@ -38,7 +38,7 @@ #include -#include "freertos_drivers/arduino/ArduinoGpio.hxx" +#include "CDIXMLGenerator.hxx" #include "freertos_drivers/arduino/Can.hxx" #include "freertos_drivers/arduino/WifiDefs.hxx" #include "openlcb/SimpleStack.hxx" @@ -49,25 +49,57 @@ #if defined(ESP32) +#include #include #include -namespace openmrn_arduino { +namespace openmrn_arduino +{ /// Default stack size to use for all OpenMRN tasks on the ESP32 platform. constexpr uint32_t OPENMRN_STACK_SIZE = 4096L; /// Default thread priority for any OpenMRN owned tasks on the ESP32 platform. -/// ESP32 hardware CAN RX and TX tasks run at lower priority (-1 and -2 -/// respectively) of this default priority to ensure timely consumption of CAN -/// frames from the hardware driver. /// Note: This is set to one priority level lower than the TCP/IP task uses on /// the ESP32. constexpr UBaseType_t OPENMRN_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 1; } // namespace openmrn_arduino +#include "freertos_drivers/esp32/Esp32Gpio.hxx" +#include "freertos_drivers/esp32/Esp32SocInfo.hxx" + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) + +// If we are using ESP-IDF v4.3 (or later) enable the Esp32Ledc API. +#include "freertos_drivers/esp32/Esp32Ledc.hxx" + +// ESP32-H2 and ESP32-C2 do not have a built-in TWAI controller. +#if !defined(CONFIG_IDF_TARGET_ESP32H2) && !defined(CONFIG_IDF_TARGET_ESP32C2) + +// If we are using ESP-IDF v4.3 (or later) enable the usage of the TWAI device +// which allows usage of the filesystem based CAN interface methods. +#include "freertos_drivers/esp32/Esp32HardwareTwai.hxx" +#define HAVE_CAN_FS_DEVICE + +// The ESP-IDF VFS layer has an optional wrapper around the select() interface +// when disabled we can not use select() for the CAN/TWAI driver. Normally this +// is enabled for arduino-esp32. +#if CONFIG_VFS_SUPPORT_SELECT +#define HAVE_CAN_FS_SELECT +#endif + +#endif // NOT ESP32-H2 and NOT ESP32-C2 + +#endif // IDF v4.3+ + +#if defined(CONFIG_IDF_TARGET_ESP32) +// Note: This code is deprecated in favor of the TWAI interface which exposes +// both select() and fnctl() interfaces. Support for this may be removed in the +// future. #include "freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx" +#endif // ESP32 only + #include "freertos_drivers/esp32/Esp32HardwareSerialAdapter.hxx" #include "freertos_drivers/esp32/Esp32WiFiManager.hxx" @@ -79,11 +111,13 @@ constexpr UBaseType_t OPENMRN_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 1; #ifdef ARDUINO_ARCH_STM32 +#include "freertos_drivers/arduino/ArduinoGpio.hxx" #include "freertos_drivers/stm32/Stm32Can.hxx" #endif -namespace openmrn_arduino { +namespace openmrn_arduino +{ /// Bridge class that connects an Arduino API style serial port (sending CAN /// frames via gridconnect format) to the OpenMRN core stack. This can be @@ -409,9 +443,9 @@ class OpenMRN : private Executable , OPENMRN_TASK_PRIORITY // priority , nullptr // task handle , PRO_CPU_NUM); // cpu core -#else +#else // NOT ESP32 stack_->executor()->start_thread( - "OpenMRN", OPENMRN_TASK_PRIORITY, OPENMRN_STACK_SIZE); + "OpenMRN", 0 /* default priority */, 0 /* default stack size */); #endif // ESP32 } #endif // OPENMRN_FEATURE_SINGLE_THREADED @@ -444,6 +478,40 @@ class OpenMRN : private Executable loopMembers_.push_back(new CanBridge(port, stack()->can_hub())); } +#if defined(HAVE_CAN_FS_DEVICE) + /// Adds a CAN bus port with synchronous driver API. + void add_can_port_blocking(const char *device) + { + stack_->add_can_port_blocking(device); + } + + /// Adds a CAN bus port with asynchronous driver API. + void add_can_port_async(const char *device) + { + stack_->add_can_port_async(device); + } + +#if defined(HAVE_CAN_FS_SELECT) + /// Adds a CAN bus port with select-based asynchronous driver API. + /// + /// NOTE: Be sure to call @ref start_executor_thread in the setup() method. + void add_can_port_select(const char *device) + { + stack_->add_can_port_select(device); + } + + /// Adds a CAN bus port with select-based asynchronous driver API. + /// @param fd file descriptor to add to can hub + /// @param on_error Notifiable to wakeup on error + /// + /// NOTE: Be sure to call @ref start_executor_thread in the setup() method. + void add_can_port_select(int fd, Notifiable *on_error = nullptr) + { + stack_->add_can_port_select(fd, on_error); + } +#endif // HAVE_CAN_FS_SELECT +#endif // HAVE_CAN_FS_DEVICE + #if defined(HAVE_FILESYSTEM) /// Creates the XML representation of the configuration structure and saves /// it to a file on the filesystem. Must be called after SPIFFS.begin() but @@ -461,50 +529,13 @@ class OpenMRN : private Executable /// @param cfg is the global configuration instance (usually called cfg). /// @param filename is where the xml file can be stored on the /// filesystem. For example "/spiffs/cdi.xml". + /// @returns true if the cdi.xml was updated, false otherwise. template - void create_config_descriptor_xml( + bool create_config_descriptor_xml( const ConfigDef &config, const char *filename) { - string cdi_string; - ConfigDef cfg(config.offset()); - cfg.config_renderer().render_cdi(&cdi_string); - - cdi_string += '\0'; - - bool need_write = false; - FILE *ff = fopen(filename, "rb"); - if (!ff) - { - need_write = true; - } - else - { - fclose(ff); - string current_str = read_file_to_string(filename); - if (current_str != cdi_string) - { - need_write = true; - } - } - if (need_write) - { - LOG(INFO, "Updating CDI file %s (len %u)", filename, - cdi_string.size()); - write_string_to_file(filename, cdi_string); - } - - // Creates list of event IDs for factory reset. - auto *v = new vector(); - cfg.handle_events([v](unsigned o) { v->push_back(o); }); - v->push_back(0); - stack()->set_event_offsets(v); - // We leak v because it has to stay alive for the entire lifetime of - // the stack. - - // Exports the file memory space. - openlcb::MemorySpace *space = new openlcb::ROFileMemorySpace(filename); - stack()->memory_config_handler()->registry()->insert( - stack()->node(), openlcb::MemoryConfigDefs::SPACE_CDI, space); + return CDIXMLGenerator::create_config_descriptor_xml( + config, filename, stack()); } #endif // HAVE_FILESYSTEM diff --git a/arduino/examples/ESP32C3CanLoadTest/.gitignore b/arduino/examples/ESP32C3CanLoadTest/.gitignore new file mode 100644 index 000000000..70cfab999 --- /dev/null +++ b/arduino/examples/ESP32C3CanLoadTest/.gitignore @@ -0,0 +1 @@ +wifi_params.cpp diff --git a/arduino/examples/ESP32C3CanLoadTest/ESP32C3CanLoadTest.ino b/arduino/examples/ESP32C3CanLoadTest/ESP32C3CanLoadTest.ino new file mode 100644 index 000000000..b6f4cc297 --- /dev/null +++ b/arduino/examples/ESP32C3CanLoadTest/ESP32C3CanLoadTest.ino @@ -0,0 +1,286 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file ESP32C3CanLoadTest.ino + * + * Main file for the ESP32-C3 CAN Load Test application. + * + * @author Mike Dunston + * @date 2 May 2021 + */ + +#include +#include +#include + +#include +#include + +#include +#include +#include + +// Pick an operating mode below, if you select USE_WIFI it will expose +// this node on WIFI if you select USE_TWAI, this node will be available +// on CAN. +// Enabling both options will allow the ESP32 to be accessible from +// both WiFi and CAN interfaces. + +#define USE_WIFI +//#define USE_TWAI + +// Uncomment USE_TWAI_SELECT to enable the usage of select() for the TWAI +// interface. +//#define USE_TWAI_SELECT + +// uncomment the line below to have all packets printed to the Serial +// output. This is not recommended for production deployment. +//#define PRINT_PACKETS + +// If USE_TWAI_SELECT or USE_TWAI_ASYNC is enabled but USE_TWAI is not, enable +// USE_TWAI. +#if defined(USE_TWAI_SELECT) && !defined(USE_TWAI) +#define USE_TWAI +#endif // USE_TWAI_SELECT && !USE_TWAI + +#include "config.h" + +/// This is the node id to assign to this device, this must be unique +/// on the CAN bus. +static constexpr uint64_t NODE_ID = UINT64_C(0x05010101182c); + +#if defined(USE_WIFI) +// Configuring WiFi accesspoint name and password +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// There are two options: +// 1) edit the sketch to set this information just below. Use quotes: +// const char* ssid = "linksys"; +// const char* password = "superSecret"; +// 2) add a new file to the sketch folder called something.cpp with the +// following contents: +// #include +// +// char WIFI_SSID[] = "linksys"; +// char WIFI_PASS[] = "theTRUEsupers3cr3t"; + +/// This is the name of the WiFi network (access point) to connect to. +const char *ssid = WIFI_SSID; + +/// Password of the wifi network. +const char *password = WIFI_PASS; + +/// This is the hostname which the ESP32 will advertise via mDNS, it should be +/// unique. +const char *hostname = "esp32mrn"; + +OVERRIDE_CONST(gridconnect_buffer_size, 3512); +//OVERRIDE_CONST(gridconnect_buffer_delay_usec, 200000); +OVERRIDE_CONST(gridconnect_buffer_delay_usec, 2000); +OVERRIDE_CONST(gc_generate_newlines, CONSTANT_TRUE); +OVERRIDE_CONST(executor_select_prescaler, 60); +OVERRIDE_CONST(gridconnect_bridge_max_outgoing_packets, 2); + +#endif // USE_WIFI + +#if defined(USE_TWAI) +// This is the ESP32-C3 pin connected to the SN65HVD23x/MCP2551 R (RX) pin. +// Note: Any pin can be used for this other than 11-17 which are connected to +// the onboard flash. +// Note: Adjusting this pin assignment will require updating the GPIO_PIN +// declarations below for input/outputs. +constexpr gpio_num_t TWAI_RX_PIN = GPIO_NUM_18; + +// This is the ESP32-C3 pin connected to the SN65HVD23x/MCP2551 D (TX) pin. +// Note: Any pin can be used for this other than 11-17 which are connected to +// the onboard flash. +// Note: Adjusting this pin assignment will require updating the GPIO_PIN +// declarations below for input/outputs. +constexpr gpio_num_t TWAI_TX_PIN = GPIO_NUM_19; + +#endif // USE_TWAI + +// This is the primary entrypoint for the OpenMRN/LCC stack. +OpenMRN openmrn(NODE_ID); + +// This tracks the CPU usage of the ESP32-C3 through the usage of a hardware +// timer that records what the CPU is currently executing roughly 163 times per +// second. +CpuLoad cpu_load; + +// This will report the usage to the console output. +CpuLoadLog cpu_log(openmrn.stack()->service()); + +// ConfigDef comes from config.h and is specific to this particular device and +// target. It defines the layout of the configuration memory space and is also +// used to generate the cdi.xml file. Here we instantiate the configuration +// layout. The argument of offset zero is ignored and will be removed later. +static constexpr openlcb::ConfigDef cfg(0); + +#if defined(USE_WIFI) +Esp32WiFiManager wifi_mgr(ssid, password, openmrn.stack(), cfg.seg().wifi()); +#endif // USE_WIFI + +#if defined(USE_TWAI) +Esp32HardwareTwai twai(TWAI_RX_PIN, TWAI_TX_PIN); +#endif // USE_TWAI + +// This will perform the factory reset procedure for this node's configuration +// items. +// +// The node name and description will be set to the SNIP model name field +// value. +// Descriptions for intputs and outputs will be set to a blank string, input +// debounce parameters will be set to default values. +class FactoryResetHelper : public DefaultConfigUpdateListener { +public: + UpdateAction apply_configuration(int fd, bool initial_load, + BarrierNotifiable *done) OVERRIDE { + AutoNotify n(done); + return UPDATED; + } + + void factory_reset(int fd) override + { + cfg.userinfo().name().write(fd, openlcb::SNIP_STATIC_DATA.model_name); + cfg.userinfo().description().write( + fd, openlcb::SNIP_STATIC_DATA.model_name); + } +} factory_reset_helper; + +namespace openlcb +{ + // Name of CDI.xml to generate dynamically. + const char CDI_FILENAME[] = "/spiffs/cdi.xml"; + + // This will stop openlcb from exporting the CDI memory space upon start. + extern const char CDI_DATA[] = ""; + + // Path to where OpenMRN should persist general configuration data. + extern const char *const CONFIG_FILENAME = "/spiffs/openlcb_config"; + + // The size of the memory space to export over the above device. + extern const size_t CONFIG_FILE_SIZE = cfg.seg().size() + cfg.seg().offset(); + + // Default to store the dynamic SNIP data is stored in the same persistant + // data file as general configuration data. + extern const char *const SNIP_DYNAMIC_FILENAME = CONFIG_FILENAME; +} + +// Callback function for the hardware timer configured to fire roughly 163 +// times per second. +void ARDUINO_ISR_ATTR record_cpu_usage() +{ +#if CONFIG_ARDUINO_ISR_IRAM + // if the ISR is called with flash disabled we can not safely recored the + // cpu usage. + if (!spi_flash_cache_enabled()) + { + return; + } +#endif + // Retrieves the vtable pointer from the currently running executable. + unsigned *pp = (unsigned *)openmrn.stack()->executor()->current(); + cpuload_tick(pp ? pp[0] | 1 : 0); +} + +void setup() +{ + Serial.begin(115200L); + LOG(INFO, "[Node] ID: %s", uint64_to_string_hex(NODE_ID).c_str()); + LOG(INFO, "[SNIP] version:%d, manufacturer:%s, model:%s, hw-v:%s, sw-v:%s" + , openlcb::SNIP_STATIC_DATA.version + , openlcb::SNIP_STATIC_DATA.manufacturer_name + , openlcb::SNIP_STATIC_DATA.model_name + , openlcb::SNIP_STATIC_DATA.hardware_version + , openlcb::SNIP_STATIC_DATA.software_version); + + // Register hardware timer zero to use a 1Mhz resolution and to count up + // from zero when the timer triggers. + auto timer = timerBegin(0, 80, true); + // Attach our callback function to be called when the timer is ready to + // fire. Note that the edge parameter is not used/supported on the + // ESP32-C3. + timerAttachInterrupt(timer, record_cpu_usage, true); + // Configure the trigger point to be roughly 163 times per second. + timerAlarmWrite(timer, 1000000/163, true); + // Enable the timer. + timerAlarmEnable(timer); + + // Initialize the SPIFFS filesystem as our persistence layer + if (!SPIFFS.begin()) + { + LOG(WARNING, "SPIFFS failed to mount, attempting to format and remount"); + if (!SPIFFS.begin(true)) + { + LOG_ERROR("SPIFFS mount failed even with format, giving up!"); + while (1) + { + // Unable to start SPIFFS successfully, give up and wait + // for WDT to kick in + } + } + } + + // Create the CDI.xml dynamically + openmrn.create_config_descriptor_xml(cfg, openlcb::CDI_FILENAME); + + // Create the default internal configuration file + openmrn.stack()->create_config_file_if_needed(cfg.seg().internal_config(), + openlcb::CANONICAL_VERSION, openlcb::CONFIG_FILE_SIZE); + +#if defined(USE_TWAI) + twai.hw_init(); +#endif // USE_TWAI + + // Start the OpenMRN stack + openmrn.begin(); + +#if defined(PRINT_PACKETS) + // Dump all packets as they are sent/received. + // Note: This should not be enabled in deployed nodes as it will + // have performance impact. + openmrn.stack()->print_all_packets(); +#endif // PRINT_PACKETS + +#if defined(USE_TWAI_SELECT) + // add TWAI driver with select() usage + openmrn.add_can_port_select("/dev/twai/twai0"); + + // start executor thread since this is required for select() to work in the + // OpenMRN executor. + openmrn.start_executor_thread(); +#else + // add TWAI driver with non-blocking usage + openmrn.add_can_port_async("/dev/twai/twai0"); +#endif // USE_TWAI_SELECT +} + +void loop() +{ + // Call the OpenMRN executor, this needs to be done as often + // as possible from the loop() method. + openmrn.loop(); +} diff --git a/arduino/examples/ESP32C3CanLoadTest/config.h b/arduino/examples/ESP32C3CanLoadTest/config.h new file mode 100644 index 000000000..40913fdc6 --- /dev/null +++ b/arduino/examples/ESP32C3CanLoadTest/config.h @@ -0,0 +1,78 @@ +#ifndef _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ +#define _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ + +#include "openlcb/ConfiguredConsumer.hxx" +#include "openlcb/ConfiguredProducer.hxx" +#include "openlcb/ConfigRepresentation.hxx" +#include "openlcb/MemoryConfig.hxx" + +#include "freertos_drivers/esp32/Esp32WiFiConfiguration.hxx" + +// catch invalid configuration at compile time +#if !defined(USE_TWAI) && !defined(USE_WIFI) +#error "Invalid configuration detected, USE_CAN or USE_WIFI must be defined." +#endif + +namespace openlcb +{ + +/// Defines the identification information for the node. The arguments are: +/// +/// - 4 (version info, always 4 by the standard +/// - Manufacturer name +/// - Model name +/// - Hardware version +/// - Software version +/// +/// This data will be used for all purposes of the identification: +/// +/// - the generated cdi.xml will include this data +/// - the Simple Node Ident Info Protocol will return this data +/// - the ACDI memory space will contain this data. +extern const SimpleNodeStaticValues SNIP_STATIC_DATA = +{ + 4, + "OpenMRN", +#if defined(USE_WIFI) && !defined(USE_TWAI) + "Arduino Load Test (WiFi)", +#elif defined(USE_TWAI) && !defined(USE_WIFI) + "Arduino Load Test (TWAI)", +#else + "Arduino Load Test (WiFi/TWAI)", +#endif + ARDUINO_VARIANT, + "1.00" +}; + +/// Modify this value every time the EEPROM needs to be cleared on the node +/// after an update. +static constexpr uint16_t CANONICAL_VERSION = 0x100b; + +/// Defines the main segment in the configuration CDI. This is laid out at +/// origin 128 to give space for the ACDI user data at the beginning. +CDI_GROUP(IoBoardSegment, Segment(MemoryConfigDefs::SPACE_CONFIG), Offset(128)); +/// Each entry declares the name of the current entry, then the type and then +/// optional arguments list. +CDI_GROUP_ENTRY(internal_config, InternalConfigData); +#if defined(USE_WIFI) +CDI_GROUP_ENTRY(wifi, WiFiConfiguration, Name("WiFi Configuration")); +#endif +CDI_GROUP_END(); + +/// The main structure of the CDI. ConfigDef is the symbol we use in main.cxx +/// to refer to the configuration defined here. +CDI_GROUP(ConfigDef, MainCdi()); +/// Adds the tag with the values from SNIP_STATIC_DATA above. +CDI_GROUP_ENTRY(ident, Identification); +/// Adds an tag. +CDI_GROUP_ENTRY(acdi, Acdi); +/// Adds a segment for changing the values in the ACDI user-defined +/// space. UserInfoSegment is defined in the system header. +CDI_GROUP_ENTRY(userinfo, UserInfoSegment, Name("User Info")); +/// Adds the main configuration segment. +CDI_GROUP_ENTRY(seg, IoBoardSegment, Name("Settings")); +CDI_GROUP_END(); + +} // namespace openlcb + +#endif // _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ diff --git a/arduino/examples/ESP32C3IOBoard/.gitignore b/arduino/examples/ESP32C3IOBoard/.gitignore new file mode 100644 index 000000000..70cfab999 --- /dev/null +++ b/arduino/examples/ESP32C3IOBoard/.gitignore @@ -0,0 +1 @@ +wifi_params.cpp diff --git a/arduino/examples/ESP32C3IOBoard/ESP32C3IOBoard.ino b/arduino/examples/ESP32C3IOBoard/ESP32C3IOBoard.ino new file mode 100644 index 000000000..8a0caa7e4 --- /dev/null +++ b/arduino/examples/ESP32C3IOBoard/ESP32C3IOBoard.ino @@ -0,0 +1,432 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file ESP32C3IOBoard.ino + * + * Main file for the io board application on an ESP32-C3. + * + * @author Mike Dunston + * @date 2 May 2021 + */ + +#include +#include + +#include +#include "openlcb/ConfiguredConsumer.hxx" +#include "openlcb/ConfiguredProducer.hxx" +#include "openlcb/MultiConfiguredConsumer.hxx" +#include "utils/GpioInitializer.hxx" + +// Pick an operating mode below, if you select USE_WIFI it will expose this +// node on WIFI. If USE_TWAI or USE_TWAI_ASYNC are enabled the node will be +// available on CAN. +// +// Enabling both options will allow the ESP32 to be accessible from +// both WiFi and CAN interfaces. + +#define USE_WIFI +//#define USE_TWAI +//#define USE_TWAI_ASYNC + +// uncomment the line below to have all packets printed to the Serial +// output. This is not recommended for production deployment. +//#define PRINT_PACKETS + +// uncomment the line below to specify a GPIO pin that should be used to force +// a factory reset when the node starts and the GPIO pin reads LOW. +// Note: GPIO 10 is also used for IO9, care must be taken to ensure that this +// GPIO pin is not used both for FACTORY_RESET and an OUTPUT pin. +//#define FACTORY_RESET_GPIO_PIN 10 + +// Uncomment FIRMWARE_UPDATE_BOOTLOADER to enable the bootloader feature when +// using the TWAI device. +// +// NOTE: in order for this to work you *MUST* use a partition schema that has +// two app partitions, typically labeled with "OTA" in the partition name in +// the Arduino IDE. +//#define FIRMWARE_UPDATE_BOOTLOADER + +// Configuration option validation + +#if defined(USE_TWAI_ASYNC) && defined(USE_TWAI) +#error USE_TWAI_ASYNC and USE_TWAI are mutually exclusive! +#endif + +#if defined(USE_TWAI_ASYNC) && !defined(USE_TWAI) +#define USE_TWAI +#endif // USE_TWAI_ASYNC && !USE_TWAI + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) && !defined(USE_TWAI) +#error Firmware update is only supported via TWAI, enable USE_TWAI to use this. +#endif + +#include "config.h" + +/// This is the node id to assign to this device, this must be unique +/// on the TWAI bus. +static constexpr uint64_t NODE_ID = UINT64_C(0x05010101182d); + +#if defined(USE_WIFI) +// Configuring WiFi accesspoint name and password +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// There are two options: +// 1) edit the sketch to set this information just below. Use quotes: +// const char* ssid = "linksys"; +// const char* password = "superSecret"; +// 2) add a new file to the sketch folder called something.cpp with the +// following contents: +// #include +// +// char WIFI_SSID[] = "linksys"; +// char WIFI_PASS[] = "theTRUEsupers3cr3t"; + +/// This is the name of the WiFi network (access point) to connect to. +const char *ssid = WIFI_SSID; + +/// Password of the wifi network. +const char *password = WIFI_PASS; + +/// This is the hostname which the ESP32 will advertise via mDNS, it should be +/// unique. +const char *hostname = "esp32mrn"; + +// Uncomment this line to enable usage of ::select() within the Grid Connect +// code. +//OVERRIDE_CONST_TRUE(gridconnect_tcp_use_select); + +#endif // USE_WIFI + +#if defined(USE_TWAI) +// This is the ESP32-C3 pin connected to the SN65HVD23x/MCP2551 R (RX) pin. +// NOTE: Any pin can be used for this other than 11-17 which are connected to +// the onboard flash. +// NOTE: Adjusting this pin assignment will require updating the GPIO_PIN +// declarations below for input/outputs. +// NOTE: USB is connected to GPIO 18 (D-) and 19 (D+) and can not be changed +// to any other pins. When "USB CDC On Boot" is selected in Arduino IDE this +// pin will need to be changed as well as input/output pins changed +// accordingly. +constexpr gpio_num_t TWAI_RX_PIN = GPIO_NUM_18; + +// This is the ESP32-C3 pin connected to the SN65HVD23x/MCP2551 D (TX) pin. +// NOTE: Any pin can be used for this other than 11-17 which are connected to +// the onboard flash. +// NOTE: Adjusting this pin assignment will require updating the GPIO_PIN +// declarations below for input/outputs. +// NOTE: USB is connected to GPIO 18 (D-) and 19 (D+) and can not be changed +// to any other pins. When "USB CDC On Boot" is selected in Arduino IDE this +// pin will need to be changed as well as input/output pins changed +// accordingly. +constexpr gpio_num_t TWAI_TX_PIN = GPIO_NUM_19; + +#endif // USE_TWAI + +#if defined(FACTORY_RESET_GPIO_PIN) +static constexpr uint8_t FACTORY_RESET_COUNTDOWN_SECS = 10; +#endif // FACTORY_RESET_GPIO_PIN + +/// This is the primary entrypoint for the OpenMRN/LCC stack. +OpenMRN openmrn(NODE_ID); + +// ConfigDef comes from config.h and is specific to this particular device and +// target. It defines the layout of the configuration memory space and is also +// used to generate the cdi.xml file. Here we instantiate the configuration +// layout. The argument of offset zero is ignored and will be removed later. +static constexpr openlcb::ConfigDef cfg(0); + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) +// Include the Bootloader HAL implementation for the ESP32. This should only +// be included in one ino/cpp file. +#include "freertos_drivers/esp32/Esp32BootloaderHal.hxx" +#endif // FIRMWARE_UPDATE_BOOTLOADER + +#if defined(USE_WIFI) +Esp32WiFiManager wifi_mgr(ssid, password, openmrn.stack(), cfg.seg().wifi()); +#endif // USE_WIFI + +#if defined(USE_TWAI) +Esp32HardwareTwai twai(TWAI_RX_PIN, TWAI_TX_PIN); +#endif // USE_TWAI + +// Declare output pins. +GPIO_PIN(IO0, GpioOutputSafeLow, 0); +GPIO_PIN(IO1, GpioOutputSafeLow, 1); +GPIO_PIN(IO2, GpioOutputSafeLow, 2); +GPIO_PIN(IO3, GpioOutputSafeLow, 3); +GPIO_PIN(IO4, GpioOutputSafeLow, 4); + +// Declare input pins. +GPIO_PIN(IO5, GpioInputPU, 5); +GPIO_PIN(IO6, GpioInputPU, 6); +GPIO_PIN(IO7, GpioInputPU, 7); +GPIO_PIN(IO8, GpioInputPU, 9); +GPIO_PIN(IO9, GpioInputPU, 10); + +#if defined(FACTORY_RESET_GPIO_PIN) +GPIO_PIN(FACTORY_RESET, GpioInputPU, FACTORY_RESET_GPIO_PIN); +#endif // FACTORY_RESET_GPIO_PIN + +// List of GPIO objects that will be used for the output pins. You should keep +// the constexpr declaration, because it will produce a compile error in case +// the list of pointers cannot be compiled into a compiler constant and thus +// would be placed into RAM instead of ROM. +constexpr const Gpio *const outputGpioSet[] = { + IO0_Pin::instance(), IO1_Pin::instance(), // + IO2_Pin::instance(), IO3_Pin::instance(), // + IO4_Pin::instance() +}; + +openlcb::MultiConfiguredConsumer gpio_consumers(openmrn.stack()->node(), outputGpioSet, + ARRAYSIZE(outputGpioSet), cfg.seg().consumers()); + +openlcb::ConfiguredProducer IO5_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<0>(), IO5_Pin()); +openlcb::ConfiguredProducer IO6_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<1>(), IO6_Pin()); +openlcb::ConfiguredProducer IO7_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<2>(), IO7_Pin()); +openlcb::ConfiguredProducer IO8_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<3>(), IO8_Pin()); +openlcb::ConfiguredProducer IO9_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<4>(), IO9_Pin()); + +// Create an initializer that can initialize all the GPIO pins in one shot +typedef GpioInitializer< +#if defined(FACTORY_RESET_GPIO_PIN) + FACTORY_RESET_Pin, // factory reset +#endif // FACTORY_RESET_GPIO_PIN + IO0_Pin, IO1_Pin, IO2_Pin, IO3_Pin, IO4_Pin, // output pins + IO5_Pin, IO6_Pin, IO7_Pin, IO8_Pin, IO9_Pin // input pins + > GpioInit; + +// The producers need to be polled repeatedly for changes and to execute the +// debouncing algorithm. This class instantiates a refreshloop and adds the +// producers to it. +openlcb::RefreshLoop producer_refresh_loop(openmrn.stack()->node(), + { + IO5_producer.polling(), + IO6_producer.polling(), + IO7_producer.polling(), + IO8_producer.polling(), + IO9_producer.polling() + } +); + +// This will perform the factory reset procedure for this node's configuration +// items. +// +// The node name and description will be set to the SNIP model name field +// value. +// Descriptions for intputs and outputs will be set to a blank string, input +// debounce parameters will be set to default values. +class FactoryResetHelper : public DefaultConfigUpdateListener { +public: + UpdateAction apply_configuration(int fd, bool initial_load, + BarrierNotifiable *done) OVERRIDE { + AutoNotify n(done); + return UPDATED; + } + + void factory_reset(int fd) override + { + cfg.userinfo().name().write(fd, openlcb::SNIP_STATIC_DATA.model_name); + cfg.userinfo().description().write( + fd, openlcb::SNIP_STATIC_DATA.model_name); + for(int i = 0; i < openlcb::NUM_OUTPUTS; i++) + { + cfg.seg().consumers().entry(i).description().write(fd, ""); + } + for(int i = 0; i < openlcb::NUM_INPUTS; i++) + { + cfg.seg().producers().entry(i).description().write(fd, ""); + CDI_FACTORY_RESET(cfg.seg().producers().entry(i).debounce); + } + } +} factory_reset_helper; + +namespace openlcb +{ + // Name of CDI.xml to generate dynamically. + const char CDI_FILENAME[] = "/spiffs/cdi.xml"; + + // This will stop openlcb from exporting the CDI memory space upon start. + extern const char CDI_DATA[] = ""; + + // Path to where OpenMRN should persist general configuration data. + extern const char *const CONFIG_FILENAME = "/spiffs/openlcb_config"; + + // The size of the memory space to export over the above device. + extern const size_t CONFIG_FILE_SIZE = cfg.seg().size() + cfg.seg().offset(); + + // Default to store the dynamic SNIP data is stored in the same persistant + // data file as general configuration data. + extern const char *const SNIP_DYNAMIC_FILENAME = CONFIG_FILENAME; +} + +void check_for_factory_reset() +{ +#if defined(FACTORY_RESET_GPIO_PIN) + // Check the factory reset pin which should normally read HIGH (set), if it + // reads LOW (clr) delete the cdi.xml and openlcb_config + if (!FACTORY_RESET_Pin::instance()->read()) + { + LOG(WARNING, "!!!! WARNING WARNING WARNING WARNING WARNING !!!!"); + LOG(WARNING, "The factory reset GPIO pin %d has been triggered.", + FACTORY_RESET_GPIO_PIN); + for (uint8_t sec = FACTORY_RESET_COUNTDOWN_SECS; + sec > 0 && !FACTORY_RESET_Pin::instance()->read(); sec--) + { + LOG(WARNING, "Factory reset will be initiated in %d seconds.", + sec); + usleep(SEC_TO_USEC(1)); + } + if (!FACTORY_RESET_Pin::instance()->read()) + { + unlink(openlcb::CDI_FILENAME); + unlink(openlcb::CONFIG_FILENAME); + LOG(WARNING, "Factory reset complete"); + } + else + { + LOG(WARNING, "Factory reset aborted as pin %d was not held LOW", + FACTORY_RESET_GPIO_PIN); + } + } +#endif // FACTORY_RESET_GPIO_PIN +} + +void setup() +{ + Serial.begin(115200L); + uint8_t reset_reason = Esp32SocInfo::print_soc_info(); + LOG(INFO, "[Node] ID: %s", uint64_to_string_hex(NODE_ID).c_str()); + LOG(INFO, "[SNIP] version:%d, manufacturer:%s, model:%s, hw-v:%s, sw-v:%s", + openlcb::SNIP_STATIC_DATA.version, + openlcb::SNIP_STATIC_DATA.manufacturer_name, + openlcb::SNIP_STATIC_DATA.model_name, + openlcb::SNIP_STATIC_DATA.hardware_version, + openlcb::SNIP_STATIC_DATA.software_version); + + // Initialize the SPIFFS filesystem as our persistence layer + if (!SPIFFS.begin()) + { + LOG(WARNING, "SPIFFS failed to mount, attempting to format and remount"); + if (!SPIFFS.begin(true)) + { + LOG_ERROR("SPIFFS mount failed even with format, giving up!"); + while (1) + { + // Unable to start SPIFFS successfully, give up and wait + // for WDT to kick in + } + } + } + + // initialize all declared GPIO pins + GpioInit::hw_init(); + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) + // initialize the bootloader. + esp32_bootloader_init(reset_reason); + + // if we have a request to enter the bootloader we need to process it + // before we startup the OpenMRN stack. + if (request_bootloader()) + { + esp32_bootloader_run(NODE_ID, TWAI_RX_PIN, TWAI_TX_PIN); + // This line should not be reached as the esp32_bootloader_run method + // will not return by default. + HASSERT(false); + } +#endif // FIRMWARE_UPDATE_BOOTLOADER + + check_for_factory_reset(); + + // Create the CDI.xml dynamically + openmrn.create_config_descriptor_xml(cfg, openlcb::CDI_FILENAME); + + // Create the default internal configuration file + openmrn.stack()->create_config_file_if_needed(cfg.seg().internal_config(), + openlcb::CANONICAL_VERSION, openlcb::CONFIG_FILE_SIZE); + +#if defined(USE_TWAI) + twai.hw_init(); +#endif // USE_TWAI + + // Start the OpenMRN stack + openmrn.begin(); + + if (reset_reason == RTCWDT_BROWN_OUT_RESET) + { + openmrn.stack()->executor()->add(new CallbackExecutable([]() + { + openmrn.stack()->send_event(openlcb::Defs::NODE_POWER_BROWNOUT_EVENT); + })); + } + +#if defined(PRINT_PACKETS) + // Dump all packets as they are sent/received. + // Note: This should not be enabled in deployed nodes as it will have + // performance impact. + openmrn.stack()->print_all_packets(); +#endif // PRINT_PACKETS + +#if defined(USE_TWAI_ASYNC) + openmrn.add_can_port_async("/dev/twai/twai0"); +#elif defined(USE_TWAI) + openmrn.add_can_port_select("/dev/twai/twai0"); + + // start executor thread since this is required for select() to work in the + // OpenMRN executor. + openmrn.start_executor_thread(); +#endif // USE_TWAI_ASYNC +} + +void loop() +{ + // Call the OpenMRN executor, this needs to be done as often + // as possible from the loop() method. + openmrn.loop(); +} + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) + +extern "C" +{ + +/// Updates the state of a status LED. +/// +/// @param led is the LED to update. +/// @param value is the new state of the LED. +void bootloader_led(enum BootloaderLed led, bool value) +{ + LOG(INFO, "[Bootloader] bootloader_led(%d, %d)", led, value); +} + +} // extern "C" + +#endif // FIRMWARE_UPDATE_BOOTLOADER \ No newline at end of file diff --git a/arduino/examples/ESP32C3IOBoard/config.h b/arduino/examples/ESP32C3IOBoard/config.h new file mode 100644 index 000000000..82ff92814 --- /dev/null +++ b/arduino/examples/ESP32C3IOBoard/config.h @@ -0,0 +1,92 @@ +#ifndef _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ +#define _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ + +#include "openlcb/ConfiguredConsumer.hxx" +#include "openlcb/ConfiguredProducer.hxx" +#include "openlcb/ConfigRepresentation.hxx" +#include "openlcb/MemoryConfig.hxx" + +#include "freertos_drivers/esp32/Esp32WiFiConfiguration.hxx" + +// catch invalid configuration at compile time +#if !defined(USE_TWAI) && !defined(USE_WIFI) +#error "Invalid configuration detected, USE_TWAI or USE_WIFI must be defined." +#endif + +namespace openlcb +{ + +/// Defines the identification information for the node. The arguments are: +/// +/// - 4 (version info, always 4 by the standard +/// - Manufacturer name +/// - Model name +/// - Hardware version +/// - Software version +/// +/// This data will be used for all purposes of the identification: +/// +/// - the generated cdi.xml will include this data +/// - the Simple Node Ident Info Protocol will return this data +/// - the ACDI memory space will contain this data. +extern const SimpleNodeStaticValues SNIP_STATIC_DATA = +{ + 4, + "OpenMRN", +#if defined(USE_WIFI) && !defined(USE_TWAI) + "Arduino IO Board (WiFi)", +#elif defined(USE_TWAI) && !defined(USE_WIFI) + "Arduino IO Board (CAN)", +#elif defined(USE_TWAI) && defined(USE_WIFI) + "Arduino IO Board (WiFi/CAN)", +#else + "Arduino IO Board", +#endif + ARDUINO_VARIANT, + "1.00" +}; + +constexpr uint8_t NUM_OUTPUTS = 5; +constexpr uint8_t NUM_INPUTS = 5; + +/// Declares a repeated group of a given base group and number of repeats. The +/// ProducerConfig and ConsumerConfig groups represent the configuration layout +/// needed by the ConfiguredProducer and ConfiguredConsumer classes, and come +/// from their respective hxx file. +using AllConsumers = RepeatedGroup; +using AllProducers = RepeatedGroup; + +/// Modify this value every time the EEPROM needs to be cleared on the node +/// after an update. +static constexpr uint16_t CANONICAL_VERSION = 0x100a; + +/// Defines the main segment in the configuration CDI. This is laid out at +/// origin 128 to give space for the ACDI user data at the beginning. +CDI_GROUP(IoBoardSegment, Segment(MemoryConfigDefs::SPACE_CONFIG), Offset(128)); +/// Each entry declares the name of the current entry, then the type and then +/// optional arguments list. +CDI_GROUP_ENTRY(internal_config, InternalConfigData); +CDI_GROUP_ENTRY(consumers, AllConsumers, Name("Outputs"), RepName("Output")); +CDI_GROUP_ENTRY(producers, AllProducers, Name("Inputs"), RepName("Input")); +#if defined(USE_WIFI) +CDI_GROUP_ENTRY(wifi, WiFiConfiguration, Name("WiFi Configuration")); +#endif +CDI_GROUP_END(); + +/// The main structure of the CDI. ConfigDef is the symbol we use in main.cxx +/// to refer to the configuration defined here. +CDI_GROUP(ConfigDef, MainCdi()); +/// Adds the tag with the values from SNIP_STATIC_DATA above. +CDI_GROUP_ENTRY(ident, Identification); +/// Adds an tag. +CDI_GROUP_ENTRY(acdi, Acdi); +/// Adds a segment for changing the values in the ACDI user-defined +/// space. UserInfoSegment is defined in the system header. +CDI_GROUP_ENTRY(userinfo, UserInfoSegment, Name("User Info")); +/// Adds the main configuration segment. +CDI_GROUP_ENTRY(seg, IoBoardSegment, Name("Settings")); +CDI_GROUP_END(); + +} // namespace openlcb + +#endif // _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ diff --git a/arduino/examples/ESP32CanLoadTest/ESP32CanLoadTest.ino b/arduino/examples/ESP32CanLoadTest/ESP32CanLoadTest.ino index 19b5fd99c..4017be7ed 100644 --- a/arduino/examples/ESP32CanLoadTest/ESP32CanLoadTest.ino +++ b/arduino/examples/ESP32CanLoadTest/ESP32CanLoadTest.ino @@ -33,10 +33,7 @@ */ #include -#include #include -#include -#include #include #include @@ -46,25 +43,45 @@ #include #include -// Pick an operating mode below, if you select USE_WIFI it will expose -// this node on WIFI if you select USE_CAN, this node will be available -// on CAN. + +// Pick an operating mode below, if you select USE_WIFI it will expose this +// node on WIFI. If USE_CAN / USE_TWAI / USE_TWAI_ASYNC are enabled the node +// will be available on CAN. +// // Enabling both options will allow the ESP32 to be accessible from // both WiFi and CAN interfaces. +// +// NOTE: USE_TWAI and USE_TWAI_ASYNC are similar to USE_CAN but utilize the +// new TWAI driver which offers both select() (default) or fnctl() (async) +// access. +// NOTE: USE_CAN is deprecated and no longer supported upstream by ESP-IDF as +// of v4.2 or arduino-esp32 as of v2.0.0. #define USE_WIFI -#define USE_CAN - -// Uncomment the line below to have this node advertise itself via mDNS as a -// hub. When this is enabled, other devices can find and connect to this node -// via mDNS, treating it as a hub. Note this requires USE_WIFI to be enabled -// above and should only be enabled on one node which is acting as a hub. -// #define BROADCAST_MDNS +//#define USE_CAN +//#define USE_TWAI +//#define USE_TWAI_ASYNC // uncomment the line below to have all packets printed to the Serial // output. This is not recommended for production deployment. //#define PRINT_PACKETS +// Configuration option validation + +// If USE_TWAI_ASYNC is enabled but USE_TWAI is not, enable USE_TWAI. +#if defined(USE_TWAI_ASYNC) && !defined(USE_TWAI) +#define USE_TWAI +#endif // USE_TWAI_ASYNC && !USE_TWAI + +// Verify that both CAN and TWAI are not enabled. +#if defined(USE_CAN) && defined(USE_TWAI) +#error Enabling both USE_CAN and USE_TWAI is not supported. +#endif // USE_CAN && USE_TWAI + +#if defined(USE_TWAI) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,3,0) +#error Esp32HardwareTwai is not supported on this version of arduino-esp32. +#endif // USE_TWAI && IDF < v4.3 + #include "config.h" /// This is the node id to assign to this device, this must be unique @@ -102,10 +119,9 @@ OVERRIDE_CONST(gc_generate_newlines, CONSTANT_TRUE); OVERRIDE_CONST(executor_select_prescaler, 60); OVERRIDE_CONST(gridconnect_bridge_max_outgoing_packets, 2); - #endif // USE_WIFI -#if defined(USE_CAN) +#if defined(USE_TWAI) /// This is the ESP32 pin connected to the SN65HVD23x/MCP2551 R (RX) pin. /// Recommended pins: 4, 16, 21. /// Note: Any pin can be used for this other than 6-11 which are connected to @@ -122,7 +138,7 @@ constexpr gpio_num_t CAN_RX_PIN = GPIO_NUM_4; /// the GPIO pin definitions for the outputs. constexpr gpio_num_t CAN_TX_PIN = GPIO_NUM_5; -#endif // USE_CAN +#endif // USE_TWAI /// This is the primary entrypoint for the OpenMRN/LCC stack. OpenMRN openmrn(NODE_ID); @@ -141,86 +157,9 @@ static constexpr openlcb::ConfigDef cfg(0); Esp32WiFiManager wifi_mgr(ssid, password, openmrn.stack(), cfg.seg().wifi()); #endif // USE_WIFI -// Declare output pins -// NOTE: pins 6-11 are connected to the onboard flash and can not be used for -// any purpose and pins 34-39 are INPUT only. -GPIO_PIN(IO0, GpioOutputSafeLow, 15); -GPIO_PIN(IO1, GpioOutputSafeLow, 2); -GPIO_PIN(IO2, GpioOutputSafeLow, 16); -GPIO_PIN(IO3, GpioOutputSafeLow, 17); -GPIO_PIN(IO4, GpioOutputSafeLow, 13); -GPIO_PIN(IO5, GpioOutputSafeLow, 12); -GPIO_PIN(IO6, GpioOutputSafeLow, 14); -GPIO_PIN(IO7, GpioOutputSafeLow, 27); - -// List of GPIO objects that will be used for the output pins. You should keep -// the constexpr declaration, because it will produce a compile error in case -// the list of pointers cannot be compiled into a compiler constant and thus -// would be placed into RAM instead of ROM. -constexpr const Gpio *const outputGpioSet[] = { - IO0_Pin::instance(), IO1_Pin::instance(), // - IO2_Pin::instance(), IO3_Pin::instance(), // - IO4_Pin::instance(), IO5_Pin::instance(), // - IO6_Pin::instance(), IO7_Pin::instance() // -}; - -openlcb::MultiConfiguredConsumer gpio_consumers(openmrn.stack()->node(), outputGpioSet, - ARRAYSIZE(outputGpioSet), cfg.seg().consumers()); - -// Declare input pins, these are using analog pins as digital inputs -// NOTE: pins 25 and 26 can not safely be used as analog pins while -// WiFi is active. All analog pins can safely be used for digital -// inputs regardless of WiFi being active or not. -GPIO_PIN(IO8, GpioInputPU, 32); -GPIO_PIN(IO9, GpioInputPU, 33); -GPIO_PIN(IO10, GpioInputPU, 34); -GPIO_PIN(IO11, GpioInputPU, 35); -GPIO_PIN(IO12, GpioInputPU, 36); -GPIO_PIN(IO13, GpioInputPU, 39); -GPIO_PIN(IO14, GpioInputPU, 25); -GPIO_PIN(IO15, GpioInputPU, 26); - -openlcb::ConfiguredProducer IO8_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<0>(), IO8_Pin()); -openlcb::ConfiguredProducer IO9_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<1>(), IO9_Pin()); -openlcb::ConfiguredProducer IO10_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<2>(), IO10_Pin()); -openlcb::ConfiguredProducer IO11_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<3>(), IO11_Pin()); -openlcb::ConfiguredProducer IO12_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<4>(), IO12_Pin()); -openlcb::ConfiguredProducer IO13_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<5>(), IO13_Pin()); -openlcb::ConfiguredProducer IO14_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<6>(), IO14_Pin()); -openlcb::ConfiguredProducer IO15_producer( - openmrn.stack()->node(), cfg.seg().producers().entry<7>(), IO15_Pin()); - - -// Create an initializer that can initialize all the GPIO pins in one shot -typedef GpioInitializer< - IO0_Pin, IO1_Pin, IO2_Pin, IO3_Pin, // outputs 0-3 - IO4_Pin, IO5_Pin, IO6_Pin, IO7_Pin, // outputs 4-7 - IO8_Pin, IO9_Pin, IO10_Pin, IO11_Pin, // inputs 0-3 - IO12_Pin, IO13_Pin, IO14_Pin, IO15_Pin // inputs 4-7 - > GpioInit; - -// The producers need to be polled repeatedly for changes and to execute the -// debouncing algorithm. This class instantiates a refreshloop and adds the -// producers to it. -openlcb::RefreshLoop producer_refresh_loop(openmrn.stack()->node(), - { - IO8_producer.polling(), - IO9_producer.polling(), - IO10_producer.polling(), - IO11_producer.polling(), - IO12_producer.polling(), - IO13_producer.polling(), - IO14_producer.polling(), - IO15_producer.polling() - } -); +#if defined(USE_TWAI) +Esp32HardwareTwai twai(CAN_RX_PIN, CAN_TX_PIN); +#endif // USE_TWAI class FactoryResetHelper : public DefaultConfigUpdateListener { public: @@ -235,15 +174,6 @@ public: cfg.userinfo().name().write(fd, openlcb::SNIP_STATIC_DATA.model_name); cfg.userinfo().description().write( fd, "OpenLCB + Arduino-ESP32 on an ESP32."); - for(int i = 0; i < openlcb::NUM_OUTPUTS; i++) - { - cfg.seg().consumers().entry(i).description().write(fd, ""); - } - for(int i = 0; i < openlcb::NUM_INPUTS; i++) - { - cfg.seg().producers().entry(i).description().write(fd, ""); - CDI_FACTORY_RESET(cfg.seg().producers().entry(i).debounce); - } } } factory_reset_helper; @@ -315,15 +245,15 @@ void setup() openmrn.stack()->create_config_file_if_needed(cfg.seg().internal_config(), openlcb::CANONICAL_VERSION, openlcb::CONFIG_FILE_SIZE); - // initialize all declared GPIO pins - GpioInit::hw_init(); +#if defined(USE_TWAI) + twai.hw_init(); +#endif // USE_TWAI // Start the OpenMRN stack openmrn.begin(); openmrn.start_executor_thread(); cpu_log = new CpuLoadLog(openmrn.stack()->service()); - #if defined(PRINT_PACKETS) // Dump all packets as they are sent/received. // Note: This should not be enabled in deployed nodes as it will @@ -335,7 +265,13 @@ void setup() // Add the hardware CAN device as a bridge openmrn.add_can_port( new Esp32HardwareCan("esp32can", CAN_RX_PIN, CAN_TX_PIN)); -#endif // USE_CAN +#elif defined(USE_TWAI_ASYNC) + // add TWAI driver with non-blocking usage + openmrn.add_can_port_async("/dev/twai/twai0"); +#elif defined(USE_TWAI) + // add TWAI driver with select() usage + openmrn.add_can_port_select("/dev/twai/twai0"); +#endif // USE_TWAI_SELECT / USE_TWAI } void loop() diff --git a/arduino/examples/ESP32CanLoadTest/config.h b/arduino/examples/ESP32CanLoadTest/config.h index eeeb2bbc2..e9875a9fc 100644 --- a/arduino/examples/ESP32CanLoadTest/config.h +++ b/arduino/examples/ESP32CanLoadTest/config.h @@ -9,8 +9,8 @@ #include "freertos_drivers/esp32/Esp32WiFiConfiguration.hxx" // catch invalid configuration at compile time -#if !defined(USE_CAN) && !defined(USE_WIFI) -#error "Invalid configuration detected, USE_CAN or USE_WIFI must be defined." +#if !defined(USE_TWAI) && !defined(USE_WIFI) +#error "Invalid configuration detected, USE_TWAI or USE_WIFI must be defined." #endif namespace openlcb @@ -32,11 +32,11 @@ namespace openlcb extern const SimpleNodeStaticValues SNIP_STATIC_DATA = { 4, "OpenMRN", -#if defined(USE_WIFI) && !defined(USE_CAN) +#if defined(USE_WIFI) && !defined(USE_TWAI) "Arduino IO Board (WiFi)", -#elif defined(USE_CAN) && !defined(USE_WIFI) +#elif defined(USE_TWAI) && !defined(USE_WIFI) "Arduino IO Board (CAN)", -#elif defined(USE_CAN) && defined(USE_WIFI) +#elif defined(USE_TWAI) && defined(USE_WIFI) "Arduino IO Board (WiFi/CAN)", #else "Arduino IO Board", @@ -44,16 +44,6 @@ extern const SimpleNodeStaticValues SNIP_STATIC_DATA = { ARDUINO_VARIANT, "1.00"}; -constexpr uint8_t NUM_OUTPUTS = 8; -constexpr uint8_t NUM_INPUTS = 8; - -/// Declares a repeated group of a given base group and number of repeats. The -/// ProducerConfig and ConsumerConfig groups represent the configuration layout -/// needed by the ConfiguredProducer and ConfiguredConsumer classes, and come -/// from their respective hxx file. -using AllConsumers = RepeatedGroup; -using AllProducers = RepeatedGroup; - /// Modify this value every time the EEPROM needs to be cleared on the node /// after an update. static constexpr uint16_t CANONICAL_VERSION = 0x100b; @@ -64,8 +54,6 @@ CDI_GROUP(IoBoardSegment, Segment(MemoryConfigDefs::SPACE_CONFIG), Offset(128)); /// Each entry declares the name of the current entry, then the type and then /// optional arguments list. CDI_GROUP_ENTRY(internal_config, InternalConfigData); -CDI_GROUP_ENTRY(consumers, AllConsumers, Name("Outputs")); -CDI_GROUP_ENTRY(producers, AllProducers, Name("Inputs")); #if defined(USE_WIFI) CDI_GROUP_ENTRY(wifi, WiFiConfiguration, Name("WiFi Configuration")); #endif diff --git a/arduino/examples/ESP32IOBoard/ESP32IOBoard.ino b/arduino/examples/ESP32IOBoard/ESP32IOBoard.ino index a9ecfb2ba..c1d65a3e3 100644 --- a/arduino/examples/ESP32IOBoard/ESP32IOBoard.ino +++ b/arduino/examples/ESP32IOBoard/ESP32IOBoard.ino @@ -41,14 +41,23 @@ #include "openlcb/MultiConfiguredConsumer.hxx" #include "utils/GpioInitializer.hxx" -// Pick an operating mode below, if you select USE_WIFI it will expose -// this node on WIFI if you select USE_CAN, this node will be available -// on CAN. +// Pick an operating mode below, if you select USE_WIFI it will expose this +// node on WIFI. If USE_CAN / USE_TWAI / USE_TWAI_ASYNC are enabled the node +// will be available on CAN. +// // Enabling both options will allow the ESP32 to be accessible from // both WiFi and CAN interfaces. +// +// NOTE: USE_TWAI and USE_TWAI_ASYNC are similar to USE_CAN but utilize the +// new TWAI driver which offers both select() (default) or fnctl() (async) +// access. +// NOTE: USE_CAN is deprecated and no longer supported upstream by ESP-IDF as +// of v4.2 or arduino-esp32 as of v2.0.0. #define USE_WIFI -//#define USE_CAN +#define USE_CAN +//#define USE_TWAI +//#define USE_TWAI_ASYNC // uncomment the line below to have all packets printed to the Serial // output. This is not recommended for production deployment. @@ -60,6 +69,30 @@ // factory resets. //#define FACTORY_RESET_GPIO_PIN 22 +// Uncomment FIRMWARE_UPDATE_BOOTLOADER to enable the bootloader feature when +// using the TWAI device. +// +// Since many ESP32 DevKit boards do not have an on-board LED, there are no +// LED indicators enabled by default. If indicator LEDs are desired they can be +// added to the bootloader_led function at the end of this file. +// +// NOTE: in order for this to work you *MUST* use a partition schema that has +// two app partitions, typically labeled with "OTA" in the partition name in +// the Arduino IDE. +//#define FIRMWARE_UPDATE_BOOTLOADER + +#if defined(USE_TWAI_ASYNC) && !defined(USE_TWAI) +#define USE_TWAI +#endif // USE_TWAI_ASYNC && !USE_TWAI + +#if defined(USE_CAN) && defined(USE_TWAI) +#error USE_CAN and USE_TWAI are mutually exclusive! +#endif + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) && !defined(USE_TWAI) +#error FIRMWARE_UPDATE_BOOTLOADER requires USE_TWAI or USE_TWAI_SELECT. +#endif + #include "config.h" /// This is the node id to assign to this device, this must be unique @@ -96,7 +129,7 @@ const char *hostname = "esp32mrn"; #endif // USE_WIFI -#if defined(USE_CAN) +#if defined(USE_CAN) || defined(USE_TWAI) /// This is the ESP32 pin connected to the SN65HVD23x/MCP2551 R (RX) pin. /// Recommended pins: 4, 16, 21. /// Note: Any pin can be used for this other than 6-11 which are connected to @@ -113,7 +146,13 @@ constexpr gpio_num_t CAN_RX_PIN = GPIO_NUM_4; /// the GPIO pin definitions for the outputs. constexpr gpio_num_t CAN_TX_PIN = GPIO_NUM_5; -#endif // USE_CAN +#endif // USE_CAN || USE_TWAI + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) +// Include the Bootloader HAL implementation for the ESP32. This should only +// be included in one ino/cpp file. +#include "freertos_drivers/esp32/Esp32BootloaderHal.hxx" +#endif // FIRMWARE_UPDATE_BOOTLOADER /// This is the primary entrypoint for the OpenMRN/LCC stack. OpenMRN openmrn(NODE_ID); @@ -132,6 +171,10 @@ static constexpr openlcb::ConfigDef cfg(0); Esp32WiFiManager wifi_mgr(ssid, password, openmrn.stack(), cfg.seg().wifi()); #endif // USE_WIFI +#if defined(USE_TWAI) && !defined(USE_CAN) +Esp32HardwareTwai twai(CAN_RX_PIN, CAN_TX_PIN); +#endif // USE_TWAI && !USE_CAN + // Declare output pins // NOTE: pins 6-11 are connected to the onboard flash and can not be used for // any purpose and pins 34-39 are INPUT only. @@ -231,7 +274,7 @@ public: { cfg.userinfo().name().write(fd, openlcb::SNIP_STATIC_DATA.model_name); cfg.userinfo().description().write( - fd, "OpenLCB + Arduino-ESP32 on an ESP32."); + fd, openlcb::SNIP_STATIC_DATA.model_name); for(int i = 0; i < openlcb::NUM_OUTPUTS; i++) { cfg.seg().consumers().entry(i).description().write(fd, ""); @@ -263,28 +306,8 @@ namespace openlcb extern const char *const SNIP_DYNAMIC_FILENAME = CONFIG_FILENAME; } -void setup() +void check_for_factory_reset() { - Serial.begin(115200L); - - // Initialize the SPIFFS filesystem as our persistence layer - if (!SPIFFS.begin()) - { - printf("SPIFFS failed to mount, attempting to format and remount\n"); - if (!SPIFFS.begin(true)) - { - printf("SPIFFS mount failed even with format, giving up!\n"); - while (1) - { - // Unable to start SPIFFS successfully, give up and wait - // for WDT to kick in - } - } - } - - // initialize all declared GPIO pins - GpioInit::hw_init(); - #if defined(FACTORY_RESET_GPIO_PIN) // Check the factory reset pin which should normally read HIGH (set), if it // reads LOW (clr) delete the cdi.xml and openlcb_config @@ -311,6 +334,58 @@ void setup() } } #endif // FACTORY_RESET_GPIO_PIN +} + +void setup() +{ + Serial.begin(115200L); + uint8_t reset_reason = Esp32SocInfo::print_soc_info(); + LOG(INFO, "[Node] ID: %s", uint64_to_string_hex(NODE_ID).c_str()); + LOG(INFO, "[SNIP] version:%d, manufacturer:%s, model:%s, hw-v:%s, sw-v:%s", + openlcb::SNIP_STATIC_DATA.version, + openlcb::SNIP_STATIC_DATA.manufacturer_name, + openlcb::SNIP_STATIC_DATA.model_name, + openlcb::SNIP_STATIC_DATA.hardware_version, + openlcb::SNIP_STATIC_DATA.software_version); + + // Initialize the SPIFFS filesystem as our persistence layer + if (!SPIFFS.begin()) + { + printf("SPIFFS failed to mount, attempting to format and remount\n"); + if (!SPIFFS.begin(true)) + { + printf("SPIFFS mount failed even with format, giving up!\n"); + while (1) + { + // Unable to start SPIFFS successfully, give up and wait + // for WDT to kick in + } + } + } + + // initialize all declared GPIO pins + GpioInit::hw_init(); + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) + // initialize the bootloader. + esp32_bootloader_init(reset_reason); + + // if we have a request to enter the bootloader we need to process it + // before we startup the OpenMRN stack. + if (request_bootloader()) + { + esp32_bootloader_run(NODE_ID, TWAI_RX_PIN, TWAI_TX_PIN); + // This line should not be reached as the esp32_bootloader_run method + // will not return by default. + HASSERT(false); + } +#endif // FIRMWARE_UPDATE_BOOTLOADER + +#if defined(USE_TWAI) && !defined(USE_CAN) + twai.hw_init(); +#endif // USE_TWAI && !USE_CAN + + check_for_factory_reset(); // Create the CDI.xml dynamically openmrn.create_config_descriptor_xml(cfg, openlcb::CDI_FILENAME); @@ -321,8 +396,13 @@ void setup() // Start the OpenMRN stack openmrn.begin(); - openmrn.start_executor_thread(); - + if (reset_reason == RTCWDT_BROWN_OUT_RESET) + { + openmrn.stack()->executor()->add(new CallbackExecutable([]() + { + openmrn.stack()->send_event(openlcb::Defs::NODE_POWER_BROWNOUT_EVENT); + })); + } #if defined(PRINT_PACKETS) // Dump all packets as they are sent/received. // Note: This should not be enabled in deployed nodes as it will @@ -334,8 +414,13 @@ void setup() // Add the hardware CAN device as a bridge openmrn.add_can_port( new Esp32HardwareCan("esp32can", CAN_RX_PIN, CAN_TX_PIN)); +#elif defined(USE_TWAI_ASYNC) + openmrn.add_can_port_async("/dev/twai/twai0"); +#elif defined(USE_TWAI) + openmrn.add_can_port_select("/dev/twai/twai0"); #endif // USE_CAN + openmrn.start_executor_thread(); } void loop() @@ -344,3 +429,21 @@ void loop() // as possible from the loop() method. openmrn.loop(); } + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) + +extern "C" +{ + +/// Updates the state of a status LED. +/// +/// @param led is the LED to update. +/// @param value is the new state of the LED. +void bootloader_led(enum BootloaderLed led, bool value) +{ + LOG(INFO, "[Bootloader] bootloader_led(%d, %d)", led, value); +} + +} // extern "C" + +#endif // FIRMWARE_UPDATE_BOOTLOADER \ No newline at end of file diff --git a/arduino/examples/ESP32IOBoard/config.h b/arduino/examples/ESP32IOBoard/config.h index 78bad363d..b8e292a27 100644 --- a/arduino/examples/ESP32IOBoard/config.h +++ b/arduino/examples/ESP32IOBoard/config.h @@ -32,12 +32,16 @@ namespace openlcb extern const SimpleNodeStaticValues SNIP_STATIC_DATA = { 4, "OpenMRN", -#if defined(USE_WIFI) && !defined(USE_CAN) +#if defined(USE_WIFI) && !defined(USE_CAN) && !defined(USE_TWAI) "Arduino IO Board (WiFi)", #elif defined(USE_CAN) && !defined(USE_WIFI) "Arduino IO Board (CAN)", +#elif defined(USE_TWAI) && !defined(USE_WIFI) + "Arduino IO Board (TWAI)", #elif defined(USE_CAN) && defined(USE_WIFI) "Arduino IO Board (WiFi/CAN)", +#elif defined(USE_TWAI) && defined(USE_WIFI) + "Arduino IO Board (WiFi/TWAI)", #else "Arduino IO Board", #endif @@ -64,8 +68,8 @@ CDI_GROUP(IoBoardSegment, Segment(MemoryConfigDefs::SPACE_CONFIG), Offset(128)); /// Each entry declares the name of the current entry, then the type and then /// optional arguments list. CDI_GROUP_ENTRY(internal_config, InternalConfigData); -CDI_GROUP_ENTRY(consumers, AllConsumers, Name("Outputs")); -CDI_GROUP_ENTRY(producers, AllProducers, Name("Inputs")); +CDI_GROUP_ENTRY(consumers, AllConsumers, Name("Outputs"), RepName("Output")); +CDI_GROUP_ENTRY(producers, AllProducers, Name("Inputs"), RepName("Input")); #if defined(USE_WIFI) CDI_GROUP_ENTRY(wifi, WiFiConfiguration, Name("WiFi Configuration")); #endif diff --git a/arduino/examples/ESP32S2CanLoadTest/.gitignore b/arduino/examples/ESP32S2CanLoadTest/.gitignore new file mode 100644 index 000000000..70cfab999 --- /dev/null +++ b/arduino/examples/ESP32S2CanLoadTest/.gitignore @@ -0,0 +1 @@ +wifi_params.cpp diff --git a/arduino/examples/ESP32S2CanLoadTest/ESP32S2CanLoadTest.ino b/arduino/examples/ESP32S2CanLoadTest/ESP32S2CanLoadTest.ino new file mode 100644 index 000000000..076519176 --- /dev/null +++ b/arduino/examples/ESP32S2CanLoadTest/ESP32S2CanLoadTest.ino @@ -0,0 +1,333 @@ +/** \copyright + * Copyright (c) 2019, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file ESP32S2CanLoadTest.ino + * + * Main file for the ESP32-S2 CAN Load Test application. + * + * @author Mike Dunston + * @date 2 May 2021 + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +// Pick an operating mode below, if you select USE_WIFI it will expose +// this node on WIFI if you select USE_TWAI this node will be available on +// TWAI (CAN). +// Enabling both options will allow the ESP32 to be accessible from +// both WiFi and TWAI (CAN) interfaces. + +#define USE_WIFI +//#define USE_TWAI + +// Uncomment USE_TWAI_ASYNC to enable the usage of fnctl() for the TWAI +// interface. +//#define USE_TWAI_ASYNC + +// uncomment the line below to have all packets printed to the Serial +// output. This is not recommended for production deployment. +//#define PRINT_PACKETS + +// Uncomment the line below to configure the native USB CDC for all output from +// OpenMRNLite. When not defined the default UART0 will be used instead. +//#define USE_USB_CDC_OUTPUT + +// Configuration option validation + +// If USE_TWAI_ASYNC is enabled but USE_TWAI is not, enable USE_TWAI. +#if defined(USE_TWAI_ASYNC) && !defined(USE_TWAI) +#define USE_TWAI +#endif // USE_TWAI_ASYNC && !USE_TWAI + +#if ARDUINO_USB_CDC_ON_BOOT && defined(USE_USB_CDC_OUTPUT) +// When ARDUINO_USB_CDC_ON_BOOT = 1 "Serial" will map to USB-CDC automatically +// and will be enabled on startup. We do not need to wrap or otherwise treat it +// any differently. +#undef USE_USB_CDC_OUTPUT +#warning Disabling USE_USB_CDC_OUTPUT since USB-CDC is enabled in Arduino IDE +#endif + +#include "config.h" + +/// This is the node id to assign to this device, this must be unique +/// on the CAN bus. +static constexpr uint64_t NODE_ID = UINT64_C(0x05010101182e); + +#if defined(USE_WIFI) +// Configuring WiFi accesspoint name and password +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// There are two options: +// 1) edit the sketch to set this information just below. Use quotes: +// const char* ssid = "linksys"; +// const char* password = "superSecret"; +// 2) add a new file to the sketch folder called something.cpp with the +// following contents: +// #include +// +// char WIFI_SSID[] = "linksys"; +// char WIFI_PASS[] = "theTRUEsupers3cr3t"; + +// This is the name of the WiFi network (access point) to connect to. +const char *ssid = WIFI_SSID; + +// Password of the wifi network. +const char *password = WIFI_PASS; + +// This is the hostname which the ESP32 will advertise via mDNS, it should be +// unique. +const char *hostname = "esp32mrn"; + +OVERRIDE_CONST(gridconnect_buffer_size, 3512); +OVERRIDE_CONST(gridconnect_buffer_delay_usec, 2000); +OVERRIDE_CONST(gc_generate_newlines, CONSTANT_TRUE); +OVERRIDE_CONST(executor_select_prescaler, 60); +OVERRIDE_CONST(gridconnect_bridge_max_outgoing_packets, 2); + +#endif // USE_WIFI + +#if defined(USE_TWAI) +// This is the ESP32-S2 pin connected to the SN65HVD23x/MCP2551 R (RX) pin. +// Recommended pins: 40, 41. +// Note: Any pin can be used for this other than 26-32 which are connected to +// the onboard flash. +// Note: If you are using a pin other than 40 you will likely need to adjust +// the GPIO pin definitions for the outputs. +constexpr gpio_num_t CAN_RX_PIN = GPIO_NUM_40; + +// This is the ESP32 pin connected to the SN65HVD23x/MCP2551 D (TX) pin. +// Recommended pins: 40, 41. +// Note: Any pin can be used for this other than 26-32 which are connected to +// the onboard flash. +// Note: If you are using a pin other than 41 you will likely need to adjust +// the GPIO pin definitions for the outputs. +constexpr gpio_num_t CAN_TX_PIN = GPIO_NUM_41; + +#endif // USE_TWAI + +// This is the primary entrypoint for the OpenMRN/LCC stack. +OpenMRN openmrn(NODE_ID); + +// note the dummy string below is required due to a bug in the GCC compiler +// for the ESP32 +string dummystring("abcdef"); + +// This tracks the CPU usage of the ESP32-S2 through the usage of a hardware +// timer that records what the CPU is currently executing roughly 163 times per +// second. +CpuLoad cpu_load; + +// This reports the usage to the console output. +CpuLoadLog cpu_log(openmrn.stack()->service()); + +// ConfigDef comes from config.h and is specific to this particular device and +// target. It defines the layout of the configuration memory space and is also +// used to generate the cdi.xml file. Here we instantiate the configuration +// layout. The argument of offset zero is ignored and will be removed later. +static constexpr openlcb::ConfigDef cfg(0); + +#if defined(USE_WIFI) +Esp32WiFiManager wifi_mgr(ssid, password, openmrn.stack(), cfg.seg().wifi()); +#endif // USE_WIFI + +#if defined(USE_TWAI) +Esp32HardwareTwai twai(CAN_RX_PIN, CAN_TX_PIN); +#endif // USE_TWAI + +// This will perform the factory reset procedure for this node's configuration +// items. +// +// The node name and description will be set to the SNIP model name field +// value. +// Descriptions for intputs and outputs will be set to a blank string, input +// debounce parameters will be set to default values. +class FactoryResetHelper : public DefaultConfigUpdateListener { +public: + UpdateAction apply_configuration(int fd, bool initial_load, + BarrierNotifiable *done) OVERRIDE { + AutoNotify n(done); + return UPDATED; + } + + void factory_reset(int fd) override + { + cfg.userinfo().name().write(fd, openlcb::SNIP_STATIC_DATA.model_name); + cfg.userinfo().description().write( + fd, openlcb::SNIP_STATIC_DATA.model_name); + } +} factory_reset_helper; + +namespace openlcb +{ + // Name of CDI.xml to generate dynamically. + const char CDI_FILENAME[] = "/spiffs/cdi.xml"; + + // This will stop openlcb from exporting the CDI memory space upon start. + extern const char CDI_DATA[] = ""; + + // Path to where OpenMRN should persist general configuration data. + extern const char *const CONFIG_FILENAME = "/spiffs/openlcb_config"; + + // The size of the memory space to export over the above device. + extern const size_t CONFIG_FILE_SIZE = cfg.seg().size() + cfg.seg().offset(); + + // Default to store the dynamic SNIP data is stored in the same persistant + // data file as general configuration data. + extern const char *const SNIP_DYNAMIC_FILENAME = CONFIG_FILENAME; +} + +// Callback function for the hardware timer configured to fire roughly 163 +// times per second. +void ARDUINO_ISR_ATTR record_cpu_usage() +{ +#if CONFIG_ARDUINO_ISR_IRAM + // if the ISR is called with flash disabled we can not safely recored the + // cpu usage. + if (!spi_flash_cache_enabled()) + { + return; + } +#endif + // Retrieves the vtable pointer from the currently running executable. + unsigned *pp = (unsigned *)openmrn.stack()->executor()->current(); + cpuload_tick(pp ? pp[0] | 1 : 0); +} + +#if defined(USE_USB_CDC_OUTPUT) +// USB CDC wrapper that uses TinyUSB internally to route data to/from the USB +// CDC device driver. +USBCDC USBSerial; + +// Override the log_output method from OpenMRNLite to redirect all log output +// via USB CDC. +void log_output(char* buf, int size) +{ + if (size <= 0) return; + buf[size] = '\0'; + USBSerial.println(buf); +} +#endif // USE_USB_CDC_OUTPUT + +void setup() +{ +#if !defined(USE_USB_CDC_OUTPUT) + Serial.begin(115200L); +#else + USB.productName(openlcb::SNIP_STATIC_DATA.model_name); + USB.manufacturerName(openlcb::SNIP_STATIC_DATA.manufacturer_name); + USB.firmwareVersion(openlcb::CANONICAL_VERSION); + USB.serialNumber(uint64_to_string_hex(NODE_ID).c_str()); + USB.begin(); + USBSerial.begin(); + USBSerial.setDebugOutput(true); + // Give time for the USB peripheral to startup and be ready to use. + delay(5000); +#endif // USE_USB_CDC_OUTPUT + + Esp32SocInfo::print_soc_info(); + LOG(INFO, "[Node] ID: %s", uint64_to_string_hex(NODE_ID).c_str()); + LOG(INFO, "[SNIP] version:%d, manufacturer:%s, model:%s, hw-v:%s, sw-v:%s" + , openlcb::SNIP_STATIC_DATA.version + , openlcb::SNIP_STATIC_DATA.manufacturer_name + , openlcb::SNIP_STATIC_DATA.model_name + , openlcb::SNIP_STATIC_DATA.hardware_version + , openlcb::SNIP_STATIC_DATA.software_version); + + // Register hardware timer zero to use a 1Mhz resolution and to count up + // from zero when the timer triggers. + auto timer = timerBegin(0, 80, true); + // Attach our callback function to be called on the timer edge signal. + timerAttachInterrupt(timer, record_cpu_usage, true); + // Configure the trigger point to be roughly 163 times per second. + timerAlarmWrite(timer, 1000000/163, true); + // Enable the timer. + timerAlarmEnable(timer); + + // Initialize the SPIFFS filesystem as our persistence layer + if (!SPIFFS.begin()) + { + LOG(WARNING, + "SPIFFS failed to mount, attempting to format and remount"); + if (!SPIFFS.begin(true)) + { + LOG_ERROR("SPIFFS mount failed even with format, giving up!"); + while (1) + { + // Unable to start SPIFFS successfully, give up and wait + // for WDT to kick in + } + } + } + + // Create the CDI.xml dynamically + openmrn.create_config_descriptor_xml(cfg, openlcb::CDI_FILENAME); + + // Create the default internal configuration file + openmrn.stack()->create_config_file_if_needed(cfg.seg().internal_config(), + openlcb::CANONICAL_VERSION, openlcb::CONFIG_FILE_SIZE); + +#if defined(USE_TWAI) + twai.hw_init(); +#endif // USE_TWAI + + // Start the OpenMRN stack + openmrn.begin(); + +#if defined(PRINT_PACKETS) + // Dump all packets as they are sent/received. + // Note: This should not be enabled in deployed nodes as it will + // have performance impact. + openmrn.stack()->print_all_packets(); +#endif // PRINT_PACKETS + +#if defined(USE_TWAI_ASYNC) + // add TWAI driver with non-blocking usage + openmrn.add_can_port_async("/dev/twai/twai0"); +#elif defined(USE_TWAI) + // add TWAI driver with select() usage + openmrn.add_can_port_select("/dev/twai/twai0"); + + // start executor thread since this is required for select() to work in the + // OpenMRN executor. + openmrn.start_executor_thread(); +#endif // USE_TWAI_SELECT +} + +void loop() +{ + // Call the OpenMRN executor, this needs to be done as often + // as possible from the loop() method. + openmrn.loop(); +} diff --git a/arduino/examples/ESP32S2CanLoadTest/config.h b/arduino/examples/ESP32S2CanLoadTest/config.h new file mode 100644 index 000000000..d79df9023 --- /dev/null +++ b/arduino/examples/ESP32S2CanLoadTest/config.h @@ -0,0 +1,76 @@ +#ifndef _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ +#define _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ + +#include "openlcb/ConfiguredConsumer.hxx" +#include "openlcb/ConfiguredProducer.hxx" +#include "openlcb/ConfigRepresentation.hxx" +#include "openlcb/MemoryConfig.hxx" + +#include "freertos_drivers/esp32/Esp32WiFiConfiguration.hxx" + +// catch invalid configuration at compile time +#if !defined(USE_TWAI) && !defined(USE_WIFI) +#error "Invalid configuration detected, USE_CAN or USE_WIFI must be defined." +#endif + +namespace openlcb +{ + +/// Defines the identification information for the node. The arguments are: +/// +/// - 4 (version info, always 4 by the standard +/// - Manufacturer name +/// - Model name +/// - Hardware version +/// - Software version +/// +/// This data will be used for all purposes of the identification: +/// +/// - the generated cdi.xml will include this data +/// - the Simple Node Ident Info Protocol will return this data +/// - the ACDI memory space will contain this data. +extern const SimpleNodeStaticValues SNIP_STATIC_DATA = { + 4, + "OpenMRN", +#if defined(USE_WIFI) && !defined(USE_TWAI) + "Arduino Load Test (WiFi)", +#elif defined(USE_TWAI) && !defined(USE_WIFI) + "Arduino Load Test (TWAI)", +#else + "Arduino Load Test (WiFi/TWAI)", +#endif + ARDUINO_VARIANT, + "1.00"}; + +/// Modify this value every time the EEPROM needs to be cleared on the node +/// after an update. +static constexpr uint16_t CANONICAL_VERSION = 0x100b; + +/// Defines the main segment in the configuration CDI. This is laid out at +/// origin 128 to give space for the ACDI user data at the beginning. +CDI_GROUP(IoBoardSegment, Segment(MemoryConfigDefs::SPACE_CONFIG), Offset(128)); +/// Each entry declares the name of the current entry, then the type and then +/// optional arguments list. +CDI_GROUP_ENTRY(internal_config, InternalConfigData); +#if defined(USE_WIFI) +CDI_GROUP_ENTRY(wifi, WiFiConfiguration, Name("WiFi Configuration")); +#endif +CDI_GROUP_END(); + +/// The main structure of the CDI. ConfigDef is the symbol we use in main.cxx +/// to refer to the configuration defined here. +CDI_GROUP(ConfigDef, MainCdi()); +/// Adds the tag with the values from SNIP_STATIC_DATA above. +CDI_GROUP_ENTRY(ident, Identification); +/// Adds an tag. +CDI_GROUP_ENTRY(acdi, Acdi); +/// Adds a segment for changing the values in the ACDI user-defined +/// space. UserInfoSegment is defined in the system header. +CDI_GROUP_ENTRY(userinfo, UserInfoSegment, Name("User Info")); +/// Adds the main configuration segment. +CDI_GROUP_ENTRY(seg, IoBoardSegment, Name("Settings")); +CDI_GROUP_END(); + +} // namespace openlcb + +#endif // _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ diff --git a/arduino/examples/ESP32S2IOBoard/.gitignore b/arduino/examples/ESP32S2IOBoard/.gitignore new file mode 100644 index 000000000..70cfab999 --- /dev/null +++ b/arduino/examples/ESP32S2IOBoard/.gitignore @@ -0,0 +1 @@ +wifi_params.cpp diff --git a/arduino/examples/ESP32S2IOBoard/ESP32S2IOBoard.ino b/arduino/examples/ESP32S2IOBoard/ESP32S2IOBoard.ino new file mode 100644 index 000000000..9ff7c3681 --- /dev/null +++ b/arduino/examples/ESP32S2IOBoard/ESP32S2IOBoard.ino @@ -0,0 +1,523 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file ESP32S2IOBoard.ino + * + * Main file for the io board application on an ESP32-S2. + * + * @author Mike Dunston + * @date 2 May 2021 + */ + +#include +#include +#include +#include + +#include +#include "freertos_drivers/arduino/CpuLoad.hxx" +#include "openlcb/MultiConfiguredConsumer.hxx" +#include "openlcb/TcpDefs.hxx" +#include "utils/GpioInitializer.hxx" + +// Pick an operating mode below, if you select USE_WIFI it will expose this +// node on WIFI. If USE_CAN / USE_TWAI / USE_TWAI_ASYNC are enabled the node +// will be available on CAN. +// +// Enabling both options will allow the ESP32 to be accessible from +// both WiFi and CAN interfaces. +// +// NOTE: USE_TWAI and USE_TWAI_ASYNC are similar to USE_CAN but utilize the +// new TWAI driver which offers both select() (default) or fnctl() (async) +// access. + +#define USE_WIFI +//#define USE_TWAI +//#define USE_TWAI_ASYNC + +// Uncomment the line below to have all packets printed to the Serial +// output. This is not recommended for production deployment. +//#define PRINT_PACKETS + +// uncomment the line below to specify a GPIO pin that should be used to force +// a factory reset when the node starts and the GPIO pin reads LOW. +// NOTE: GPIO 15 is also used for IO16, care must be taken to ensure that this +// GPIO pin is not used both for FACTORY_RESET and an OUTPUT pin. +//#define FACTORY_RESET_GPIO_PIN 15 + +// Uncomment the line below to configure the native USB CDC for all output from +// OpenMRNLite. When not defined the default UART0 will be used instead. +// +// NOTE: USB CDC is connected to GPIO 19 (D-) and 20 (D+) and can not be +// changed to any other pins. +//#define USE_USB_CDC_OUTPUT + +// Uncomment FIRMWARE_UPDATE_BOOTLOADER to enable the bootloader feature when +// using the TWAI device. +// +// NOTE: in order for this to work you *MUST* use a partition schema that has +// two app partitions, typically labeled with "OTA" in the partition name in +// the Arduino IDE. +//#define FIRMWARE_UPDATE_BOOTLOADER + +// Configuration option validation + +#if defined(USE_TWAI_ASYNC) && defined(USE_TWAI) +#error USE_TWAI_ASYNC and USE_TWAI are mutually exclusive! +#endif + +#if defined(USE_TWAI_ASYNC) && !defined(USE_TWAI) +#define USE_TWAI +#endif // USE_TWAI_ASYNC && !USE_TWAI + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) && !defined(USE_TWAI) +#error Firmware update is only supported via TWAI, enable USE_TWAI to use this. +#endif + +#if ARDUINO_USB_CDC_ON_BOOT && defined(USE_USB_CDC_OUTPUT) +// When ARDUINO_USB_CDC_ON_BOOT = 1 "Serial" will map to USB-CDC automatically +// and will be enabled on startup. We do not need to wrap or otherwise treat it +// any differently. +#undef USE_USB_CDC_OUTPUT +#warning Disabling USE_USB_CDC_OUTPUT since USB-CDC is enabled in Arduino IDE +#endif + +#include "config.h" + +/// This is the node id to assign to this device, this must be unique +/// on the CAN bus. +static constexpr uint64_t NODE_ID = UINT64_C(0x05010101182f); + +#if defined(USE_WIFI) +// Configuring WiFi accesspoint name and password +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +// There are two options: +// 1) edit the sketch to set this information just below. Use quotes: +// const char* ssid = "linksys"; +// const char* password = "superSecret"; +// 2) add a new file to the sketch folder called something.cpp with the +// following contents: +// #include +// +// char WIFI_SSID[] = "linksys"; +// char WIFI_PASS[] = "theTRUEsupers3cr3t"; + +/// This is the name of the WiFi network (access point) to connect to. +const char *ssid = WIFI_SSID; + +/// Password of the wifi network. +const char *password = WIFI_PASS; + +/// This is the hostname which the ESP32 will advertise via mDNS, it should be +/// unique. +const char *hostname = "esp32mrn"; + +OVERRIDE_CONST(gridconnect_buffer_size, 3512); +//OVERRIDE_CONST(gridconnect_buffer_delay_usec, 200000); +OVERRIDE_CONST(gridconnect_buffer_delay_usec, 2000); +OVERRIDE_CONST(gc_generate_newlines, CONSTANT_TRUE); +OVERRIDE_CONST(executor_select_prescaler, 60); +OVERRIDE_CONST(gridconnect_bridge_max_outgoing_packets, 2); + +#endif // USE_WIFI + +#if defined(USE_TWAI) +/// This is the ESP32-S2 pin connected to the SN65HVD23x/MCP2551 R (RX) pin. +/// Recommended pins: 40, 41, 42. +/// NOTE: If you are using a pin other than 40 you will likely need to adjust +/// the GPIO pin definitions for the outputs. +constexpr gpio_num_t CAN_RX_PIN = GPIO_NUM_40; + +/// This is the ESP32 pin connected to the SN65HVD23x/MCP2551 D (TX) pin. +/// Recommended pins: 40, 41, 42. +/// NOTE: If you are using a pin other than 41 you will likely need to adjust +/// the GPIO pin definitions for the outputs. +constexpr gpio_num_t CAN_TX_PIN = GPIO_NUM_41; + +#endif // USE_CAN or USE_TWAI + +#if defined(FACTORY_RESET_GPIO_PIN) +static constexpr uint8_t FACTORY_RESET_COUNTDOWN_SECS = 10; +#endif // FACTORY_RESET_GPIO_PIN + +/// This is the primary entrypoint for the OpenMRN/LCC stack. +OpenMRN openmrn(NODE_ID); + +/// ConfigDef comes from config.h and is specific to this particular device and +/// target. It defines the layout of the configuration memory space and is also +/// used to generate the cdi.xml file. Here we instantiate the configuration +/// layout. The argument of offset zero is ignored and will be removed later. +static constexpr openlcb::ConfigDef cfg(0); + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) +// Include the Bootloader HAL implementation for the ESP32. This should only +// be included in one ino/cpp file. +#include "freertos_drivers/esp32/Esp32BootloaderHal.hxx" +#endif // FIRMWARE_UPDATE_BOOTLOADER + +#if defined(USE_WIFI) +Esp32WiFiManager wifi_mgr(ssid, password, openmrn.stack(), cfg.seg().wifi()); +#endif // USE_WIFI + +#if defined(USE_TWAI) +Esp32HardwareTwai twai(CAN_RX_PIN, CAN_TX_PIN); +#endif // USE_TWAI + +// Declare output pins. +GPIO_PIN(IO0, GpioOutputSafeLow, 0); +GPIO_PIN(IO1, GpioOutputSafeLow, 1); +GPIO_PIN(IO2, GpioOutputSafeLow, 2); +GPIO_PIN(IO3, GpioOutputSafeLow, 3); +GPIO_PIN(IO4, GpioOutputSafeLow, 4); +GPIO_PIN(IO5, GpioOutputSafeLow, 5); +GPIO_PIN(IO6, GpioOutputSafeLow, 6); +GPIO_PIN(IO7, GpioOutputSafeLow, 7); +GPIO_PIN(IO8, GpioOutputSafeLow, 8); +GPIO_PIN(IO9, GpioOutputSafeLow, 9); +GPIO_PIN(IO10, GpioOutputSafeLow, 10); +GPIO_PIN(IO11, GpioOutputSafeLow, 11); +GPIO_PIN(IO12, GpioOutputSafeLow, 12); +GPIO_PIN(IO13, GpioOutputSafeLow, 45); + +// Declare input pins +// Notes: +// GPIO 18 is reserved for the status LED. +// GPIO 19 and 20 are intentionally skipped as they are reserved for native USB. +// GPIO 43 and 44 are skipped as they are connected to UART0. +// GPIO 40 and 41 are intentionally skipped as they are reserved for TWAI. +GPIO_PIN(IO14, GpioInputPU, 13); +GPIO_PIN(IO15, GpioInputPU, 14); +GPIO_PIN(IO16, GpioInputPU, 15); +GPIO_PIN(IO17, GpioInputPU, 16); +GPIO_PIN(IO18, GpioInputPU, 17); +GPIO_PIN(IO19, GpioInputPU, 21); +GPIO_PIN(IO20, GpioInputPU, 33); +GPIO_PIN(IO21, GpioInputPU, 34); +GPIO_PIN(IO22, GpioInputPU, 35); +GPIO_PIN(IO23, GpioInputPU, 36); +GPIO_PIN(IO24, GpioInputPU, 37); +GPIO_PIN(IO25, GpioInputPU, 38); +GPIO_PIN(IO26, GpioInputPU, 39); +GPIO_PIN(IO27, GpioInputPU, 42); + +// List of GPIO objects that will be used for the output pins. You should keep +// the constexpr declaration, because it will produce a compile error in case +// the list of pointers cannot be compiled into a compiler constant and thus +// would be placed into RAM instead of ROM. +constexpr const Gpio *const outputGpioSet[] = { + IO0_Pin::instance(), IO1_Pin::instance(), // + IO2_Pin::instance(), IO3_Pin::instance(), // + IO4_Pin::instance(), IO5_Pin::instance(), // + IO6_Pin::instance(), IO7_Pin::instance(), // + IO8_Pin::instance(), IO9_Pin::instance(), // + IO10_Pin::instance(), IO11_Pin::instance(), // + IO12_Pin::instance(), IO13_Pin::instance(), // +}; + +openlcb::MultiConfiguredConsumer gpio_consumers(openmrn.stack()->node(), outputGpioSet, + ARRAYSIZE(outputGpioSet), cfg.seg().consumers()); + +openlcb::ConfiguredProducer IO14_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<0>(), IO14_Pin()); +openlcb::ConfiguredProducer IO15_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<1>(), IO15_Pin()); +openlcb::ConfiguredProducer IO16_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<2>(), IO16_Pin()); +openlcb::ConfiguredProducer IO17_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<3>(), IO17_Pin()); +openlcb::ConfiguredProducer IO18_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<4>(), IO18_Pin()); +openlcb::ConfiguredProducer IO19_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<5>(), IO19_Pin()); +openlcb::ConfiguredProducer IO20_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<6>(), IO20_Pin()); +openlcb::ConfiguredProducer IO21_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<7>(), IO21_Pin()); +openlcb::ConfiguredProducer IO22_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<8>(), IO22_Pin()); +openlcb::ConfiguredProducer IO23_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<9>(), IO23_Pin()); +openlcb::ConfiguredProducer IO24_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<10>(), IO24_Pin()); +openlcb::ConfiguredProducer IO25_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<11>(), IO25_Pin()); +openlcb::ConfiguredProducer IO26_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<12>(), IO26_Pin()); +openlcb::ConfiguredProducer IO27_producer( + openmrn.stack()->node(), cfg.seg().producers().entry<13>(), IO27_Pin()); + +// Create an initializer that can initialize all the GPIO pins in one shot +typedef GpioInitializer< +#if defined(FACTORY_RESET_GPIO_PIN) + FACTORY_RESET_Pin, // factory reset +#endif // FACTORY_RESET_GPIO_PIN + IO0_Pin, IO1_Pin, IO2_Pin, IO3_Pin, // outputs 0-3 + IO4_Pin, IO5_Pin, IO6_Pin, IO7_Pin, // outputs 4-7 + IO8_Pin, IO9_Pin, IO10_Pin, IO11_Pin, // outputs 8-11 + IO12_Pin, IO13_Pin, // outputs 12-13 + IO14_Pin, IO15_Pin, IO16_Pin, IO17_Pin, // inputs 0-3 + IO18_Pin, IO19_Pin, IO20_Pin, IO21_Pin, // inputs 4-7 + IO22_Pin, IO23_Pin, IO24_Pin, IO25_Pin, // inputs 8-11 + IO26_Pin, IO27_Pin // inputs 12-13 + > GpioInit; + +// The producers need to be polled repeatedly for changes and to execute the +// debouncing algorithm. This class instantiates a refreshloop and adds the +// producers to it. +openlcb::RefreshLoop producer_refresh_loop(openmrn.stack()->node(), + { + IO14_producer.polling(), + IO15_producer.polling(), + IO16_producer.polling(), + IO17_producer.polling(), + IO18_producer.polling(), + IO19_producer.polling(), + IO20_producer.polling(), + IO21_producer.polling(), + IO22_producer.polling(), + IO23_producer.polling(), + IO24_producer.polling(), + IO25_producer.polling(), + IO26_producer.polling(), + IO27_producer.polling() + } +); + +class FactoryResetHelper : public DefaultConfigUpdateListener { +public: + UpdateAction apply_configuration(int fd, bool initial_load, + BarrierNotifiable *done) OVERRIDE { + AutoNotify n(done); + return UPDATED; + } + + void factory_reset(int fd) override + { + cfg.userinfo().name().write(fd, openlcb::SNIP_STATIC_DATA.model_name); + cfg.userinfo().description().write( + fd, "OpenLCB + Arduino-ESP32 on an " ARDUINO_VARIANT); + for(int i = 0; i < openlcb::NUM_OUTPUTS; i++) + { + cfg.seg().consumers().entry(i).description().write(fd, ""); + } + for(int i = 0; i < openlcb::NUM_INPUTS; i++) + { + cfg.seg().producers().entry(i).description().write(fd, ""); + CDI_FACTORY_RESET(cfg.seg().producers().entry(i).debounce); + } + } +} factory_reset_helper; + +namespace openlcb +{ + // Name of CDI.xml to generate dynamically. + const char CDI_FILENAME[] = "/spiffs/cdi.xml"; + + // This will stop openlcb from exporting the CDI memory space upon start. + extern const char CDI_DATA[] = ""; + + // Path to where OpenMRN should persist general configuration data. + extern const char *const CONFIG_FILENAME = "/spiffs/openlcb_config"; + + // The size of the memory space to export over the above device. + extern const size_t CONFIG_FILE_SIZE = cfg.seg().size() + cfg.seg().offset(); + + // Default to store the dynamic SNIP data is stored in the same persistant + // data file as general configuration data. + extern const char *const SNIP_DYNAMIC_FILENAME = CONFIG_FILENAME; +} + +#if defined(USE_USB_CDC_OUTPUT) +// USB CDC wrapper that uses TinyUSB internally to route data to/from the USB +// CDC device driver. +USBCDC USBSerial; + +// Override the log_output method from OpenMRNLite to redirect all log output +// via USB CDC. +void log_output(char* buf, int size) +{ + if (size <= 0) return; + buf[size] = '\0'; + USBSerial.println(buf); +} +#endif // USE_USB_CDC_OUTPUT + +void check_for_factory_reset() +{ +#if defined(FACTORY_RESET_GPIO_PIN) + // Check the factory reset pin which should normally read HIGH (set), if it + // reads LOW (clr) delete the cdi.xml and openlcb_config + if (!FACTORY_RESET_Pin::instance()->read()) + { + LOG(WARNING, "!!!! WARNING WARNING WARNING WARNING WARNING !!!!"); + LOG(WARNING, "The factory reset GPIO pin %d has been triggered.", + FACTORY_RESET_GPIO_PIN); + for (uint8_t sec = FACTORY_RESET_COUNTDOWN_SECS; + sec > 0 && !FACTORY_RESET_Pin::instance()->read(); sec--) + { + LOG(WARNING, "Factory reset will be initiated in %d seconds.", + sec); + usleep(SEC_TO_USEC(1)); + } + if (!FACTORY_RESET_Pin::instance()->read()) + { + unlink(openlcb::CDI_FILENAME); + unlink(openlcb::CONFIG_FILENAME); + LOG(WARNING, "Factory reset complete"); + } + else + { + LOG(WARNING, "Factory reset aborted as pin %d was not held LOW", + FACTORY_RESET_GPIO_PIN); + } + } +#endif // FACTORY_RESET_GPIO_PIN +} + +void setup() +{ +#if !defined(USE_USB_CDC_OUTPUT) + Serial.begin(115200L); +#else + USB.productName(openlcb::SNIP_STATIC_DATA.model_name); + USB.manufacturerName(openlcb::SNIP_STATIC_DATA.manufacturer_name); + USB.firmwareVersion(openlcb::CANONICAL_VERSION); + USB.serialNumber(uint64_to_string_hex(NODE_ID).c_str()); + USB.begin(); + USBSerial.begin(); + USBSerial.setDebugOutput(true); + // Give time for the USB peripheral to startup and be ready to use. + delay(5000); +#endif // USE_USB_CDC_OUTPUT + + uint8_t reset_reason = Esp32SocInfo::print_soc_info(); + LOG(INFO, "[Node] ID: %s", uint64_to_string_hex(NODE_ID).c_str()); + LOG(INFO, "[SNIP] version:%d, manufacturer:%s, model:%s, hw-v:%s, sw-v:%s", + openlcb::SNIP_STATIC_DATA.version, + openlcb::SNIP_STATIC_DATA.manufacturer_name, + openlcb::SNIP_STATIC_DATA.model_name, + openlcb::SNIP_STATIC_DATA.hardware_version, + openlcb::SNIP_STATIC_DATA.software_version); + + // Initialize the SPIFFS filesystem as our persistence layer + if (!SPIFFS.begin()) + { + LOG(WARNING, + "SPIFFS failed to mount, attempting to format and remount"); + if (!SPIFFS.begin(true)) + { + LOG_ERROR("SPIFFS mount failed even with format, giving up!"); + while (1) + { + // Unable to start SPIFFS successfully, give up and wait + // for WDT to kick in + } + } + } + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) + // initialize the bootloader. + esp32_bootloader_init(reset_reason); + + // if we have a request to enter the bootloader we need to process it + // before we startup the OpenMRN stack. + if (request_bootloader()) + { + esp32_bootloader_run(NODE_ID, CAN_RX_PIN, CAN_TX_PIN); + // This line should not be reached as the esp32_bootloader_run method + // will not return by default. + HASSERT(false); + } +#endif // FIRMWARE_UPDATE_BOOTLOADER + check_for_factory_reset(); + + // Create the CDI.xml dynamically + openmrn.create_config_descriptor_xml(cfg, openlcb::CDI_FILENAME); + + // Create the default internal configuration file + openmrn.stack()->create_config_file_if_needed(cfg.seg().internal_config(), + openlcb::CANONICAL_VERSION, openlcb::CONFIG_FILE_SIZE); + + // initialize all declared GPIO pins + GpioInit::hw_init(); + +#if defined(USE_TWAI) + twai.hw_init(); +#endif // USE_TWAI + + // Start the OpenMRN stack + openmrn.begin(); + + if (reset_reason == RTCWDT_BROWN_OUT_RESET) + { + openmrn.stack()->executor()->add(new CallbackExecutable([]() + { + openmrn.stack()->send_event(openlcb::Defs::NODE_POWER_BROWNOUT_EVENT); + })); + } + +#if defined(PRINT_PACKETS) + // Dump all packets as they are sent/received. + // Note: This should not be enabled in deployed nodes as it will have + // performance impact. + openmrn.stack()->print_all_packets(); +#endif // PRINT_PACKETS + +#if defined(USE_TWAI_ASYNC) + openmrn.add_can_port_async("/dev/twai/twai0"); +#elif defined(USE_TWAI) + openmrn.add_can_port_select("/dev/twai/twai0"); + + // start executor thread since this is required for select() to work in the + // OpenMRN executor. + openmrn.start_executor_thread(); +#endif // USE_TWAI_ASYNC +} + +void loop() +{ + // Call the OpenMRN executor, this needs to be done as often + // as possible from the loop() method. + openmrn.loop(); +} + +#if defined(FIRMWARE_UPDATE_BOOTLOADER) + +extern "C" +{ + +/// Updates the state of a status LED. +/// +/// @param led is the LED to update. +/// @param value is the new state of the LED. +void bootloader_led(enum BootloaderLed led, bool value) +{ + LOG(INFO, "[Bootloader] bootloader_led(%d, %d)", led, value); +} + +} // extern "C" + +#endif // FIRMWARE_UPDATE_BOOTLOADER \ No newline at end of file diff --git a/arduino/examples/ESP32S2IOBoard/config.h b/arduino/examples/ESP32S2IOBoard/config.h new file mode 100644 index 000000000..51751ac15 --- /dev/null +++ b/arduino/examples/ESP32S2IOBoard/config.h @@ -0,0 +1,90 @@ +#ifndef _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ +#define _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ + +#include "openlcb/ConfiguredConsumer.hxx" +#include "openlcb/ConfiguredProducer.hxx" +#include "openlcb/ConfigRepresentation.hxx" +#include "openlcb/MemoryConfig.hxx" + +#include "freertos_drivers/esp32/Esp32WiFiConfiguration.hxx" + +// catch invalid configuration at compile time +#if !defined(USE_TWAI) && !defined(USE_WIFI) +#error "Invalid configuration detected, USE_TWAI or USE_WIFI must be defined." +#endif + +namespace openlcb +{ + +/// Defines the identification information for the node. The arguments are: +/// +/// - 4 (version info, always 4 by the standard +/// - Manufacturer name +/// - Model name +/// - Hardware version +/// - Software version +/// +/// This data will be used for all purposes of the identification: +/// +/// - the generated cdi.xml will include this data +/// - the Simple Node Ident Info Protocol will return this data +/// - the ACDI memory space will contain this data. +extern const SimpleNodeStaticValues SNIP_STATIC_DATA = { + 4, + "OpenMRN", +#if defined(USE_WIFI) && !defined(USE_TWAI) + "Arduino IO Board (WiFi)", +#elif defined(USE_TWAI) && !defined(USE_WIFI) + "Arduino IO Board (CAN)", +#elif defined(USE_TWAI) && defined(USE_WIFI) + "Arduino IO Board (WiFi/CAN)", +#else + "Arduino IO Board", +#endif + ARDUINO_VARIANT, + "1.00"}; + +constexpr uint8_t NUM_OUTPUTS = 14; +constexpr uint8_t NUM_INPUTS = 14; + +/// Declares a repeated group of a given base group and number of repeats. The +/// ProducerConfig and ConsumerConfig groups represent the configuration layout +/// needed by the ConfiguredProducer and ConfiguredConsumer classes, and come +/// from their respective hxx file. +using AllConsumers = RepeatedGroup; +using AllProducers = RepeatedGroup; + +/// Modify this value every time the EEPROM needs to be cleared on the node +/// after an update. +static constexpr uint16_t CANONICAL_VERSION = 0x100b; + +/// Defines the main segment in the configuration CDI. This is laid out at +/// origin 128 to give space for the ACDI user data at the beginning. +CDI_GROUP(IoBoardSegment, Segment(MemoryConfigDefs::SPACE_CONFIG), Offset(128)); +/// Each entry declares the name of the current entry, then the type and then +/// optional arguments list. +CDI_GROUP_ENTRY(internal_config, InternalConfigData); +CDI_GROUP_ENTRY(consumers, AllConsumers, Name("Outputs"), RepName("Output")); +CDI_GROUP_ENTRY(producers, AllProducers, Name("Inputs"), RepName("Input")); +#if defined(USE_WIFI) +CDI_GROUP_ENTRY(wifi, WiFiConfiguration, Name("WiFi Configuration")); +#endif +CDI_GROUP_END(); + +/// The main structure of the CDI. ConfigDef is the symbol we use in main.cxx +/// to refer to the configuration defined here. +CDI_GROUP(ConfigDef, MainCdi()); +/// Adds the tag with the values from SNIP_STATIC_DATA above. +CDI_GROUP_ENTRY(ident, Identification); +/// Adds an tag. +CDI_GROUP_ENTRY(acdi, Acdi); +/// Adds a segment for changing the values in the ACDI user-defined +/// space. UserInfoSegment is defined in the system header. +CDI_GROUP_ENTRY(userinfo, UserInfoSegment, Name("User Info")); +/// Adds the main configuration segment. +CDI_GROUP_ENTRY(seg, IoBoardSegment, Name("Settings")); +CDI_GROUP_END(); + +} // namespace openlcb + +#endif // _ARDUINO_EXAMPLE_ESP32IOBOARD_CONFIG_H_ diff --git a/arduino/libify.sh b/arduino/libify.sh index eaa8e3ba6..61ce6df88 100755 --- a/arduino/libify.sh +++ b/arduino/libify.sh @@ -133,13 +133,13 @@ function copy_dir() { copy_file . arduino/{library.json,library.properties,keywords.txt,README.md,LICENSE,CONTRIBUTING.md} copy_dir . arduino/examples -copy_file src arduino/OpenMRNLite.{h,cpp} \ +copy_file src arduino/OpenMRNLite.{h,cpp} arduino/CDIXMLGenerator.hxx \ include/{can_frame.h,nmranet_config.h,openmrn_features.h} \ - include/freertos/{freertos_includes.h,endian.h} \ + include/freertos/{bootloader_hal.h,can_ioctl.h,endian.h,freertos_includes.h,stropts.h} \ include/freertos_select/ifaddrs.h # General DCC related files (all headers and DCC packet related cxx) -copy_file src/dcc src/dcc/*.hxx src/dcc/*.h src/dcc/{DccDebug,Packet}.cxx +copy_file src/dcc src/dcc/*.hxx src/dcc/*.h src/dcc/{dcc_constants,DccDebug,LocalTrackIf,Packet}.cxx # RailCom related DCC files copy_file src/dcc src/dcc/{RailCom,RailcomBroadcastDecoder,RailcomDebug}.cxx @@ -154,16 +154,20 @@ copy_file src/executor src/executor/*.hxx src/executor/*.cxx copy_file src/openlcb src/openlcb/*.hxx src/openlcb/*.cxx rm -f ${TARGET_LIB_DIR}/src/openlcb/CompileCdiMain.cxx \ + ${TARGET_LIB_DIR}/src/openlcb/EventHandlerMock.hxx \ ${TARGET_LIB_DIR}/src/openlcb/Stream.cxx \ ${TARGET_LIB_DIR}/src/openlcb/Stream.hxx copy_file src/freertos_drivers/arduino \ src/freertos_drivers/arduino/* \ src/freertos_drivers/common/DeviceBuffer.{hxx,cxx} \ + src/freertos_drivers/common/DummyGPIO.hxx \ src/freertos_drivers/common/GpioWrapper.hxx \ src/freertos_drivers/common/CpuLoad.{hxx,cxx} \ src/freertos_drivers/common/WifiDefs.{hxx,cxx} \ src/freertos_drivers/common/libatomic.c \ + src/freertos_drivers/common/PWM.hxx \ + src/freertos_drivers/common/RailcomDriver.hxx copy_file src/freertos_drivers/esp32 \ src/freertos_drivers/esp32/* @@ -184,11 +188,13 @@ rm -f ${TARGET_LIB_DIR}/src/utils/ReflashBootloader.cxx \ ${TARGET_LIB_DIR}/src/utils/AesCcmTestVectorsEx.hxx \ ${TARGET_LIB_DIR}/src/utils/async_datagram_test_helper.hxx \ ${TARGET_LIB_DIR}/src/utils/async_if_test_helper.hxx \ + ${TARGET_LIB_DIR}/src/utils/async_stream_test_helper.hxx \ ${TARGET_LIB_DIR}/src/utils/async_traction_test_helper.hxx \ ${TARGET_LIB_DIR}/src/utils/EEPROMEmuTest.hxx \ ${TARGET_LIB_DIR}/src/utils/hub_test_utils.hxx \ ${TARGET_LIB_DIR}/src/utils/if_tcp_test_helper.hxx \ - ${TARGET_LIB_DIR}/src/utils/test_main.hxx + ${TARGET_LIB_DIR}/src/utils/test_main.hxx \ + ${TARGET_LIB_DIR}/src/utils/ShaTestVectors.hxx if [ "x$VERBOSE" != "x" ]; then echo "Renaming all cxx to cpp under ${TARGET_LIB_DIR}/src" diff --git a/include/can_frame.h b/include/can_frame.h index f97536ef8..7ebef825b 100644 --- a/include/can_frame.h +++ b/include/can_frame.h @@ -63,7 +63,9 @@ (_frame).can_id += ((_value) & CAN_SFF_MASK); \ } -#elif defined (__nuttx__) || defined (__FreeRTOS__) || defined (__MACH__) || defined (__WIN32__) || defined(__EMSCRIPTEN__) || defined(ESP_NONOS) || defined(ARDUINO) +#elif defined (__nuttx__) || defined (__FreeRTOS__) || defined (__MACH__) || \ + defined (__WIN32__) || defined (__EMSCRIPTEN__) || defined (ESP_NONOS) || \ + defined (ARDUINO) || defined (ESP32) #include struct can_frame diff --git a/include/freertos/can_ioctl.h b/include/freertos/can_ioctl.h index 1358f55ae..69f5d228e 100644 --- a/include/freertos/can_ioctl.h +++ b/include/freertos/can_ioctl.h @@ -35,7 +35,11 @@ #define _FREERTOS_CAN_IOCTL_H_ #include +#ifdef __FreeRTOS__ #include "freertos/stropts.h" +#elif defined(ESP32) +#include "stropts.h" +#endif #if defined (__cplusplus) extern "C" { diff --git a/include/openmrn_features.h b/include/openmrn_features.h index 8a619a0f6..810e34cfd 100644 --- a/include/openmrn_features.h +++ b/include/openmrn_features.h @@ -37,6 +37,12 @@ #ifndef _INCLUDE_OPENMRN_FEATURES_ #define _INCLUDE_OPENMRN_FEATURES_ +#ifdef ESP32 +#include +#else +#define ESP_IDF_VERSION 0 +#define ESP_IDF_VERSION_VAL(a,b,c) 1 +#endif // ESP32 #ifdef __FreeRTOS__ /// Compiles the FreeRTOS event group based ::select() implementation. @@ -48,6 +54,13 @@ #define OPENMRN_FEATURE_REENT 1 #endif +#if defined(__FreeRTOS__) || ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) +// Note: this is not using OPENMRN_FEATURE_DEVICE_SELECT due to other usages +// of that macro which may conflict with the ESP32 version of this feature. +/// Adds support for FD based CAN interfaces. +#define OPENMRN_FEATURE_FD_CAN_DEVICE 1 +#endif + #if defined(__linux__) || defined(__MACH__) || defined(__WINNT__) || defined(ESP32) || defined(OPENMRN_FEATURE_DEVTAB) /// Enables the code using ::open ::close ::read ::write for non-volatile /// storage, FileMemorySpace for the configuration space, and diff --git a/src/dcc/LocalTrackIf.cxx b/src/dcc/LocalTrackIf.cxx index bbd2fe3bb..e628ff707 100644 --- a/src/dcc/LocalTrackIf.cxx +++ b/src/dcc/LocalTrackIf.cxx @@ -36,10 +36,16 @@ #include #include +#include "openmrn_features.h" +#ifdef OPENMRN_FEATURE_FD_CAN_DEVICE #define LOGLEVEL INFO +#ifdef __FreeRTOS__ #include "freertos/can_ioctl.h" +#else +#include "can_ioctl.h" +#endif #include "dcc/LocalTrackIf.hxx" namespace dcc @@ -72,3 +78,5 @@ StateFlowBase::Action LocalTrackIfSelect::entry() { } } // namespace dcc + +#endif // OPENMRN_FEATURE_FD_CAN_DEVICE \ No newline at end of file diff --git a/src/dcc/Receiver.hxx b/src/dcc/Receiver.hxx index 21c74b653..9beec8a3e 100644 --- a/src/dcc/Receiver.hxx +++ b/src/dcc/Receiver.hxx @@ -40,7 +40,11 @@ #include "executor/StateFlow.hxx" +#ifdef __FreeRTOS__ #include "freertos/can_ioctl.h" +#else +#include "can_ioctl.h" +#endif #include "freertos_drivers/common/SimpleLog.hxx" #include "dcc/packet.h" #include "utils/Crc.hxx" diff --git a/src/executor/StateFlow.hxx b/src/executor/StateFlow.hxx index 69eed42c5..d52b83817 100644 --- a/src/executor/StateFlow.hxx +++ b/src/executor/StateFlow.hxx @@ -736,10 +736,14 @@ protected: */ Action listen_and_call(StateFlowSelectHelper *helper, int fd, Callback c) { +// ESP-IDF does not implement fstat for the LwIP VFS layer +// https://github.com/espressif/esp-idf/issues/7198 +#ifndef ESP32 // verify that the fd is a socket struct stat stat; fstat(fd, &stat); HASSERT(S_ISSOCK(stat.st_mode)); +#endif // ESP32 helper->reset(Selectable::READ, fd, Selectable::MAX_PRIO); helper->set_wakeup(this); @@ -755,10 +759,14 @@ protected: */ Action connect_and_call(StateFlowSelectHelper *helper, int fd, Callback c) { +// ESP-IDF does not implement fstat for the LwIP VFS layer +// https://github.com/espressif/esp-idf/issues/7198 +#ifndef ESP32 // verify that the fd is a socket struct stat stat; fstat(fd, &stat); HASSERT(S_ISSOCK(stat.st_mode)); +#endif // ESP32 helper->reset(Selectable::WRITE, fd, Selectable::MAX_PRIO); helper->set_wakeup(this); diff --git a/src/freertos_drivers/common/CpuLoad.cxx b/src/freertos_drivers/common/CpuLoad.cxx index 437804706..7f57db1b8 100644 --- a/src/freertos_drivers/common/CpuLoad.cxx +++ b/src/freertos_drivers/common/CpuLoad.cxx @@ -39,7 +39,12 @@ #include "os/os.h" #include "freertos_includes.h" -extern "C" { +#ifdef ESP32 +#include "sdkconfig.h" +#endif // ESP32 + +extern "C" +{ /// The bits to shift to get multiples of 1.0 static constexpr uint32_t SHIFT_ONE = 24; @@ -116,22 +121,32 @@ void cpuload_tick(unsigned irq) { if (!Singleton::exists()) return; +// On the ESP32 it is necessary to use a slightly different approach for +// recording CPU usage metrics since there may be additional CPU cores. #ifdef ESP32 if (irq != 0) { Singleton::instance()->record_value(true, (uintptr_t)irq); } - else // assumes openmrn task is pinned to core 0 + else { - auto hdl = xTaskGetCurrentTaskHandleForCPU(0); - bool is_idle = xTaskGetIdleTaskHandleForCPU(0) == hdl; + // Record the first CPU core (PRO_CPU). NOTE: This assumes that OpenMRN + // is running on this core. + auto hdl = xTaskGetCurrentTaskHandleForCPU(PRO_CPU_NUM); + bool is_idle = xTaskGetIdleTaskHandleForCPU(PRO_CPU_NUM) == hdl; Singleton::instance()->record_value(!is_idle, (uintptr_t)hdl); } - // always records CPU 1 task. - auto hdl = xTaskGetCurrentTaskHandleForCPU(1); - bool is_idle = xTaskGetIdleTaskHandleForCPU(1) == hdl; +// NOTE: The ESP32-S2 and ESP32-C3 are single-core SoC and defines the +// FREERTOS_UNICORE flag which we can use here to disable recording of the +// APP_CPU. +#ifndef CONFIG_FREERTOS_UNICORE + // Record the second CPU core (APP_CPU). NOTE: this is where application + // code typically runs. + auto hdl = xTaskGetCurrentTaskHandleForCPU(APP_CPU_NUM); + bool is_idle = xTaskGetIdleTaskHandleForCPU(APP_CPU_NUM) == hdl; Singleton::instance()->record_value(!is_idle, (uintptr_t)hdl); -#else +#endif // CONFIG_FREERTOS_UNICORE +#else // NOT ESP32 if (irq != 0) { Singleton::instance()->record_value(true, (uintptr_t)irq); @@ -140,9 +155,9 @@ void cpuload_tick(unsigned irq) auto hdl = xTaskGetCurrentTaskHandle(); bool is_idle = xTaskGetIdleTaskHandle() == hdl; Singleton::instance()->record_value(!is_idle, (uintptr_t)hdl); -#endif -} +#endif // ESP32 } +} // extern "C" DEFINE_SINGLETON_INSTANCE(CpuLoad); diff --git a/src/freertos_drivers/esp32/Esp32BootloaderHal.hxx b/src/freertos_drivers/esp32/Esp32BootloaderHal.hxx new file mode 100644 index 000000000..572758dca --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32BootloaderHal.hxx @@ -0,0 +1,562 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32BootloaderHal.hxx + * + * ESP32 specific implementation of the HAL (Hardware Abstraction Layer) used + * by the OpenLCB bootloader. + * + * Additional functions from bootloader_hal.h will be required to be defined by + * the application code. + * + * @author Mike Dunston + * @date 3 May 2021 + */ + +#ifndef _FREERTOS_DRIVERS_ESP32_ESP32BOOTLOADERHAL_HXX_ +#define _FREERTOS_DRIVERS_ESP32_ESP32BOOTLOADERHAL_HXX_ + +#include "sdkconfig.h" +#include + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,3,0) +#error ESP32 Bootloader is only supported with ESP-IDF v4.3+ +#endif // IDF v4.3+ + +#ifndef BOOTLOADER_LOG_LEVEL +#define BOOTLOADER_LOG_LEVEL VERBOSE +#endif // BOOTLOADER_LOG_LEVEL + +#ifndef BOOTLOADER_TWAI_LOG_LEVEL +#define BOOTLOADER_TWAI_LOG_LEVEL VERBOSE +#endif // BOOTLOADER_TWAI_LOG_LEVEL + +// Enable streaming support for the bootloader +#define BOOTLOADER_STREAM +// Set the buffer size to half the sector size to minimize the flash writes. +#define WRITE_BUFFER_SIZE (CONFIG_WL_SECTOR_SIZE / 2) + +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,4,0) +#include +#endif // IDF v4.4+ + +#include +#if defined(CONFIG_IDF_TARGET_ESP32) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32S2) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32S3) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32C3) +#include +#else +#error Unknown/Unsupported ESP32 variant. +#endif +#include "openlcb/Bootloader.hxx" +#include "utils/constants.hxx" +#include "utils/Hub.hxx" + +/// ESP32 Bootloader internal data. +struct Esp32BootloaderState +{ + /// Chip identifier for the currently running firmware. + esp_chip_id_t chip_id; + + /// Currently running application header information. + /// + /// NOTE: Only the size is populated in this structure, the checksum values are + /// initialized to zero as they are not used. + struct app_header app_header; + + /// Node ID to use for the bootloader. + uint64_t node_id; + + /// Partition where the firmware is currently running from. + esp_partition_t *current; + + /// Partition where the new firmware should be written to. + esp_partition_t *target; + + /// OTA handle used to track the firmware update progress. + esp_ota_handle_t ota_handle; + + /// Internal flag to indicate that we have initialized the TWAI peripheral and + /// should deinit it before exit. + bool twai_initialized; + + /// GPIO pin connected to the CAN transceiver TX pin. + gpio_num_t tx_pin; + + /// GPIO pin connected to the CAN transceiver RX pin. + gpio_num_t rx_pin; +}; + +/// Bootloader configuration data. +static Esp32BootloaderState esp_bl_state; + +/// Maximum time to wait for a TWAI frame to be received or transmitted before +/// giving up. +static constexpr BaseType_t MAX_TWAI_WAIT = pdMS_TO_TICKS(250); + +/// Flag used to indicate that we have been requested to enter the bootloader +/// instead of normal node operations. Note that this value will not be +/// initialized by the system and a check for power on reset will need to be +/// made to initialize it on first boot. +static uint32_t RTC_NOINIT_ATTR bootloader_request; + +/// Value to be assigned to @ref bootloader_request when the bootloader should +/// run instead of normal node operations. +static constexpr uint32_t RTC_BOOL_TRUE = 1; + +/// Default value to assign to @ref bootloader_request when the ESP32-C3 starts +/// the first time or when the bootloader should not be run. +static constexpr uint32_t RTC_BOOL_FALSE = 0; + +extern "C" +{ + +/// Callback from the bootloader to configure and start the TWAI hardware. +void bootloader_hw_init(void) +{ + // TWAI driver timing configuration, 125kbps. + twai_timing_config_t t_config = TWAI_TIMING_CONFIG_125KBITS(); + + // TWAI driver filter configuration, accept all frames. + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + + // TWAI driver general configuration. + twai_general_config_t g_config = + TWAI_GENERAL_CONFIG_DEFAULT(esp_bl_state.tx_pin, esp_bl_state.rx_pin, + TWAI_MODE_NORMAL); + g_config.tx_queue_len = config_can_tx_buffer_size(); + g_config.rx_queue_len = config_can_rx_buffer_size(); + + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Configuring TWAI driver"); + ESP_ERROR_CHECK(twai_driver_install(&g_config, &t_config, &f_config)); + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Starting TWAI driver"); + ESP_ERROR_CHECK(twai_start()); + esp_bl_state.twai_initialized = true; +} + +/// Callback from the bootloader for entering the application. +/// +/// This will default to reboot the ESP32. +void application_entry(void) +{ + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] application_entry"); + + // reset the RTC persistent variable to not enter bootloader on next + // restart. + bootloader_request = RTC_BOOL_FALSE; + + // restart the esp32 since we do not have a way to rerun app_main. + esp_restart(); +} + +/// Callback from the bootloader when a reboot should be triggered. +/// +/// Currently a NO-OP. +void bootloader_reboot(void) +{ + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Reboot requested"); +} + +/// Callback from the bootloader to read a single CAN frame. +/// +/// @param frame buffer for receiving a CAN frame into. +/// @return true when successful, false otherwise. +bool read_can_frame(struct can_frame *frame) +{ + twai_message_t rx_msg; + memset(&rx_msg, 0, sizeof(twai_message_t)); + if (twai_receive(&rx_msg, MAX_TWAI_WAIT) == ESP_OK) + { + LOG(BOOTLOADER_TWAI_LOG_LEVEL, "[Bootloader] CAN_RX"); + frame->can_id = rx_msg.identifier; + frame->can_dlc = rx_msg.data_length_code; + frame->can_err = 0; + frame->can_eff = rx_msg.extd; + frame->can_rtr = rx_msg.rtr; + memcpy(frame->data, rx_msg.data, frame->can_dlc); + return true; + } + return false; +} + +/// Callback from the bootloader to transmit a single CAN frame. +/// +/// @param frame CAN frame to transmit. +/// @return true when successful, false otherwise. +bool try_send_can_frame(const struct can_frame &frame) +{ + twai_message_t tx_msg; + memset(&tx_msg, 0, sizeof(twai_message_t)); + tx_msg.identifier = frame.can_id; + tx_msg.data_length_code = frame.can_dlc; + tx_msg.extd = frame.can_eff; + tx_msg.rtr = frame.can_rtr; + memcpy(tx_msg.data, frame.data, frame.can_dlc); + if (twai_transmit(&tx_msg, MAX_TWAI_WAIT) == ESP_OK) + { + LOG(BOOTLOADER_TWAI_LOG_LEVEL, "[Bootloader] CAN_TX"); + return true; + } + return false; +} + +/// Callback from the bootloader to retrieve flash boundaries. +/// +/// @param flash_min Minimum flash address to write to. +/// @param flash_max Maximum flash address to write to. +/// @param app_header Pointer to the app_header struct for the currently +/// running firmware. +/// +/// NOTE: This will default to set @param flash_min to zero, @param flash_max +/// to the partition size and @param app_header to a statically declared struct +/// with only the size populated. +void get_flash_boundaries(const void **flash_min, const void **flash_max, + const struct app_header **app_header) +{ + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] get_flash_boundaries(%d,%d)", 0, + esp_bl_state.app_header.app_size); + *((uint32_t *)flash_min) = 0; + *((uint32_t *)flash_max) = esp_bl_state.app_header.app_size; + *app_header = &esp_bl_state.app_header; +} + +/// Callback from the bootloader to retrieve flash page information. +/// +/// @param address Flash address to retrieve information on. +/// @param page_state Starting address for the page. +/// @param page_length_bytes Length of the page. +void get_flash_page_info( + const void *address, const void **page_start, uint32_t *page_length_bytes) +{ + uint32_t value = (uint32_t)address; + value &= ~(CONFIG_WL_SECTOR_SIZE - 1); + *page_start = (const void *)value; + *page_length_bytes = CONFIG_WL_SECTOR_SIZE; + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] get_flash_page_info(%d, %d)", + value, *page_length_bytes); +} + +/// Callback from the bootloader to erase a flash page. +/// +/// @param address Flash address to erase. +/// +/// NOTE: This is a NO-OP on the ESP32 as this is handled internally by the +/// esp_ota_write() API. +void erase_flash_page(const void *address) +{ + // NO OP as this is handled automatically as part of esp_ota_write. + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Erase: %d", (uint32_t)address); +} + +/// Callback from the bootloader to write to flash. +/// +/// @param address Flash address to write to. +/// @param data Buffer to write to flash. +/// @param size_bytes Number of bytes to write to flash. +/// +/// This method leverages the ESP32 OTA APIs to write the flash data. Upon +/// writing to flash address zero (first address), the @param data will be +/// validated to ensure the firmware being received is applicable to the +/// ESP32 and that it appears correct (has correct image magic byte). If the +/// firmware is not applicable or otherwise corrupted for the first flash +/// address the ESP32 will reboot to abort the bootloader download. +void write_flash(const void *address, const void *data, uint32_t size_bytes) +{ + uint32_t addr = (uint32_t)address; + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Write: %d, %d", addr, size_bytes); + + // The first part of the received binary should have the image header, + // segment header and app description. These are used as a first pass + // validation of the received data to ensure it is a valid firmware. + if (addr == 0) + { + // Mapping of known ESP32 chip id values. + const char * const CHIP_ID_NAMES[] = + { + // Note: this must be kept in sync with esp_chip_id_t. + "ESP32", // 0x00 ESP_CHIP_ID_ESP32 + "INVALID", // 0x01 invalid (placeholder) + "ESP32-S2", // 0x02 ESP_CHIP_ID_ESP32S2 + "INVALID", // 0x03 invalid (placeholder) + "INVALID", // 0x04 invalid (placeholder) + "ESP32-C3", // 0x05 ESP_CHIP_ID_ESP32C3 + "INVALID", // 0x06 invalid (placeholder) + "INVALID", // 0x07 invalid (placeholder) + "INVALID", // 0x08 invalid (placeholder) + "ESP32-S3", // 0x09 ESP_CHIP_ID_ESP32S3 + "ESP32-H2", // 0x0A ESP_CHIP_ID_ESP32H2 + "INVALID", // 0x0B invalid (placeholder) + "ESP32-C2", // 0x0C ESP_CHIP_ID_ESP32C2 + }; + + bool should_abort = false; + esp_image_header_t *image_header = (esp_image_header_t *)data; + // If the image magic is correct we can proceed with validating the + // basic details of the image. + if (image_header->magic == ESP_IMAGE_HEADER_MAGIC) + { + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Chip ID: %s / %x (%x)", + CHIP_ID_NAMES[image_header->chip_id], + image_header->chip_id, esp_bl_state.chip_id); + // validate the image magic byte and chip type to + // ensure it matches the currently running chip. + if (image_header->chip_id != ESP_CHIP_ID_INVALID && + image_header->chip_id == esp_bl_state.chip_id) + { + // start the OTA process at this point, if we have had a + // previous failure this will reset the OTA process so we can + // start fresh. + esp_err_t err = ESP_ERROR_CHECK_WITHOUT_ABORT( + esp_ota_begin(esp_bl_state.target, OTA_SIZE_UNKNOWN, + &esp_bl_state.ota_handle)); + should_abort = (err != ESP_OK); + } + else + { + LOG_ERROR("[Bootloader] Firmware does not appear to be valid " + "or is for a different chip (%s - %x vs %s - %x).", + CHIP_ID_NAMES[image_header->chip_id], + image_header->chip_id, + CHIP_ID_NAMES[esp_bl_state.chip_id], + esp_bl_state.chip_id); + should_abort = true; + } + } + else + { + LOG_ERROR("[Bootloader] Image magic is incorrect: %d vs %d!", + image_header->magic, ESP_IMAGE_HEADER_MAGIC); + should_abort = true; + } + + // It would be ideal to abort the firmware upload at this point but the + // bootloader HAL does not offer a way to abort the transfer so instead + // reboot the node. + if (should_abort || esp_bl_state.ota_handle == 0) + { + // reset the RTC persistent variable to not enter bootloader on + // next restart. + bootloader_request = RTC_BOOL_FALSE; + esp_restart(); + } + } + bootloader_led(LED_WRITING, true); + bootloader_led(LED_ACTIVE, false); + ESP_ERROR_CHECK( + esp_ota_write(esp_bl_state.ota_handle, data, size_bytes)); + bootloader_led(LED_WRITING, false); + bootloader_led(LED_ACTIVE, true); +} + +/// Callback from the bootloader to indicate that the full firmware file has +/// been received. +/// +/// @return zero if the firmware has been received and updated to be used for +/// the next startup, otherwise non-zero and an error will be printed to the +/// console. +uint16_t flash_complete(void) +{ + LOG(INFO, "[Bootloader] Finalizing firmware update"); + esp_err_t res = + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_ota_end(esp_bl_state.ota_handle)); + if (res == ESP_OK) + { + LOG(INFO, + "[Bootloader] Firmware appears valid, updating the next boot " + "partition to %s.", esp_bl_state.target->label); + res = + ESP_ERROR_CHECK_WITHOUT_ABORT( + esp_ota_set_boot_partition(esp_bl_state.target)); + if (res != ESP_OK) + { + LOG_ERROR("[Bootloader] Failed to update the boot partition!"); + } + } + else if (res == ESP_ERR_OTA_VALIDATE_FAILED) + { + LOG_ERROR("[Bootloader] Firmware image failed validation, aborting!"); + } + return res != ESP_OK; +} + +/// Callback from the bootloader to calculate the checksum of a data block. +/// +/// @param data Start of block to calculate checksum for. +/// @param size Number of bytes to calcuate checksum for. +/// @param checksum Calculated checksum for the data block. +/// +/// NOTE: The ESP32 does not use this method and will always set the checksum +/// value to zero. +void checksum_data(const void* data, uint32_t size, uint32_t* checksum) +{ + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] checksum_data(%d)", size); + // Force the checksum to be zero since it is not currently used on the + // ESP32. The startup of the node may validate the built-in SHA256 and + // fallback to previous application binary if the SHA256 validation fails. + memset(checksum, 0, sizeof(uint32_t) * CHECKSUM_COUNT); +} + +/// Callback from the bootloader to obtain the pre-defined alias to use. +/// +/// @return zero to have the bootloader assign one based on the node-id. +uint16_t nmranet_alias(void) +{ + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] nmranet_alias"); + // let the bootloader generate it based on nmranet_nodeid(). + return 0; +} + +/// Callback from the bootloader to obtain the node-id to use. +/// +/// @return node-id provided to @ref esp32_bootloader_run. +uint64_t nmranet_nodeid(void) +{ + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] nmranet_nodeid"); + return esp_bl_state.node_id; +} + +} // extern "C" + +/// Initializes the ESP32 Bootloader. +/// +/// @param reset_reason Reason for the ESP32 startup. +/// +/// NOTE: This method must be called during the startup of the ESP32, failure +/// to call this method will result in the bootloader checks having undefined +/// behavior since RTC memory is not cleared upon startup! +/// +/// Example: +///``` +/// void setup() +/// { +/// ... +/// uint8_t reset_reason = Esp32SocInfo.print_soc_info(); +/// esp32_bootloader_init(reset_reason); +/// ... +/// } +///``` +void esp32_bootloader_init(uint8_t reset_reason) +{ + // If this is the first power up of the node we need to reset the flag + // since it will not be initialized automatically. + if (reset_reason == POWERON_RESET) + { + bootloader_request = RTC_BOOL_FALSE; + } +} + +/// Runs the ESP32 Bootloader. +/// +/// @param id Node ID to advertise on the TWAI (CAN) bus. +/// @param rx Pin connected to the SN65HVD23x/MCP2551 R (RX) pin. +/// @param tx Pin connected to the SN65HVD23x/MCP2551 D (TX) pin. +/// @param reboot_on_exit Reboot the ESP32 upon completion, default is true. +void esp32_bootloader_run(uint64_t id, gpio_num_t rx, gpio_num_t tx, + bool reboot_on_exit = true) +{ + memset(&esp_bl_state, 0, sizeof(Esp32BootloaderState)); + + esp_bl_state.node_id = id; + esp_bl_state.tx_pin = tx; + esp_bl_state.rx_pin = rx; + esp_bl_state.chip_id = ESP_CHIP_ID_INVALID; + + // Extract the currently running chip details so we can use it to confirm + // the received firmware is for this chip. + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + switch (chip_info.model) + { + case CHIP_ESP32: + esp_bl_state.chip_id = ESP_CHIP_ID_ESP32; + break; + case CHIP_ESP32S2: + esp_bl_state.chip_id = ESP_CHIP_ID_ESP32S2; + break; + case CHIP_ESP32S3: + esp_bl_state.chip_id = ESP_CHIP_ID_ESP32S3; + break; + case CHIP_ESP32C3: + esp_bl_state.chip_id = ESP_CHIP_ID_ESP32C3; + break; + default: + LOG(FATAL, "[Bootloader] Unknown/Unsupported Chip ID: %x", chip_info.model); + } + + // Initialize the app header details based on the currently running + // partition. + esp_bl_state.current = (esp_partition_t *)esp_ota_get_running_partition(); + esp_bl_state.app_header.app_size = esp_bl_state.current->size; + + // Find the next OTA partition and confirm it is not the currently running + // partition. + esp_bl_state.target = + (esp_partition_t *)esp_ota_get_next_update_partition(NULL); + if (esp_bl_state.target != nullptr && + esp_bl_state.target != esp_bl_state.current) + { + LOG(INFO, "[Bootloader] Preparing to receive firmware"); + LOG(INFO, "[Bootloader] Current partition: %s", + esp_bl_state.current->label); + LOG(INFO, "[Bootloader] Target partition: %s", + esp_bl_state.target->label); + + // since we have the target partition identified, start the bootloader. + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] calling bootloader_entry"); + bootloader_entry(); + } + else + { + LOG_ERROR("[Bootloader] Unable to locate next OTA partition!"); + } + + if (esp_bl_state.twai_initialized) + { + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Stopping TWAI driver"); + ESP_ERROR_CHECK(twai_stop()); + LOG(BOOTLOADER_LOG_LEVEL, "[Bootloader] Disabling TWAI driver"); + ESP_ERROR_CHECK(twai_driver_uninstall()); + } + + // reset the RTC persistent variable to not enter bootloader on next + // restart. + bootloader_request = RTC_BOOL_FALSE; + + if (reboot_on_exit) + { + // If we reach here we should restart the node. + LOG(INFO, "[Bootloader] Restarting!"); + esp_restart(); + } +} + +#endif // _FREERTOS_DRIVERS_ESP32_ESP32BOOTLOADERHAL_HXX_ \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32CoreDumpUtil.hxx b/src/freertos_drivers/esp32/Esp32CoreDumpUtil.hxx new file mode 100644 index 000000000..8eaf609a9 --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32CoreDumpUtil.hxx @@ -0,0 +1,149 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32CoreDumpUtil.hxx + * + * Utility class for handling ESP32 core dump files stored in flash. + * + * @author Mike Dunston + * @date 8 August 2021 + */ + +#ifndef _FREERTOS_DRIVERS_ESP32_ESP32COREDUMPUTIL_HXX_ +#define _FREERTOS_DRIVERS_ESP32_ESP32COREDUMPUTIL_HXX_ + +#include "sdkconfig.h" +#include "os/Gpio.hxx" +#include "utils/FileUtils.hxx" +#include "utils/logging.h" +#include "utils/StringPrintf.hxx" + +#include +#include + +namespace openmrn_arduino +{ + +/// Utility class containing methods related to esp32 core dumps. +/// +/// Usage of this class requires the enablement of +/// CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH in sdkconfig *AND* the creation of +/// a 64kb "coredump" partition in flash. +/// +/// When the CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH flag is not enabled all +/// methods in this class are treated as no-op. +/// +/// NOTE: ESP-IDF v4.4 or later is required for the display() method. +class Esp32CoreDumpUtil +{ +public: + /// Utility method to check if a core dump is present. + /// @return true if a core dump is available, false otherwise. + static bool is_present() + { + esp_log_level_set("esp_core_dump_flash", ESP_LOG_NONE); + esp_err_t res = esp_core_dump_image_check(); + esp_log_level_set("esp_core_dump_flash", ESP_LOG_WARN); + return res == ESP_OK; + } + + /// Utility method that displays a core dump (if present). + /// + /// @param output_path Optional output path to store the code dump on the + /// filesystem. + static void display(const char *output_path = nullptr) + { + if (is_present()) + { + esp_core_dump_summary_t details; + if (esp_core_dump_get_summary(&details) == ESP_OK) + { + // Convert the core dump to a text file + string core_dump_summary = + StringPrintf("Task:%s (%d) crashed at PC %08x\n", + details.exc_task, details.exc_tcb, details.exc_pc); + core_dump_summary += StringPrintf("Registers:\n"); + for (size_t idx = 0; idx < 16; idx += 4) + { + core_dump_summary += + StringPrintf( + "A%02zu: 0x%08x A%02zu: 0x%08x A%02zu: 0x%08x A%02zu: 0x%08x\n", + idx, details.ex_info.exc_a[idx], + idx + 1, details.ex_info.exc_a[idx + 1], + idx + 2, details.ex_info.exc_a[idx + 2], + idx + 3, details.ex_info.exc_a[idx + 3]); + } + core_dump_summary += + StringPrintf("EXCCAUSE: %08x EXCVADDR: %08x\n", + details.ex_info.exc_cause, details.ex_info.exc_vaddr); + if (details.ex_info.epcx_reg_bits) + { + core_dump_summary += "EPCX:"; + for (size_t idx = 0; idx < 8; idx++) + { + if (details.ex_info.epcx_reg_bits & BIT(idx)) + { + core_dump_summary += + StringPrintf("%zu:%08x ", idx, details.ex_info.epcx[idx]); + } + } + core_dump_summary += "\n"; + } + core_dump_summary += "Backtrace:"; + for (size_t idx = 0; idx < details.exc_bt_info.depth; idx++) + { + core_dump_summary += + StringPrintf(" 0x%08x", details.exc_bt_info.bt[idx]); + if (details.exc_bt_info.corrupted) + { + core_dump_summary += "(corrupted)"; + } + } + core_dump_summary += "\n"; + LOG_ERROR("Core dump:\n%s", core_dump_summary.c_str()); + if (output_path) + { + write_string_to_file(output_path, core_dump_summary); + } + } + } + } + + /// Utility method to cleanup a core dump. + static void cleanup() + { + if (is_present()) + { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_core_dump_image_erase()); + } + } +}; + +} // namespace openmrn_arduino + +using openmrn_arduino::Esp32CoreDumpUtil; + +#endif // _FREERTOS_DRIVERS_ESP32_ESP32COREDUMPUTIL_HXX_ \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32Gpio.hxx b/src/freertos_drivers/esp32/Esp32Gpio.hxx new file mode 100644 index 000000000..3a58f8fcc --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32Gpio.hxx @@ -0,0 +1,749 @@ +/** \copyright + * Copyright (c) 2020, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32Gpio.hxx + * + * Helper declarations for using GPIO pins via the ESP-IDF APIs. + * + * @author Mike Dunston + * @date 27 March 2020 + */ + +#ifndef _DRIVERS_ESP32GPIO_HXX_ +#define _DRIVERS_ESP32GPIO_HXX_ + +#include "freertos_drivers/arduino/GpioWrapper.hxx" +#include "os/Gpio.hxx" +#include "utils/logging.h" +#include "utils/macros.h" + +#include +#include +#include +#include + +// esp_rom_gpio.h is a target agnostic replacement for esp32/rom/gpio.h +#if __has_include() +#include +#elif ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,0,0) +#include +#else +#include +#endif + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) +#include +#endif + +#if defined(CONFIG_IDF_TARGET_ESP32C3) +/// Helper macro to test if a pin has been configured for output. +/// +/// This is necessary since ESP-IDF does not expose gpio_get_direction(pin). +#define IS_GPIO_OUTPUT(pin) (GPIO_IS_VALID_OUTPUT_GPIO(pin) && \ + GPIO.enable.data & BIT(pin & 25)) +#else // NOT ESP32-C3 +/// Helper macro to test if a pin has been configured for output. +/// +/// This is necessary since ESP-IDF does not expose gpio_get_direction(pin). +#define IS_GPIO_OUTPUT(pin) (GPIO_IS_VALID_OUTPUT_GPIO(pin) && \ + (pin < 32) ? GPIO.enable & BIT(pin & 31) : \ + GPIO.enable1.data & BIT(pin & 31)) +#endif // CONFIG_IDF_TARGET_ESP32C3 + +template struct GpioOutputPin; +template struct GpioInputPin; + +/// Defines a GPIO output pin. Writes to this structure will change the output +/// level of the pin. Reads will return the pin's current level. +/// +/// The pin is set to output at initialization time, with the level defined by +/// `SAFE_VALUE'. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +class Esp32Gpio : public Gpio +{ +public: +#if CONFIG_IDF_TARGET_ESP32S3 + static_assert(PIN_NUM >= 0 && PIN_NUM <= 48, "Valid pin range is 0..48."); + static_assert(!(PIN_NUM >= 22 && PIN_NUM <= 25) + , "Pin does not exist"); + static_assert(!(PIN_NUM >= 26 && PIN_NUM <= 32) + , "Pin is reserved for flash usage."); +#if defined(CONFIG_SPIRAM_MODE_OCT) || defined(CONFIG_ESPTOOLPY_OCT_FLASH) + static_assert(!(PIN_NUM >= 33 && PIN_NUM <= 37)), + "Pin is not available when Octal SPI mode is enabled."); +#endif // ESP32S3 with Octal SPI +#elif CONFIG_IDF_TARGET_ESP32S2 + static_assert(PIN_NUM >= 0 && PIN_NUM <= 46, "Valid pin range is 0..46."); + static_assert(!(PIN_NUM >= 22 && PIN_NUM <= 25) + , "Pin does not exist"); + static_assert(!(PIN_NUM >= 26 && PIN_NUM <= 32) + , "Pin is reserved for flash usage."); +#elif CONFIG_IDF_TARGET_ESP32C3 + static_assert(PIN_NUM >= 0 && PIN_NUM <= 21, "Valid pin range is 0..21."); + // these pins are connected to the embedded flash and are not exposed on + // the DevKitM-1 board. + static_assert(!(PIN_NUM >= 11 && PIN_NUM <= 17) + , "Pin is reserved for flash usage."); +#else // ESP32 + static_assert(PIN_NUM >= 0 && PIN_NUM <= 39, "Valid pin range is 0..39."); + static_assert(PIN_NUM != 24, "Pin does not exist"); + static_assert(!(PIN_NUM >= 28 && PIN_NUM <= 31), "Pin does not exist"); + // Most commercially available boards include a capacitor on these two + // pins, however, there is no available define that can be used to allow + // usage of these when building for a custom PCB using the bare QFN chip. + // static_assert(PIN_NUM != 37, "Pin is connected to GPIO 36 via capacitor."); + // static_assert(PIN_NUM != 38, "Pin is connected to GPIO 39 via capacitor."); +#if defined(ESP32_PICO) + static_assert(!(PIN_NUM >= 6 && PIN_NUM <= 8) + , "Pin is reserved for flash usage."); + static_assert(PIN_NUM != 11 && PIN_NUM != 16 && PIN_NUM != 17 + , "Pin is reserved for flash usage."); +#else + static_assert(!(PIN_NUM >= 6 && PIN_NUM <= 11) + , "Pin is reserved for flash usage."); +#if defined(BOARD_HAS_PSRAM) + static_assert(PIN_NUM != 16 && PIN_NUM != 17 + , "Pin is reserved for PSRAM usage."); +#endif // BOARD_HAS_PSRAM +#endif // ESP32_PICO +#endif // CONFIG_IDF_TARGET_ESP32S2 / CONFIG_IDF_TARGET_ESP32S3 + + /// Sets the output state of the connected GPIO pin. + /// + /// @param new_state State to set the GPIO pin to. + void write(Value new_state) const override + { + if (INVERTED) + { + LOG(VERBOSE, "Esp32Gpio(%d) write %s", PIN_NUM, + new_state == Value::SET ? "CLR" : "SET"); + ESP_ERROR_CHECK(gpio_set_level(PIN_NUM, new_state == Value::CLR)); + } + else + { + LOG(VERBOSE, "Esp32Gpio(%d) write %s", PIN_NUM, + new_state == Value::SET ? "SET" : "CLR"); + ESP_ERROR_CHECK(gpio_set_level(PIN_NUM, new_state)); + } + } + + /// Reads the current state of the connected GPIO pin. + /// @return @ref SET if currently high, @ref CLR if currently low. + Value read() const override + { + return (Value)gpio_get_level(PIN_NUM); + } + + /// Sets output to HIGH. + void set() const override + { + write(Value::SET); + } + + /// Sets output to LOW. + void clr() const override + { + write(Value::CLR); + } + + /// Sets the direction of the connected GPIO pin. + void set_direction(Direction dir) const override + { + if (dir == Direction::DOUTPUT) + { + HASSERT(GPIO_IS_VALID_OUTPUT_GPIO(PIN_NUM)); + // using GPIO_MODE_INPUT_OUTPUT instead of GPIO_MODE_OUTPUT so that + // we can read the IO state + ESP_ERROR_CHECK( + gpio_set_direction(PIN_NUM, GPIO_MODE_INPUT_OUTPUT)); + LOG(VERBOSE, "Esp32Gpio(%d) configured as OUTPUT", PIN_NUM); + } + else + { + ESP_ERROR_CHECK(gpio_set_direction(PIN_NUM, GPIO_MODE_INPUT)); + LOG(VERBOSE, "Esp32Gpio(%d) configured as INPUT", PIN_NUM); + } + } + + /// Gets the GPIO direction. + /// @return @ref DINPUT or @ref DOUTPUT + Direction direction() const override + { + if (IS_GPIO_OUTPUT(PIN_NUM)) + { + return Direction::DOUTPUT; + } + return Direction::DINPUT; + } +private: + template friend struct GpioOutputPin; + template friend struct GpioInputPin; + /// Static instance variable that can be used for libraries expecting a + /// generic Gpio pointer. This instance variable will be initialized by the + /// linker and (assuming the application developer initialized the hardware + /// pins in hw_preinit) is accessible, including virtual methods at static + /// constructor time. + static const Esp32Gpio instance_; +}; + +/// Defines the linker symbol for the wrapped Gpio instance. +template +const Esp32Gpio Esp32Gpio::instance_; + +/// Parametric GPIO output class. +/// @param Defs is the GPIO pin's definition base class, supplied by the +/// GPIO_PIN macro. +/// @param SAFE_VALUE is the initial value for the GPIO output pin. +/// @param INVERT inverts the high/low state of the pin when set. +template +struct GpioOutputPin : public Defs +{ +public: + using Defs::PIN_NUM; +// compile time sanity check that the selected pin is valid for output. +#if CONFIG_IDF_TARGET_ESP32S2 + static_assert(PIN_NUM != 46, "Pin 46 can not be used for output."); +#elif CONFIG_IDF_TARGET_ESP32 + static_assert(PIN_NUM < 34, "Pins 34 and above can not be used as output."); +#endif // CONFIG_IDF_TARGET_ESP32S2 / CONFIG_IDF_TARGET_ESP32S3 + + /// Initializes the hardware pin. + static void hw_init() + { + LOG(VERBOSE, + "[Esp32Gpio] Configuring output pin %d, default value: %d", + PIN_NUM, SAFE_VALUE); +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,3,0) + gpio_pad_select_gpio(PIN_NUM); +#else // IDF v4.4 (or later) + esp_rom_gpio_pad_select_gpio(PIN_NUM); +#endif // IDF v4.3 (or earlier) + gpio_config_t cfg; + memset(&cfg, 0, sizeof(gpio_config_t)); + cfg.pin_bit_mask = BIT64(PIN_NUM); + // using GPIO_MODE_INPUT_OUTPUT instead of GPIO_MODE_OUTPUT so that + // we can read the IO state + cfg.mode = GPIO_MODE_INPUT_OUTPUT; + ESP_ERROR_CHECK(gpio_config(&cfg)); + ESP_ERROR_CHECK(gpio_set_level(PIN_NUM, SAFE_VALUE)); + } + + /// Sets the hardware pin to a safe value. + static void hw_set_to_safe() + { + ESP_ERROR_CHECK(gpio_set_level(PIN_NUM, SAFE_VALUE)); + } + + /// Toggles the state of the pin to the opposite of what it is currently. + static void toggle() + { + instance()->write(!instance()->read()); + } + + /// Sets the output pin @param value if true, output is set to HIGH, if + /// false, output is set to LOW. + static void set(bool value) + { + instance()->write(value); + } + + /// @return static Gpio object instance that controls this output pin. + static constexpr const Gpio *instance() + { + return &Esp32Gpio::instance_; + } +}; + +/// Defines a GPIO output pin, initialized to be an output pin with low level. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeLow : public GpioOutputPin +{ +}; + +/// Defines a GPIO output pin, initialized to be an output pin with low +/// level. All set() commands are acted upon by inverting the value. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeLowInvert : public GpioOutputPin +{ +}; + +/// Defines a GPIO output pin, initialized to be an output pin with high level. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeHigh : public GpioOutputPin +{ +}; + +/// Defines a GPIO output pin, initialized to be an output pin with high +/// level. All set() commands are acted upon by inverting the value. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template +struct GpioOutputSafeHighInvert : public GpioOutputPin +{ +}; + +/// Parametric GPIO input class. +/// @param Defs is the GPIO pin's definition base class, supplied by the +/// GPIO_PIN macro. +/// @param PUEN is true if the pull-up should be enabled. +/// @param PDEN is true if the pull-down should be enabled. +template struct GpioInputPin : public Defs +{ +public: + using Defs::PIN_NUM; +#if CONFIG_IDF_TARGET_ESP32S2 + // GPIO 45 and 46 typically have pull-down resistors. + static_assert(!PUEN || (PUEN && (PIN_NUM != 45 && PIN_NUM != 46)), + "GPIO 45 and 46 typically have built-in pull-down " + "resistors, enabling pull-up is not possible."); + // GPIO 0 typically has a pull-up resistor + static_assert(!PDEN || (PDEN && PIN_NUM != 0), + "GPIO 0 typically has a built-in pull-up resistors, " + "enabling pull-down is not possible."); +#elif CONFIG_IDF_TARGET_ESP32S3 + // GPIO 0 typically has a pull-up resistor + static_assert(!PDEN || (PDEN && PIN_NUM != 0), + "GPIO 0 typically has a built-in pull-up resistors, " + "enabling pull-down is not possible."); +#elif CONFIG_IDF_TARGET_ESP32C3 + // GPIO 9 typically has a pull-up resistor + static_assert(!PDEN || (PDEN && PIN_NUM != 9), + "GPIO 9 typically has a built-in pull-up resistors, " + "enabling pull-down is not possible."); +#else // ESP32 + // GPIO 2, 4 and 12 typically have pull-down resistors. + static_assert(!PUEN || + (PUEN && (PIN_NUM != 2 && PIN_NUM != 4 && PIN_NUM != 12)), + "GPIO 2, 4, 12 typically have built-in pull-down resistors, " + "enabling pull-up is not possible."); + // GPIO 0, 5 and 15 typically have pull-up resistors. + static_assert(!PDEN || + (PDEN && (PIN_NUM != 0 && PIN_NUM != 5 && PIN_NUM == 15)), + "GPIO 0, 5, 15 typically have built-in pull-up resistors, " + "enabling pull-down is not possible."); +#endif // CONFIG_IDF_TARGET_ESP32S2 + /// Initializes the hardware pin. + static void hw_init() + { + LOG(VERBOSE, "[Esp32Gpio] Configuring input pin %d, PUEN: %d, PDEN: %d", + PIN_NUM, PUEN, PDEN); +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,3,0) + gpio_pad_select_gpio(PIN_NUM); +#else // IDF v4.4 (or later) + esp_rom_gpio_pad_select_gpio(PIN_NUM); +#endif // IDF v4.3 (or earlier) + gpio_config_t cfg; + memset(&cfg, 0, sizeof(gpio_config_t)); + cfg.pin_bit_mask = BIT64(PIN_NUM); + // using GPIO_MODE_INPUT_OUTPUT instead of GPIO_MODE_OUTPUT so that + // we can read the IO state + cfg.mode = GPIO_MODE_INPUT; + if (PUEN) + { + cfg.pull_up_en = GPIO_PULLUP_ENABLE; + } + if (PDEN) + { + cfg.pull_down_en = GPIO_PULLDOWN_ENABLE; + } + ESP_ERROR_CHECK(gpio_config(&cfg)); + } + /// Sets the hardware pin to a safe state. + static void hw_set_to_safe() + { + hw_init(); + } + + /// @return static Gpio object instance that controls this output pin. + static constexpr const Gpio *instance() + { + return &Esp32Gpio::instance_; + } +}; + +/// Defines a GPIO input pin with pull-up and pull-down disabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputNP : public GpioInputPin +{ +}; + +/// Defines a GPIO input pin with pull-up enabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputPU : public GpioInputPin +{ +}; + +/// Defines a GPIO input pin with pull-down enabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputPD : public GpioInputPin +{ +}; + +/// Defines a GPIO input pin with pull-up and pull-down enabled. +/// +/// Do not use this class directly. Use @ref GPIO_PIN instead. +template struct GpioInputPUPD : public GpioInputPin +{ +}; + +/// Defines an ADC input pin. +/// +/// Do not use this class directly. Use @ref ADC_PIN instead. +template struct Esp32ADCInput : public Defs +{ +public: + using Defs::CHANNEL; + using Defs::PIN; + using Defs::ATTEN; + using Defs::BITS; +#if CONFIG_IDF_TARGET_ESP32 + static const adc_unit_t UNIT = PIN >= 30 ? ADC_UNIT_1 : ADC_UNIT_2; +#elif CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + static const adc_unit_t UNIT = PIN <= 10 ? ADC_UNIT_1 : ADC_UNIT_2; +#elif CONFIG_IDF_TARGET_ESP32C3 + static const adc_unit_t UNIT = PIN <= 4 ? ADC_UNIT_1 : ADC_UNIT_2; +#endif + static void hw_init() + { + LOG(VERBOSE, + "[Esp32ADCInput] Configuring ADC%d:%d input pin %d, " + "attenuation %d, bits %d", + UNIT + 1, CHANNEL, PIN, ATTEN, BITS); + + if (UNIT == ADC_UNIT_1) + { + ESP_ERROR_CHECK(adc1_config_width(BITS)); + ESP_ERROR_CHECK( + adc1_config_channel_atten((adc1_channel_t)CHANNEL, ATTEN)); + } + else + { + ESP_ERROR_CHECK( + adc2_config_channel_atten((adc2_channel_t)CHANNEL, ATTEN)); + } + } + + /// NO-OP + static void hw_set_to_safe() + { + // NO-OP + } + + /// NO-OP + static void set(bool value) + { + // NO-OP + } + + static int sample() + { + int value = 0; + if (UNIT == ADC_UNIT_1) + { + value = adc1_get_raw((adc1_channel_t)CHANNEL); + } + else + { + ESP_ERROR_CHECK( + adc2_get_raw((adc2_channel_t)CHANNEL, BITS, &value)); + } + return value; + } +}; + +/// Helper macro for defining GPIO pins on the ESP32. +/// +/// @param NAME is the basename of the declaration. For NAME==FOO the macro +/// declared FOO_Pin as a structure on which the read-write functions will be +/// available. +/// +/// @param BaseClass is the initialization structure, such as @ref LedPin, or +/// @ref GpioOutputSafeHigh or @ref GpioOutputSafeLow. +/// +/// @param NUM is the pin number, such as 3 (see below for usable range). +/// +/// There are multiple variations available for the ESP32: ESP32, WROVER, +/// WROVER-B, PICO, ESP32-Solo, ESP32-S2, ESP32-C3. Each of these have slight +/// differences in the available pins. +/// +/// ESP32: Valid pin range is 0..39 with the following restrictions: +/// - 0 : Bootstrap pin, pull-up resistor on most modules. +/// - 1 : UART0 TX, serial console. +/// - 2 : Bootstrap pin, pull-down resistor on most modules. +/// - 3 : UART0 RX, serial console. +/// - 4 : Pull-down resistor on most modules. +/// - 5 : Bootstrap pin, pull-up resistor on most modules. +/// - 6 - 11 : Used for on-board flash. If you have the PICO-D4 see the +/// section below. +/// - 12 : Bootstrap pin, pull-down resistor on most modules. +/// - 15 : Bootstrap pin, pull-up resistor on most modules. +/// - 24 : Does not exist. +/// - 37, 38 : Not exposed on most modules and will have a capacitor +/// connected to 36 and 39 under the metal shielding of the +/// module. The capacitor is typically 270pF. +/// - 34 - 39 : These pins are INPUT only. +/// NOTE: ESP32 covers the ESP32-WROOM-32, DOWD, D2WD, S0WD, U4WDH and the +/// ESP32-Solo. +/// +/// ESP32-PICO-D4: Nearly the same as ESP32 but with the following differences: +/// - 9, 10 : Available for use, other modules use these for the on-board +/// flash. +/// - 16, 17 : Used for flash and/or PSRAM. +/// +/// ESP32-PICO-V3: Nearly the same as ESP32-PICO-D4 with the following +/// differences: +/// - 7, 8 : Available for use, however validation checks will prevent +/// usage due to no compile time constant available to +/// uniquely identify this variant. +/// - 20 : Available for use. +/// - 16 - 18, 23: Not available, however validations checks will not +/// prevent usage of GPIO 18 or 23 due to no compile time +/// constant available to uniquely identify this variant. +/// +/// ESP32-WROVER and WROVER-B: Nearly the same as ESP32 but with the following +/// differences: +/// - 16, 17 : Reserved for PSRAM on WROVER/WROVER-B modules. +/// +/// ESP32-S2: Valid pin range is 0..46 with the following notes: +/// - 0 : Bootstrap pin, pull-up resistor on most modules. +/// - 18 : Most modules have an RGB LED on this pin. +/// - 19 : USB OTG D-, available to use if not using this +/// functionality. +/// - 20 : USB OTG D+, available to use if not using this +/// functionality. +/// - 22 - 25 : Do not exist. +/// - 26 - 32 : Used for on-board flash and/or PSRAM. +/// - 43 : UART0 TX, serial console. +/// - 44 : UART0 RX, serial console. +/// - 45 : Bootstrap pin, pull-down resistor on most modules. Note: +/// ESP32-S2-Kaluga-1 modules have an RGB LED on this pin that +/// can be enabled via a jumper. +/// - 46 : Bootstrap pin, pull-down resistor on most modules, INPUT only. +/// +/// ESP32-C3: Valid pin range is 0..21 with the following notes: +/// - 8 : Bootstrap pin, most modules have an RGB LED on this pin. +/// - 9 : Bootstrap pin, connected to built-in pull-up resistor, may +/// also have an external pull-up resistor. +/// - 11 - 17 : Used for flash and/or PSRAM. +/// - 18 : USB CDC-ACM D- / JTAG, available to use if not using this +/// functionality. +/// - 19 : USB CDC ACM D+ / JTAG, available to use if not using this +/// functionality. +/// - 20 : UART0 RX, serial console. +/// - 21 : UART0 TX, serial console. +/// +/// ESP8685: This is an ESP32-C3 in a smaller package without SPI pins exposed. +/// +/// ESP32-S3: Valid pin range is 0..48 with the following notes: +/// - 0 : Bootstrap pin, pull-up resistor on most modules. +/// - 18 : Most modules have an RGB LED on this pin. +/// - 19 : USB OTG D-, available to use if not using this +/// functionality. +/// - 20 : USB OTG D+, available to use if not using this +/// functionality. +/// - 22 - 25 : Do not exist. +/// - 26 - 32 : Used for on-board flash and/or PSRAM. +/// - 33 - 37 : Used for on-board flash and/or PSRAM if Octal SPI mode is +/// enabled. +/// - 39 - 42 : JTAG interface (pins in order: MTCK, MTDO, MTDI, MTMS) +/// - 43 : UART0 TX, serial console. +/// - 44 : UART0 RX, serial console. +/// - 45 : Bootstrap pin, pull-down resistor on most modules. +/// - 46 : Bootstrap pin, pull-down resistor on most modules. +/// - 48 : Most modules have an RGB LED on this pin. +/// +/// Pins marked as having a pull-up or pull-down resistor are typically 10kOhm. +/// +/// Any pins marked as a bootstrap pin can alter the behavior of the ESP32 if +/// they are not in the default/expected state on startup. Consult the +/// schematic and/or datasheet for more details. +/// +/// The built in pull-up/pull-down resistor for all ESP32 variants are +/// typically 45kOhm. +/// +/// SoC datasheet references: +/// ESP32: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf +/// ESP32-WROVER: https://www.espressif.com/sites/default/files/documentation/esp32-wrover_datasheet_en.pdf +/// ESP32-WROVER-B: https://www.espressif.com/sites/default/files/documentation/esp32-wrover-b_datasheet_en.pdf +/// ESP32-PICO-D4: https://www.espressif.com/sites/default/files/documentation/esp32-pico-d4_datasheet_en.pdf +/// ESP32-PICO-V3: https://www.espressif.com/sites/default/files/documentation/esp32-pico-v3_datasheet_en.pdf +/// ESP32-S2: https://www.espressif.com/sites/default/files/documentation/esp32-s2_datasheet_en.pdf +/// ESP32-S2-WROVER: https://www.espressif.com/sites/default/files/documentation/esp32-s2-wrover_esp32-s2-wrover-i_datasheet_en.pdf +/// ESP32-C3: https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf +/// ESP32-S3: https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_en.pdf +/// ESP8685: https://www.espressif.com/sites/default/files/documentation/esp8685_datasheet_en.pdf +/// +/// SoC technical references: +/// ESP32: https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf +/// ESP32-S2: https://www.espressif.com/sites/default/files/documentation/esp32-s2_technical_reference_manual_en.pdf +/// ESP32-C3: https://www.espressif.com/sites/default/files/documentation/esp32-c3_technical_reference_manual_en.pdf +/// ESP32-S3: https://www.espressif.com/sites/default/files/documentation/esp32-s3_technical_reference_manual_en.pdf +/// +/// Module schematic references: +/// DevKitC v4: https://dl.espressif.com/dl/schematics/esp32_devkitc_v4-sch-20180607a.pdf +/// DevKitC v2: https://dl.espressif.com/dl/schematics/ESP32-Core-Board-V2_sch.pdf +/// WROVER KIT v4.1: https://dl.espressif.com/dl/schematics/ESP-WROVER-KIT_V4_1.pdf +/// WROVER KIT v3: https://dl.espressif.com/dl/schematics/ESP-WROVER-KIT_SCH-3.pdf +/// WROVER KIT v2: https://dl.espressif.com/dl/schematics/ESP-WROVER-KIT_SCH-2.pdf +/// WROVER KIT v1: https://dl.espressif.com/dl/schematics/ESP32-DevKitJ-v1_sch.pdf +/// PICO KIT v4.1: https://dl.espressif.com/dl/schematics/esp32-pico-kit-v4.1_schematic.pdf +/// PICO KIT v3: https://dl.espressif.com/dl/schematics/esp32-pico-kit-v3_schematic.pdf +/// ESP32-S2-Saola-1: https://dl.espressif.com/dl/schematics/ESP32-S2-SAOLA-1_V1.1_schematics.pdf +/// ESP32-S2-Kaluga-1: https://dl.espressif.com/dl/schematics/ESP32-S2-Kaluga-1_V1_3_SCH_20200526A.pdf +/// ESP32-C3-DevKitM-1: https://dl.espressif.com/dl/schematics/SCH_ESP32-C3-DEVKITM-1_V1_20200915A.pdf +/// +/// NOTE: The WROVER KIT v1 is also known as DevKitJ and is RED colored PCB +/// that supports both WROVER and WROOM-32 modules. +/// +/// Example: +/// GPIO_PIN(FOO, GpioOutputSafeLow, 3); +/// ... +/// FOO_Pin::set(true); +#define GPIO_PIN(NAME, BaseClass, NUM) \ + struct NAME##Defs \ + { \ + static const gpio_num_t PIN_NUM = (gpio_num_t)NUM; \ + public: \ + static const gpio_num_t pin() \ + { \ + return PIN_NUM; \ + } \ + }; \ + typedef BaseClass NAME##_Pin + +/// Helper macro for an ADC GPIO input on the ESP32. +/// +/// @param NAME is the basename of the declaration. For NAME==FOO the macro +/// declared FOO_Pin as a structure on which the read-write functions will be +/// available. +/// @param ADC_CHANNEL is the ADC channel to configure. +/// @param ATTENUATION is the voltage range for the ADC input. +/// @param BIT_RANGE is the bit range to configure the ADC to use. +/// +/// Supported ATTENUATION values and voltage ranges: +/// ADC_ATTEN_DB_0 - 0dB attenuaton gives full-scale voltage 1.1V +/// ADC_ATTEN_DB_2_5 - 2.5dB attenuation gives full-scale voltage 1.5V +/// ADC_ATTEN_DB_6 - 6dB attenuation gives full-scale voltage 2.2V +/// ADC_ATTEN_DB_11 - 11dB attenuation gives full-scale voltage 3.9V +/// +/// Supported BIT_RANGE values and ADC sample values: +/// ADC_WIDTH_BIT_9 - 0-511 +/// ADC_WIDTH_BIT_10 - 0-1023 +/// ADC_WIDTH_BIT_11 - 0-2047 +/// ADC_WIDTH_BIT_12 - 0-4065 +/// ADC_WIDTH_BIT_13 - 0-8191 -- Only valid on the ESP32-S2 and ESP32-S3. +/// NOTE: When using ADC1_CHANNEL_X this bit range will be applied to all +/// ADC1 channels, it is not recommended to mix values for ADC1 channels. +/// +/// Supported ADC_CHANNEL values and pin assignments for the ESP32: +/// ADC1_CHANNEL_0 : 36 +/// ADC1_CHANNEL_1 : 37 -- NOTE: Not recommended for use, see note below. +/// ADC1_CHANNEL_2 : 38 -- NOTE: Not recommended for use, see note below. +/// ADC1_CHANNEL_3 : 39 +/// ADC1_CHANNEL_4 : 32 +/// ADC1_CHANNEL_5 : 33 +/// ADC1_CHANNEL_6 : 34 +/// ADC1_CHANNEL_7 : 35 +/// ADC2_CHANNEL_0 : 4 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_1 : 0 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_2 : 2 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_3 : 15 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_4 : 13 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_5 : 12 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_6 : 14 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_7 : 27 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_8 : 25 -- NOTE: Not usable when WiFi is active. +/// ADC2_CHANNEL_9 : 29 -- NOTE: Not usable when WiFi is active. +/// NOTE: ADC1_CHANNEL_1 and ADC1_CHANNEL_2 typically have a capacitor which +/// connects to ADC1_CHANNEL_0 or ADC1_CHANNEL_3. The only known exception to +/// this is for some ESP32-PICO-D4/ESP32-PICO-V3 based boards, confirm on the +/// board schematic before using these pins. +/// +/// Supported ADC_CHANNEL values and pin assignments for the ESP32-S2/ESP32-S3: +/// ADC1_CHANNEL_0 : 1 +/// ADC1_CHANNEL_1 : 2 +/// ADC1_CHANNEL_2 : 3 +/// ADC1_CHANNEL_3 : 4 +/// ADC1_CHANNEL_4 : 5 +/// ADC1_CHANNEL_5 : 6 +/// ADC1_CHANNEL_6 : 7 +/// ADC1_CHANNEL_7 : 8 +/// ADC1_CHANNEL_8 : 9 +/// ADC1_CHANNEL_9 : 10 +/// ADC2_CHANNEL_0 : 11 +/// ADC2_CHANNEL_1 : 12 +/// ADC2_CHANNEL_2 : 13 +/// ADC2_CHANNEL_3 : 14 +/// ADC2_CHANNEL_4 : 15 +/// ADC2_CHANNEL_5 : 16 +/// ADC2_CHANNEL_6 : 17 +/// ADC2_CHANNEL_7 : 18 +/// ADC2_CHANNEL_8 : 19 -- NOTE: This pin is also used for USB PHY (D-). +/// ADC2_CHANNEL_9 : 20 -- NOTE: This pin is also used for USB PHY (D+). +/// +/// Supported ADC_CHANNEL values and pin assignments for the ESP32-C3: +/// ADC1_CHANNEL_0 : 0 +/// ADC1_CHANNEL_1 : 1 +/// ADC1_CHANNEL_2 : 2 +/// ADC1_CHANNEL_3 : 3 +/// ADC1_CHANNEL_4 : 4 +/// ADC2_CHANNEL_0 : 5 +/// +/// Example: +/// ADC_PIN(SENSE, ADC1_CHANNEL_0, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12); +/// ... +/// int level = SENSE_Pin::sample(); +#define ADC_PIN(NAME, ADC_CHANNEL, ATTENUATION, BIT_RANGE) \ + struct NAME##Defs \ + { \ + static const adc_channel_t CHANNEL = (adc_channel_t)ADC_CHANNEL; \ + static const gpio_num_t PIN = (gpio_num_t)ADC_CHANNEL##_GPIO_NUM; \ + static const adc_atten_t ATTEN = (adc_atten_t)ATTENUATION; \ + static const adc_bits_width_t BITS = (adc_bits_width_t)BIT_RANGE; \ + public: \ + static const gpio_num_t pin() \ + { \ + return PIN; \ + } \ + static const adc_channel_t channel() \ + { \ + return CHANNEL; \ + } \ + }; \ + typedef Esp32ADCInput NAME##_Pin + +#endif // _DRIVERS_ESP32GPIO_HXX_ \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx b/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx index 8c9c88845..3a2123289 100644 --- a/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx +++ b/src/freertos_drivers/esp32/Esp32HardwareCanAdapter.hxx @@ -38,22 +38,60 @@ #ifndef _FREERTOS_DRIVERS_ESP32_ESP32HWCAN_HXX_ #define _FREERTOS_DRIVERS_ESP32_ESP32HWCAN_HXX_ +namespace openmrn_arduino +{ + #include "freertos_drivers/arduino/Can.hxx" +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,2,0) +#include +#else // NOT IDF v4.2+ #include + +// The following types and APIs are created as aliases as a compatibility for +// IDF v4.3+ which breaks due to driver/twai.h on the ESP32 attempting to +// override a few types created as part of can_ioctl.h +typedef can_timing_config_t twai_timing_config_t; +typedef can_filter_config_t twai_filter_config_t; +typedef can_general_config_t twai_general_config_t; +typedef can_status_info_t twai_status_info_t; +typedef can_message_t twai_message_t; +#define TWAI_TIMING_CONFIG_125KBITS CAN_TIMING_CONFIG_125KBITS +#define TWAI_FILTER_CONFIG_ACCEPT_ALL CAN_FILTER_CONFIG_ACCEPT_ALL +#define TWAI_MODE_NORMAL CAN_MODE_NORMAL +#define TWAI_IO_UNUSED CAN_IO_UNUSED +#define TWAI_ALERT_NONE CAN_ALERT_NONE +#define TWAI_STATE_BUS_OFF CAN_STATE_BUS_OFF +#define TWAI_STATE_RECOVERING CAN_STATE_RECOVERING +#define TWAI_MSG_FLAG_NONE CAN_MSG_FLAG_NONE +#define TWAI_MSG_FLAG_EXTD CAN_MSG_FLAG_EXTD +#define TWAI_MSG_FLAG_RTR CAN_MSG_FLAG_RTR +#define TWAI_MSG_FLAG_DLC_NON_COMP CAN_MSG_FLAG_DLC_NON_COMP + +#define twai_driver_install can_driver_install +#define twai_start can_start +#define twai_stop can_stop +#define twai_get_status_info can_get_status_info +#define twai_initiate_recovery can_initiate_recovery +#define twai_transmit can_transmit +#define twai_receive can_receive + +#endif // IDF v4.2+ + #include #include -namespace openmrn_arduino { - /// ESP32 CAN bus status strings, used for periodic status reporting -static const char *ESP32_CAN_STATUS_STRINGS[] = { +static const char *ESP32_CAN_STATUS_STRINGS[] = +{ "STOPPED", // CAN_STATE_STOPPED "RUNNING", // CAN_STATE_RUNNING "OFF / RECOVERY NEEDED", // CAN_STATE_BUS_OFF "RECOVERY UNDERWAY" // CAN_STATE_RECOVERING }; -class Esp32HardwareCan : public Can +class Esp32HardwareCanDeprecated : public Can { public: /// Constructor. @@ -63,26 +101,26 @@ public: /// transceiver RX. /// @param txPin is the ESP32 pin that is connected to the external /// transceiver TX. - Esp32HardwareCan(const char *name, gpio_num_t rxPin, gpio_num_t txPin, - bool reportStats = true) + Esp32HardwareCanDeprecated(const char *name, gpio_num_t rxPin, + gpio_num_t txPin, bool reportStats = true) : Can(name) , reportStats_(reportStats) , overrunWarningPrinted_(false) { // Configure the ESP32 CAN driver to use 125kbps. - can_timing_config_t can_timing_config = CAN_TIMING_CONFIG_125KBITS(); + twai_timing_config_t can_timing_config = TWAI_TIMING_CONFIG_125KBITS(); // By default we accept all CAN frames. - can_filter_config_t can_filter_config = CAN_FILTER_CONFIG_ACCEPT_ALL(); + twai_filter_config_t can_filter_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); // Note: not using the CAN_GENERAL_CONFIG_DEFAULT macro due to a missing // cast for CAN_IO_UNUSED. - can_general_config_t can_general_config = {.mode = CAN_MODE_NORMAL, + twai_general_config_t can_general_config = {.mode = TWAI_MODE_NORMAL, .tx_io = txPin, .rx_io = rxPin, - .clkout_io = (gpio_num_t)CAN_IO_UNUSED, - .bus_off_io = (gpio_num_t)CAN_IO_UNUSED, + .clkout_io = (gpio_num_t)TWAI_IO_UNUSED, + .bus_off_io = (gpio_num_t)TWAI_IO_UNUSED, .tx_queue_len = (uint32_t)config_can_tx_buffer_size() / 2, .rx_queue_len = (uint32_t)config_can_rx_buffer_size() / 2, - .alerts_enabled = CAN_ALERT_NONE, + .alerts_enabled = TWAI_ALERT_NONE, .clkout_divider = 0}; LOG(VERBOSE, @@ -91,7 +129,7 @@ public: can_general_config.rx_io, can_general_config.tx_io, can_general_config.rx_queue_len, can_general_config.tx_queue_len); - ESP_ERROR_CHECK(can_driver_install( + ESP_ERROR_CHECK(twai_driver_install( &can_general_config, &can_timing_config, &can_filter_config)); xTaskCreatePinnedToCore(rx_task, "ESP32-CAN RX", OPENMRN_STACK_SIZE, @@ -100,21 +138,21 @@ public: this, TX_TASK_PRIORITY, &txTaskHandle_, tskNO_AFFINITY); } - ~Esp32HardwareCan() + ~Esp32HardwareCanDeprecated() { } /// Enables the ESP32 CAN driver virtual void enable() { - ESP_ERROR_CHECK(can_start()); + ESP_ERROR_CHECK(twai_start()); LOG(VERBOSE, "ESP32-CAN driver enabled"); } /// Disables the ESP32 CAN driver virtual void disable() { - ESP_ERROR_CHECK(can_stop()); + ESP_ERROR_CHECK(twai_stop()); LOG(VERBOSE, "ESP32-CAN driver disabled"); } @@ -128,7 +166,7 @@ protected: private: /// Default constructor. - Esp32HardwareCan(); + Esp32HardwareCanDeprecated(); /// Enables/Disables the periodic reporting of CAN bus statistics to the /// default serial stream. @@ -165,9 +203,10 @@ private: /// status reporting and BUS recovery when necessary. static void tx_task(void *can) { - /// Get handle to our parent Esp32HardwareCan object to access the - /// txBuf. - Esp32HardwareCan *parent = reinterpret_cast(can); + /// Get handle to our parent Esp32HardwareCanDeprecated object to + /// access the txBuf. + Esp32HardwareCanDeprecated *parent = + reinterpret_cast(can); #if CONFIG_TASK_WDT // Add this task to the WDT @@ -187,8 +226,8 @@ private: // periodic CAN driver monitoring and reporting, this takes care of // bus recovery when the CAN driver disables the bus due to error // conditions exceeding thresholds. - can_status_info_t status; - can_get_status_info(&status); + twai_status_info_t status; + twai_get_status_info(&status); auto current_tick_count = xTaskGetTickCount(); if (next_status_display_tick_count == 0 || current_tick_count >= next_status_display_tick_count) @@ -209,15 +248,15 @@ private: } parent->overrunWarningPrinted_ = false; } - if (status.state == CAN_STATE_BUS_OFF) + if (status.state == TWAI_STATE_BUS_OFF) { // When the bus is OFF we need to initiate recovery, transmit is // not possible when in this state. LOG(WARNING, "ESP32-CAN: initiating recovery"); - can_initiate_recovery(); + twai_initiate_recovery(); continue; } - else if (status.state == CAN_STATE_RECOVERING) + else if (status.state == TWAI_STATE_RECOVERING) { // when the bus is in recovery mode transmit is not possible. vTaskDelay(TX_DEFAULT_DELAY); @@ -240,10 +279,10 @@ private: } /// ESP32 native CAN driver frame - can_message_t msg; - bzero(&msg, sizeof(can_message_t)); + twai_message_t msg; + memset(&msg, 0, sizeof(twai_message_t)); - msg.flags = CAN_MSG_FLAG_NONE; + msg.flags = TWAI_MSG_FLAG_NONE; msg.identifier = can_frame->can_id; msg.data_length_code = can_frame->can_dlc; for (int i = 0; i < can_frame->can_dlc; i++) @@ -252,11 +291,11 @@ private: } if (IS_CAN_FRAME_EFF(*can_frame)) { - msg.flags |= CAN_MSG_FLAG_EXTD; + msg.flags |= TWAI_MSG_FLAG_EXTD; } if (IS_CAN_FRAME_RTR(*can_frame)) { - msg.flags |= CAN_MSG_FLAG_RTR; + msg.flags |= TWAI_MSG_FLAG_RTR; } // Pass the converted CAN frame to the native driver @@ -265,7 +304,7 @@ private: // the message being left in txBuf for the next iteration. // if this call returns ESP_OK we consider the frame as // transmitted by the driver and remove it from txBuf. - esp_err_t tx_res = can_transmit(&msg, pdMS_TO_TICKS(100)); + esp_err_t tx_res = twai_transmit(&msg, pdMS_TO_TICKS(100)); if (tx_res == ESP_OK) { LOG(VERBOSE, @@ -291,8 +330,10 @@ private: /// a @ref can_frame and pushing them to the @ref rxBuf. static void rx_task(void *can) { - /// Get handle to our parent Esp32HardwareCan object to access the rxBuf - Esp32HardwareCan *parent = reinterpret_cast(can); + /// Get handle to our parent Esp32HardwareCanDeprecated object to access + /// the rxBuf. + Esp32HardwareCanDeprecated *parent = + reinterpret_cast(can); #if CONFIG_TASK_WDT // Add this task to the WDT @@ -307,16 +348,16 @@ private: #endif // CONFIG_TASK_WDT /// ESP32 native CAN driver frame - can_message_t msg; - bzero(&msg, sizeof(can_message_t)); - if (can_receive(&msg, pdMS_TO_TICKS(250)) != ESP_OK) + twai_message_t msg; + memset(&msg, 0, sizeof(twai_message_t)); + if (twai_receive(&msg, pdMS_TO_TICKS(250)) != ESP_OK) { // native CAN driver did not give us a frame. continue; } // we have received a frame from the native CAN driver, verify if // it is a standard frame, if not we drop it. - if (msg.flags & CAN_MSG_FLAG_DLC_NON_COMP) + if (msg.flags & TWAI_MSG_FLAG_DLC_NON_COMP) { LOG(WARNING, "ESP32-CAN-RX: received non-compliant CAN frame, frame " @@ -354,11 +395,11 @@ private: { can_frame->data[i] = msg.data[i]; } - if (msg.flags & CAN_MSG_FLAG_EXTD) + if (msg.flags & TWAI_MSG_FLAG_EXTD) { SET_CAN_FRAME_EFF(*can_frame); } - if (msg.flags & CAN_MSG_FLAG_RTR) + if (msg.flags & TWAI_MSG_FLAG_RTR) { SET_CAN_FRAME_RTR(*can_frame); } @@ -366,9 +407,20 @@ private: parent->rxBuf->signal_condition(); } } - DISALLOW_COPY_AND_ASSIGN(Esp32HardwareCan); + DISALLOW_COPY_AND_ASSIGN(Esp32HardwareCanDeprecated); }; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) +/// Esp32HardwareCan has been deprecated due to lack of portability beyond the +/// ESP32. +/// @deprecated Use @ref Esp32HardwareTwai instead. +typedef Esp32HardwareCanDeprecated Esp32HardwareCan __attribute__ (( + deprecated("Esp32HardwareCan has been replaced with Esp32HardwareTwai."))); +#else +/// Esp32HardwareCan will be deprecated once arduino-esp32 2.0.0 has released. +typedef Esp32HardwareCanDeprecated Esp32HardwareCan; +#endif // IDF v4.3+ + } // namespace openmrn_arduino using openmrn_arduino::Esp32HardwareCan; diff --git a/src/freertos_drivers/esp32/Esp32HardwareTwai.cxx b/src/freertos_drivers/esp32/Esp32HardwareTwai.cxx new file mode 100644 index 000000000..2a1c62f20 --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32HardwareTwai.cxx @@ -0,0 +1,1035 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32HardwareTwai.cxx + * + * TWAI driver implementation for OpenMRN. This leverages the ESP-IDF TWAI HAL + * API rather than the TWAI driver to allow for a more integrated solution than + * the TWAI driver which requires polling for RX. This implementation supports + * both ::select and the non-blocking ::ioctl/::fnctl approach. + * + * @author Mike Dunston + * @date 1 May 2021 + */ + +// Ensure we only compile this code for the ESP32 family of MCUs and that the +// ESP-IDF version is supported for this code. +#if defined(ESP32) + +#include "sdkconfig.h" +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) + +#if CONFIG_VFS_SUPPORT_TERMIOS +// remove defines added by arduino-esp32 core/esp32/binary.h which are +// duplicated in sys/termios.h which may be included by esp_vfs.h +#undef B110 +#undef B1000000 +#endif // CONFIG_VFS_SUPPORT_TERMIOS + +#include +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) +#include +#else // IDF v4.x (or earlier) +#include +#endif // IDF v5+ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "can_frame.h" +#include "can_ioctl.h" +#include "executor/Notifiable.hxx" +#include "freertos_drivers/arduino/DeviceBuffer.hxx" +#include "freertos_drivers/esp32/Esp32HardwareTwai.hxx" +#include "utils/Atomic.hxx" +#include "utils/logging.h" + +namespace openmrn_arduino +{ + +/// Default file descriptor to return in the open() call. +/// +/// NOTE: The TWAI driver only supports one file descriptor at this time. +static constexpr int TWAI_VFS_FD = 0; + +/// Priority for the ESP32 TWAI status reporting task. +static constexpr BaseType_t WATCHDOG_TASK_PRIORITY = ESP_TASK_TCPIP_PRIO - 1; + +/// Stack size (bytes) to use for the ESP32 TWAI status reporting task. +static constexpr BaseType_t WATCHDOG_TASK_STACK = 2048; + +/// Interval at which to print the ESP32 TWAI bus status. +static constexpr TickType_t STATUS_PRINT_INTERVAL = pdMS_TO_TICKS(10000); + +/// TWAI default interrupt enable mask, excludes data overrun (bit[3]) and +/// brp_div (bit[4]) since these are not supported on all models. +static constexpr uint32_t TWAI_DEFAULT_INTERRUPTS = 0xE7; + +/// TWAI Driver ISR flags. +/// Defaults to level 1-3 (C/C++ compatible) and suspend when accessing flash. +static constexpr uint32_t TWAI_INTERRUPT_FLAGS = ESP_INTR_FLAG_LOWMED; + +/// ESP-IDF LOG tag used for all TWAI driver log statements. +static constexpr const char *TWAI_LOG_TAG = "ESP-TWAI"; + +/// TWAI Driver State +typedef struct +{ + /// TWAI Driver statistics. + esp32_twai_stats_t stats; + + /// TWAI HAL context object. + twai_hal_context_t context; + + /// Handle for the TWAI ISR. + intr_handle_t isr_handle; + + /// Transmit buffer + DeviceBuffer *tx_buf; + + /// Receive buffer + DeviceBuffer *rx_buf; + + /// Lock protecting @ref tx_buf and @ref rx_buf. + Atomic buf_lock; + + /// This will be notified if the device has data avilable for read. + Notifiable* readable_notify; + + /// This will be notified if the device has buffer avilable for write. + Notifiable* writable_notify; + + /// Flag indicating that the file descriptor has been opened with the + /// O_NONBLOCK flag. + bool non_blocking; + +#if CONFIG_VFS_SUPPORT_SELECT + /// Lock protecting all VFS select() cached data. + Atomic select_lock; + + /// VFS semaphore that can be used to prematurely wakeup a call to select. + /// NOTE: This is only valid after VFS has called @ref start_select and is + /// invalid after VFS calls @ref end_select. + esp_vfs_select_sem_t select_sem; + + /// Pointer to the fd_set provided by the ESP32 VFS layer used to indicate + /// the fd is ready to be read. + fd_set *readfds; + + /// Copy of the fd_set provided by the ESP32 VFS layer used to check if + /// the there is a read operation pending for the fd. + fd_set readfds_orig; + + /// Pointer to the fd_set provided by the ESP32 VFS layer used to indicate + /// the fd is ready to be written to. + fd_set *writefds; + + /// Copy of the fd_set provided by the ESP32 VFS layer used to check if + /// the fd is ready to be written to. + fd_set writefds_orig; + + /// Pointer to the fd_set provided by the ESP32 VFS layer used to indicate + /// the fd has an error. + fd_set *exceptfds; + + /// Copy of the fd_set provided by the ESP32 VFS layer used to check if + /// the fd has an error. + fd_set exceptfds_orig; +#endif // CONFIG_VFS_SUPPORT_SELECT + + /// Internal flag used for tracking if the low-level TWAI driver has been + /// configured and ready to use. + bool active; + + /// Thread handle for the background thread that monitors the TWAI driver + /// and periodically reports statistics. + os_thread_t wd_thread; + + /// Internal flag used to enable the periodic printing of TWAI driver + /// statistics. + bool report_stats; +} TwaiDriver; + +/// TWAI Driver statistics instance. +static TwaiDriver twai; + +/// Helper function that will return true if the TWAI driver is currently in a +/// RUNNING state. +static inline bool is_twai_running() +{ + return twai_hal_check_state_flags(&twai.context, TWAI_HAL_STATE_FLAG_RUNNING); +} + +/// Helper function that will return true if the TWAI driver is currently in a +/// RECOVERING state. +static inline bool is_twai_recovering() +{ + return twai_hal_check_state_flags(&twai.context, TWAI_HAL_STATE_FLAG_RECOVERING); +} + +/// Helper function that will return true if the TWAI driver is currently in a +/// ERR-WARN state. +static inline bool is_twai_err_warn() +{ + return twai_hal_check_state_flags(&twai.context, TWAI_HAL_STATE_FLAG_ERR_WARN); +} + +/// Helper function that will return true if the TWAI driver is currently in a +/// ERR-PASSIVE state. +static inline bool is_twai_err_passive() +{ + return twai_hal_check_state_flags(&twai.context, TWAI_HAL_STATE_FLAG_ERR_PASSIVE); +} + +/// Helper function that will return true if the TWAI driver is currently in an +/// OFF state. +static inline bool is_twai_bus_off() +{ + return twai_hal_check_state_flags(&twai.context, TWAI_HAL_STATE_FLAG_BUS_OFF); +} + +/// Helper function that will return true if the TWAI TX buffer is occupied. +static inline bool is_twai_tx_occupied() +{ + return twai_hal_check_state_flags(&twai.context, TWAI_HAL_STATE_FLAG_TX_BUFF_OCCUPIED); +} + +/// Helper function that will purge the TWAI RX queue and wake the OpenMRN +/// stack if it was waiting for a frame to be ready to receive. +static inline void twai_purge_rx_queue() +{ + Notifiable* n = nullptr; + { + AtomicHolder h(&twai.buf_lock); + LOG(VERBOSE, "ESP-TWAI: puring RX-Q: %d", twai.rx_buf->pending()); + twai.stats.rx_missed += twai.rx_buf->pending(); + twai.rx_buf->flush(); + std::swap(n, twai.readable_notify); + } + if (n) + { + n->notify(); + } +#if CONFIG_VFS_SUPPORT_SELECT + AtomicHolder l(&twai.select_lock); + if (FD_ISSET(TWAI_VFS_FD, &twai.exceptfds_orig)) + { + FD_SET(TWAI_VFS_FD, twai.exceptfds); + esp_vfs_select_triggered(twai.select_sem); + } +#endif // CONFIG_VFS_SUPPORT_SELECT +} + +/// Helper function that will purge the TWAI TX queue and wake the OpenMRN +/// stack if it was waiting for TX space. +static inline void twai_purge_tx_queue() +{ + Notifiable* n = nullptr; + { + AtomicHolder h(&twai.buf_lock); + LOG(VERBOSE, "ESP-TWAI: puring TX-Q: %d", twai.tx_buf->pending()); + twai.stats.tx_failed += twai.tx_buf->pending(); + twai.tx_buf->flush(); + std::swap(n, twai.writable_notify); + } + if (n) + { + n->notify(); + } +#if CONFIG_VFS_SUPPORT_SELECT + AtomicHolder l(&twai.select_lock); + if (FD_ISSET(TWAI_VFS_FD, &twai.exceptfds_orig)) + { + FD_SET(TWAI_VFS_FD, twai.exceptfds); + esp_vfs_select_triggered(twai.select_sem); + } +#endif // CONFIG_VFS_SUPPORT_SELECT +} + +/// VFS adapter for write(fd, buf, size) +/// +/// @param fd is the file descriptor being written to. +/// @param buf is the buffer containing the data to be written. +/// @param size is the size of the buffer. +/// @return number of bytes written or -1 if there is the write would be a +/// blocking operation. +static ssize_t twai_vfs_write(int fd, const void *buf, size_t size) +{ + LOG(VERBOSE, "ESP-TWAI: write(%d, %p, %d)", fd, buf, size); + DASSERT(fd == TWAI_VFS_FD); + ssize_t sent = 0; + const struct can_frame *data = (const struct can_frame *)buf; + size /= sizeof(struct can_frame); + bool bus_error = false; + while (size && !bus_error) + { + if (is_twai_bus_off()) + { + // If the TWAI bus is OFF initiate recovery and purge the pending TX queue. + LOG_ERROR("ESP-TWAI: Bus is OFF, initiating recovery."); + twai_hal_start_bus_recovery(&twai.context); + bus_error = true; + break; + } + else if (!is_twai_running()) + { + LOG_ERROR("ESP-TWAI: TWAI driver is not running, unable to write " + "%d frames.", size); + bus_error = true; + break; + } + + size_t frames_written = 0; + { + AtomicHolder h(&twai.buf_lock); + frames_written = twai.tx_buf->put(data, size < 8 ? size : 8); + } + if (frames_written == 0) + { + // No space in the TX queue + break; + } + else + { + twai.stats.tx_processed += frames_written; + } + + if (is_twai_running() && !is_twai_tx_occupied() && frames_written) + { + // since the TX buffer is not occupied, retrieve the first + // frame and transmit it here. + AtomicHolder h(&twai.buf_lock); + struct can_frame *frame = nullptr; + twai_message_t tx_frame; + twai_hal_frame_t hal_frame; + if (twai.tx_buf->data_read_pointer(&frame) && frame != nullptr) + { + memset(&tx_frame, 0, sizeof(twai_message_t)); + tx_frame.identifier = frame->can_id; + tx_frame.extd = IS_CAN_FRAME_EFF(*frame); + tx_frame.rtr = IS_CAN_FRAME_RTR(*frame); + tx_frame.data_length_code = frame->can_dlc; + memcpy(tx_frame.data, frame->data, frame->can_dlc); + twai_hal_format_frame(&tx_frame, &hal_frame); + twai_hal_set_tx_buffer_and_transmit(&twai.context, &hal_frame); + } + } + sent += frames_written; + size -= frames_written; + } + + if (bus_error) + { + twai_purge_tx_queue(); + } + + if (!sent) + { + errno = EWOULDBLOCK; + } + LOG(VERBOSE, "ESP-TWAI: write() %d", sent * sizeof(struct can_frame)); + return sent * sizeof(struct can_frame); +} + +/// VFS adapter for read(fd, buf, size) +/// +/// @param fd is the file descriptor being read from. +/// @param buf is the buffer to write into. +/// @param size is the size of the buffer. +/// @return number of bytes read or -1 if there is the read would be a +/// blocking operation. +static ssize_t twai_vfs_read(int fd, void *buf, size_t size) +{ + LOG(VERBOSE, "ESP-TWAI: read(%d, %p, %d)", fd, buf, size); + DASSERT(fd == TWAI_VFS_FD); + + ssize_t received = 0; + struct can_frame *data = (struct can_frame *)buf; + size /= sizeof(struct can_frame); + while (size) + { + size_t received_frames = 0; + { + AtomicHolder h(&twai.buf_lock); + received_frames = twai.rx_buf->get(data, size < 8 ? size : 8); + } + if (received_frames == 0) + { + break; + } + twai.stats.rx_processed += received_frames; + size -= received_frames; + received += received_frames; + data += received_frames; + } + if (!received) + { + errno = EWOULDBLOCK; + return -1; + } + + LOG(VERBOSE, "ESP-TWAI: read() %d", received * sizeof(struct can_frame)); + return received * sizeof(struct can_frame); +} + +/// VFS adapter for open(path, flags, mode). +/// +/// @param path is the path to the file being opened. +/// @param flags are the flags to use for opened file. +/// @param mode is the mode to use for the opened file. +/// +/// When this method is invoked it will enable the TWAI driver and start the +/// periodic timer used for RX/TX of frame data. +/// +/// @return 0 upon success, -1 upon failure with errno containing the cause. +static int twai_vfs_open(const char *path, int flags, int mode) +{ + // skip past the '/' that is passed in as first character + path++; + twai.non_blocking = (flags & O_NONBLOCK); + + LOG(INFO, "ESP-TWAI: Starting TWAI driver on:%s mode:%x (%s) fd:%d", + path, mode, twai.non_blocking ? "non-blocking" : "blocking", + TWAI_VFS_FD); + twai_purge_rx_queue(); + twai_purge_tx_queue(); + twai_hal_start(&twai.context, TWAI_MODE_NORMAL); + return TWAI_VFS_FD; +} + +/// VFS adapter for close(fd). +/// +/// @param fd is the file descriptor to close. +/// +/// When this method is invoked it will disable the TWAI driver and stop the +/// periodic timer used for RX/TX of frame data if it is running. +/// +/// @return zero upon success, negative value with errno for failure. +static int twai_vfs_close(int fd) +{ + LOG(INFO, "ESP-TWAI: Disabling TWAI driver using fd:%d", fd); + twai_purge_rx_queue(); + twai_purge_tx_queue(); + twai_hal_stop(&twai.context); + return 0; +} + +/// VFS adapter for ioctl. +/// +/// @param fd is the file descriptor to operate on. +/// @param cmd is the command to execute. +/// @param args is the args for the command. +/// +/// @return zero upon success, negative value with errno for failure. +static int twai_vfs_ioctl(int fd, int cmd, va_list args) +{ + /* sanity check to be sure we have a valid key for this device */ + HASSERT(IOC_TYPE(cmd) == CAN_IOC_MAGIC); + + // Will be called at the end if non-null. + Notifiable* n = nullptr; + + if (IOC_SIZE(cmd) == NOTIFIABLE_TYPE) + { + n = reinterpret_cast(va_arg(args, uintptr_t)); + } + + switch (cmd) + { + default: + return -EINVAL; + case CAN_IOC_READ_ACTIVE: + { + AtomicHolder h(&twai.buf_lock); + if (!twai.rx_buf->pending()) + { + std::swap(n, twai.readable_notify); + } + } + break; + case CAN_IOC_WRITE_ACTIVE: + { + AtomicHolder h(&twai.buf_lock); + if (!twai.tx_buf->space()) + { + std::swap(n, twai.writable_notify); + } + } + break; + } + if (n) + { + n->notify(); + } + return 0; +} + +/// VFS adapter for fcntl(fd, cmd, arg). +/// +/// @param fd to operate on. +/// @param cmd to be executed. +/// @param arg arg to be used for the operation. +/// +/// This method is currently a NO-OP. +/// +/// @return zero upon success, negative value with errno for failure. +static int twai_vfs_fcntl(int fd, int cmd, int arg) +{ + HASSERT(fd == TWAI_VFS_FD); + int result = 0; + + if (cmd == F_GETFL) + { + if (twai.non_blocking) + { + result |= O_NONBLOCK; + } + } + else if (cmd == F_SETFL) + { + twai.non_blocking = arg & O_NONBLOCK; + } + else + { + errno = ENOSYS; + result = -1; + } + + return result; +} + +#if CONFIG_VFS_SUPPORT_SELECT +/// VFS adapter for select() +/// +/// @param nfds is the number of FDs being checked. +/// @param readfds is the set of FDs being checked for ready to read. +/// @param writefds is the set of FDs being checked for ready to write. +/// @param exceptfds is the set of FDs being checked for exception. +/// @param sem is the semaphore to use for waking up the select() call. +static esp_err_t twai_vfs_start_select(int nfds, fd_set *readfds, + fd_set *writefds, fd_set *exceptfds, + esp_vfs_select_sem_t sem, + void **end_select_args) +{ + AtomicHolder l(&twai.select_lock); + // zero the cached copy of the fd_sets before setting the incoming copy in + // case the TWAI VFS FD is not set so we do not raise the alert when there + // is an interesting event. + FD_ZERO(&twai.readfds_orig); + FD_ZERO(&twai.writefds_orig); + FD_ZERO(&twai.exceptfds_orig); + + // If the TWAI FD is present in any of the FD sets we should process the + // select call. + if (FD_ISSET(TWAI_VFS_FD, readfds) || FD_ISSET(TWAI_VFS_FD, writefds) || + FD_ISSET(TWAI_VFS_FD, exceptfds)) + { + twai.select_sem = sem; + twai.readfds = readfds; + twai.readfds_orig = *readfds; + twai.writefds = writefds; + twai.writefds_orig = *writefds; + twai.exceptfds = exceptfds; + twai.exceptfds_orig = *exceptfds; + + // zero the fd_sets so we can mark the correct signals when we trigger + // the VFS layer. + FD_ZERO(readfds); + FD_ZERO(writefds); + FD_ZERO(exceptfds); + + // Check if we have pending frames to RX, if so trigger an early exit + // from select() + if (FD_ISSET(TWAI_VFS_FD, &twai.readfds_orig)) + { + AtomicHolder h(&twai.buf_lock); + if (twai.rx_buf->pending()) + { + FD_SET(TWAI_VFS_FD, readfds); + esp_vfs_select_triggered(sem); + } + } + } + return ESP_OK; +} + +/// VFS interface helper invoked when select() is woken up. +/// +/// @param end_select_args is any arguments provided in vfs_start_select(). +static esp_err_t twai_vfs_end_select(void *end_select_args) +{ + AtomicHolder l(&twai.select_lock); + // zero the cached copy of the fd_sets to prevent triggering the VFS wakeup + // since the select() has ended. + FD_ZERO(&twai.readfds_orig); + FD_ZERO(&twai.writefds_orig); + FD_ZERO(&twai.exceptfds_orig); + return ESP_OK; +} + +#endif // CONFIG_VFS_SUPPORT_SELECT + +/// TWAI Interrupt handler for receiving one (or more) TWAI frames. +static inline uint32_t twai_rx_frames() +{ + AtomicHolder h(&twai.buf_lock); + uint32_t rx_ready_count = twai_hal_get_rx_msg_count(&twai.context); + struct can_frame *can_frame = nullptr; + uint32_t rx_count = 0; + ESP_EARLY_LOGV(TWAI_LOG_TAG, "rx-ready-count: %d", rx_ready_count); + for (uint32_t idx = 0; idx < rx_ready_count; idx++) + { + twai_hal_frame_t frame; + if (twai_hal_read_rx_buffer_and_clear(&twai.context, &frame)) + { + if (frame.dlc > TWAI_FRAME_MAX_DLC) + { + // DLC is longer than supported, discard the frame. + twai.stats.rx_discard++; + ESP_EARLY_LOGE(TWAI_LOG_TAG, "rx-discard:%d", + twai.stats.rx_discard); + } + else if (twai.rx_buf->data_write_pointer(&can_frame)) + { + twai_message_t rx_frame; + twai_hal_parse_frame(&frame, &rx_frame); + memcpy(can_frame->data, rx_frame.data, TWAI_FRAME_MAX_DLC); + can_frame->can_dlc = rx_frame.data_length_code; + can_frame->can_id = rx_frame.identifier; + if (rx_frame.extd) + { + SET_CAN_FRAME_EFF(*can_frame); + } + else + { + CLR_CAN_FRAME_EFF(*can_frame); + } + if (rx_frame.rtr) + { + SET_CAN_FRAME_RTR(*can_frame); + } + else + { + CLR_CAN_FRAME_RTR(*can_frame); + } + rx_count += twai.rx_buf->advance(1); + ESP_EARLY_LOGV(TWAI_LOG_TAG, "rx-OK"); + } + else + { + twai.stats.rx_missed++; + ESP_EARLY_LOGE(TWAI_LOG_TAG, "rx-missed:%d", + twai.stats.rx_missed); + } + } + else + { + ESP_EARLY_LOGE(TWAI_LOG_TAG, "rx-overrun"); +// If the SOC does not support automatic clearing of the RX FIFO we need to +// handle it here and break out of the loop. +#ifndef SOC_TWAI_SUPPORTS_RX_STATUS + twai.stats.rx_overrun += + twai_hal_clear_rx_fifo_overrun(&twai.context); + break; +#else + twai.stats.rx_overrun++; +#endif // SOC_TWAI_SUPPORTS_RX_STATUS + } + } + + return rx_count; +} + +/// TWAI Interrupt handler for sending a TWAI frame to the transmit buffer. +static inline uint32_t twai_tx_frame() +{ + AtomicHolder h(&twai.buf_lock); + if (twai_hal_check_last_tx_successful(&twai.context)) + { + ESP_EARLY_LOGV(TWAI_LOG_TAG, "TX-OK"); + twai.stats.tx_success++; + twai.tx_buf->consume(1); + } + else + { + ESP_EARLY_LOGV(TWAI_LOG_TAG, "TX-FAIL"); + twai.stats.tx_failed++; + } + + // Check if we have a pending frame to transmit in the queue + struct can_frame *can_frame = nullptr; + if (twai.tx_buf->data_read_pointer(&can_frame) && can_frame != nullptr) + { + twai_message_t tx_frame; + twai_hal_frame_t hal_frame; + memset(&tx_frame, 0, sizeof(twai_message_t)); + tx_frame.identifier = can_frame->can_id; + tx_frame.extd = IS_CAN_FRAME_EFF(*can_frame); + tx_frame.rtr = IS_CAN_FRAME_RTR(*can_frame); + tx_frame.data_length_code = can_frame->can_dlc; + memcpy(tx_frame.data, can_frame->data, can_frame->can_dlc); + twai_hal_format_frame(&tx_frame, &hal_frame); + twai_hal_set_tx_buffer_and_transmit(&twai.context, &hal_frame); + return 1; + } + return 0; +} + +/// Interrupt handler for the TWAI device. +/// +/// @param arg unused. +static void twai_isr(void *arg) +{ + BaseType_t wakeup = pdFALSE; + uint32_t events = twai_hal_get_events(&twai.context); + ESP_EARLY_LOGV(TWAI_LOG_TAG, "events: %04x", events); + +#if defined(CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID) || \ + defined(CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT) + if (events & TWAI_HAL_EVENT_NEED_PERIPH_RESET) + { + ESP_EARLY_LOGV(TWAI_LOG_TAG, "periph-reset"); + twai_hal_prepare_for_reset(&twai.context); + periph_module_reset(PERIPH_TWAI_MODULE); + twai_hal_recover_from_reset(&twai.context); + twai.stats.rx_lost += twai_hal_get_reset_lost_rx_cnt(&twai.context); +#if CONFIG_VFS_SUPPORT_SELECT + AtomicHolder l(&twai.select_lock); + if (FD_ISSET(TWAI_VFS_FD, &twai.exceptfds_orig)) + { + FD_SET(TWAI_VFS_FD, twai.exceptfds); + esp_vfs_select_triggered_isr(twai.select_sem, &wakeup); + } +#endif // CONFIG_VFS_SUPPORT_SELECT + } +#endif // TWAI_ERRATA_FIX_RX_FRAME_INVALID || TWAI_ERRATA_FIX_RX_FIFO_CORRUPT + + // RX completed + if ((events & TWAI_HAL_EVENT_RX_BUFF_FRAME) && twai_rx_frames()) + { +#if CONFIG_VFS_SUPPORT_SELECT + AtomicHolder l(&twai.select_lock); + if (FD_ISSET(TWAI_VFS_FD, &twai.readfds_orig)) + { + FD_SET(TWAI_VFS_FD, twai.readfds); + esp_vfs_select_triggered_isr(twai.select_sem, &wakeup); + } +#endif // CONFIG_VFS_SUPPORT_SELECT + // std::swap is not ISR safe so it is not used here. + if (twai.readable_notify) + { + twai.readable_notify->notify_from_isr(); + twai.readable_notify = nullptr; + } + } + + // TX completed + if ((events & TWAI_HAL_EVENT_TX_BUFF_FREE) && twai_tx_frame()) + { +#if CONFIG_VFS_SUPPORT_SELECT + AtomicHolder l(&twai.select_lock); + if (FD_ISSET(TWAI_VFS_FD, &twai.writefds_orig)) + { + FD_SET(TWAI_VFS_FD, twai.writefds); + esp_vfs_select_triggered_isr(twai.select_sem, &wakeup); + } +#endif // CONFIG_VFS_SUPPORT_SELECT + // std::swap is not ISR safe so it is not used here. + if (twai.writable_notify) + { + twai.writable_notify->notify_from_isr(); + twai.writable_notify = nullptr; + } + } + + // Bus recovery complete, trigger a restart + if (events & TWAI_HAL_EVENT_BUS_RECOV_CPLT) + { + ESP_EARLY_LOGV(TWAI_LOG_TAG, "bus recovery complete"); + // start the driver automatically + twai_hal_start(&twai.context, TWAI_MODE_NORMAL); + } + + // Bus error detected + if (events & TWAI_HAL_EVENT_BUS_ERR) + { + twai.stats.bus_error++; + ESP_EARLY_LOGV(TWAI_LOG_TAG, "bus-error:%d", twai.stats.bus_error); + } + + // Arbitration error detected + if (events & TWAI_HAL_EVENT_ARB_LOST) + { + twai.stats.arb_error++; + ESP_EARLY_LOGV(TWAI_LOG_TAG, "arb-lost:%d", twai.stats.arb_error); + } + + if (wakeup == pdTRUE) + { + portYIELD_FROM_ISR(); + } +} + +/// Background task used for periodic reporting of TWAI bus status and bus +/// watchdog. +/// +/// The watchdog feature will handle the following use cases: +/// 1) TWAI Bus status remains in Recovering state for more than one reporting +/// interval. If this occurs the task will attempt to restart bus recovery. +/// 2) RX queue does not change and at least one frame is pending RX by the +/// OpenMRN stack. If this occurs the RX queue will be drained and an attempt +/// to wake the OpenMRN stack will be performed. All pending RX frames will be +/// counted as "missed" in the status reporting. +/// 3) TX queue does not change and at least one frame is pending TX by the +/// TWAI driver. If this occurs the TX queue will be drained and an attempt +/// to wake the OpenMRN stack will be performed. All pending TX frames will be +/// counted as "fail" in the status reporting. +/// +/// These use cases can occur if the CAN bus has been disconnected or there is +/// a general failure in communicating with the CAN transceiver IC. +void* twai_watchdog(void* param) +{ + LOG(INFO, "ESP-TWAI: Starting TWAI watchdog and reporting task"); + size_t last_rx_pending = 0; + size_t last_tx_pending = 0; + uint32_t last_twai_state = 0; + + while (twai.active) + { + // delay until the next reporting interval, this is being used instead + // of vTaskDelay to allow early wake up in the case of shutdown of the + // TWAI driver. + ulTaskNotifyTake(pdTRUE, STATUS_PRINT_INTERVAL); + + // If we wake up and the TWAI driver is no longer active we should exit + // this loop for shutdown. + if (!twai.active) + { + break; + } + + // If the last status of the bus and current status are the same and it + // is in a recovery state, retrigger the recovery as it will remain + // stuck indefinitely without a retrigger. + if (last_twai_state == twai.context.state_flags && + is_twai_recovering()) + { + LOG(WARNING, + "ESP-TWAI: Bus appears to be stuck, initiating bus recovery."); + twai_hal_start_bus_recovery(&twai.context); + } + last_twai_state = twai.context.state_flags; + + // If the RX queue has not changed since our last check, purge the RX + // queue and track it as missed frames. + if (last_rx_pending && last_rx_pending == twai.rx_buf->pending()) + { + LOG_ERROR("ESP-TWAI: RX-Q appears stuck, purging RX-Q!"); + twai_purge_rx_queue(); + } + last_rx_pending = twai.rx_buf->pending(); + + // If the TX queue has not changed since our last check, purge the RX + // queue and track it as failed frames. + if (last_tx_pending && last_tx_pending == twai.tx_buf->pending()) + { + LOG_ERROR("ESP-TWAI: TX-Q appears stuck, purging TX-Q!"); + twai_purge_tx_queue(); + } + last_tx_pending = twai.tx_buf->pending(); + + if (twai.report_stats) + { + LOG(INFO, + "ESP-TWAI: " + "RX:%d (pending:%zu,overrun:%d,discard:%d,missed:%d,lost:%d) " + "TX:%d (pending:%zu,suc:%d,fail:%d) " + "Bus (arb-err:%d,err:%d,state:%s)", + twai.stats.rx_processed, twai.rx_buf->pending(), + twai.stats.rx_overrun, twai.stats.rx_discard, + twai.stats.rx_missed, twai.stats.rx_lost, + twai.stats.tx_processed, twai.tx_buf->pending(), + twai.stats.tx_success, twai.stats.tx_failed, + twai.stats.arb_error, twai.stats.bus_error, + is_twai_running() ? "Running" : + is_twai_recovering() ? "Recovering" : + is_twai_err_warn() ? "Err-Warn" : + is_twai_err_passive() ? "Err-Pasv" : + "Bus Off"); + } + } + LOG(VERBOSE, "ESP-TWAI: Stopping TWAI watchdog and reporting task"); + + return NULL; +} + +Esp32HardwareTwai::Esp32HardwareTwai( + int rx, int tx, bool report, size_t rx_size, size_t tx_size, + const char *path, int clock_out, int bus_status, uint32_t isr_core) + : rxPin_(rx), txPin_(tx), extClockPin_(clock_out), + busStatusPin_(bus_status), preferredIsrCore_(isr_core), vfsPath_(path) +{ + HASSERT(GPIO_IS_VALID_GPIO(rxPin_)); + HASSERT(GPIO_IS_VALID_OUTPUT_GPIO(txPin_)); + + if (extClockPin_ != GPIO_NUM_NC) + { + HASSERT(GPIO_IS_VALID_OUTPUT_GPIO(extClockPin_)); + } + + if (busStatusPin_ != GPIO_NUM_NC) + { + HASSERT(GPIO_IS_VALID_OUTPUT_GPIO(busStatusPin_)); + } + + memset(&twai.stats, 0, sizeof(esp32_twai_stats_t)); + + twai.rx_buf = DeviceBuffer::create(rx_size); + HASSERT(twai.rx_buf != nullptr); + + twai.tx_buf = + DeviceBuffer::create(tx_size, tx_size / 2); + HASSERT(twai.tx_buf != nullptr); + + twai.report_stats = report; +} + +Esp32HardwareTwai::~Esp32HardwareTwai() +{ + if (twai.active) + { + esp_intr_free(twai.isr_handle); + twai_hal_deinit(&twai.context); + } + twai.active = false; + + esp_vfs_unregister(vfsPath_); + + twai.tx_buf->destroy(); + twai.rx_buf->destroy(); + + if (twai.wd_thread) + { + xTaskNotifyGive(twai.wd_thread); + } +} + +/// Internal function which is used to allocate the TWAI ISR on a specific +/// core. This will be either directly called (for single core SoCs) in +/// Esp32HardwareTwai::hw_init() or via esp_ipc. +static void esp32_twai_isr_init(void *param) +{ + LOG(VERBOSE, "ESP-TWAI: Allocating ISR"); + ESP_ERROR_CHECK( + esp_intr_alloc(ETS_TWAI_INTR_SOURCE, TWAI_INTERRUPT_FLAGS, twai_isr, + nullptr, &twai.isr_handle)); +} + +void Esp32HardwareTwai::hw_init() +{ + LOG(INFO, + "ESP-TWAI: Configuring TWAI (TX:%d, RX:%d, EXT-CLK:%d, BUS-CTRL:%d)", + txPin_, rxPin_, extClockPin_, busStatusPin_); + gpio_set_pull_mode((gpio_num_t)txPin_, GPIO_FLOATING); + esp_rom_gpio_connect_out_signal(txPin_, TWAI_TX_IDX, false, false); + esp_rom_gpio_pad_select_gpio(txPin_); + + gpio_set_pull_mode((gpio_num_t)rxPin_, GPIO_FLOATING); + esp_rom_gpio_connect_in_signal(rxPin_, TWAI_RX_IDX, false); + esp_rom_gpio_pad_select_gpio(rxPin_); + gpio_set_direction((gpio_num_t)rxPin_, GPIO_MODE_INPUT); + + if (extClockPin_ != GPIO_NUM_NC) + { + gpio_set_pull_mode((gpio_num_t)extClockPin_, GPIO_FLOATING); + esp_rom_gpio_connect_out_signal(extClockPin_, TWAI_CLKOUT_IDX, false, + false); + esp_rom_gpio_pad_select_gpio((gpio_num_t)extClockPin_); + } + + if (busStatusPin_ != GPIO_NUM_NC) + { + gpio_set_pull_mode((gpio_num_t)busStatusPin_, GPIO_FLOATING); + esp_rom_gpio_connect_out_signal(extClockPin_, TWAI_BUS_OFF_ON_IDX, + false, false); + esp_rom_gpio_pad_select_gpio((gpio_num_t)busStatusPin_); + } + + esp_vfs_t vfs = {}; + vfs.write = twai_vfs_write; + vfs.read = twai_vfs_read; + vfs.open = twai_vfs_open; + vfs.close = twai_vfs_close; + vfs.fcntl = twai_vfs_fcntl; + vfs.ioctl = twai_vfs_ioctl; +#if CONFIG_VFS_SUPPORT_SELECT + vfs.start_select = twai_vfs_start_select; + vfs.end_select = twai_vfs_end_select; +#endif // CONFIG_VFS_SUPPORT_SELECT + vfs.flags = ESP_VFS_FLAG_DEFAULT; + ESP_ERROR_CHECK(esp_vfs_register(vfsPath_, &vfs, this)); + + periph_module_reset(PERIPH_TWAI_MODULE); + periph_module_enable(PERIPH_TWAI_MODULE); + HASSERT(twai_hal_init(&twai.context)); + twai_timing_config_t timingCfg = TWAI_TIMING_CONFIG_125KBITS(); + twai_filter_config_t filterCfg = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + LOG(VERBOSE, "ESP-TWAI: Initiailizing peripheral"); + twai_hal_configure(&twai.context, &timingCfg, &filterCfg, + TWAI_DEFAULT_INTERRUPTS, 0); +#if SOC_CPU_CORES_NUM > 1 + ESP_ERROR_CHECK( + esp_ipc_call_blocking(preferredIsrCore_, esp32_twai_isr_init, nullptr)); +#else + esp32_twai_isr_init(nullptr); +#endif // SOC_CPU_CORES_NUM > 1 + twai.active = true; + + os_thread_create(&twai.wd_thread, "TWAI-WD", WATCHDOG_TASK_PRIORITY, + WATCHDOG_TASK_STACK, twai_watchdog, this); +} + +void Esp32HardwareTwai::get_driver_stats(esp32_twai_stats_t *stats) +{ + HASSERT(stats != nullptr); + memcpy(stats, &twai.stats, sizeof(esp32_twai_stats_t)); +} + +} // namespace openmrn_arduino + +#endif // IDF v4.3+ + +#endif // ESP32 diff --git a/src/freertos_drivers/esp32/Esp32HardwareTwai.hxx b/src/freertos_drivers/esp32/Esp32HardwareTwai.hxx new file mode 100644 index 000000000..c9f632fb0 --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32HardwareTwai.hxx @@ -0,0 +1,225 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32HardwareTwai.hxx + * + * TWAI driver implementation for OpenMRN. This leverages the ESP-IDF TWAI HAL + * API rather than the TWAI driver to allow for a more integrated solution than + * the TWAI driver which requires polling for RX. This implementation supports + * both ::select and the non-blocking ::ioctl/::fnctl approach. + * + * @author Mike Dunston + * @date 1 May 2021 + */ +#ifndef _FREERTOS_DRIVERS_ESP32_ESP32HARDWARETWAI_HXX_ +#define _FREERTOS_DRIVERS_ESP32_ESP32HARDWARETWAI_HXX_ + +#include + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,3,0) +#error Esp32HardwareTwai is only supported on ESP-IDF v4.3 and above. +#endif // IDF v4.3+ + +#include +#include +#include +#include +#include +#include + +#include "nmranet_config.h" +#include "utils/Singleton.hxx" + +namespace openmrn_arduino +{ + +/// TWAI Driver statistics. +typedef struct +{ + /// Number of frames have been removed from @ref rx_buf and sent to the + /// OpenMRN stack. + uint32_t rx_processed; + + /// Number of frames frames that could not be sent to @ref rx_buf. + uint32_t rx_missed; + + /// Number of frames that were discarded that had too large of a DLC count. + uint32_t rx_discard; + + /// Number of frames that were lost due to driver reset. + uint32_t rx_lost; + + /// Number of frames that were lost due to RX FIFO overrun. + uint32_t rx_overrun; + + /// Number of frames that have been sent to the @ref twai_tx_queue by the + /// OpenMRN stack successfully. + uint32_t tx_processed; + + /// Number of frames that have been transmitted successfully by the + /// low-level TWAI driver. + uint32_t tx_success; + + /// Number of frames that have been could not be transmitted successfully + /// by the low-level TWAI driver. + uint32_t tx_failed; + + /// Number of arbitration errors that have been observed on the TWAI bus. + uint32_t arb_error; + + /// Number of general bus errors that have been observed on the TWAI bus. + uint32_t bus_error; +} esp32_twai_stats_t; + +/// ESP32 Hardware TWAI (CAN) driver interface. +/// +/// The ESP32 has a hardware TWAI controller that requires an external CAN +/// transceiver connected via two GPIO pins (RX and TX). SPI connected CAN +/// transceivers are not supported by this interface. +/// +/// Example of usage (async API): +///``` +/// Esp32HardwareTwai twai; +/// void setup() { +/// ... +/// twai.hw_init(); +/// openmrn.begin(); +/// openmrn.add_can_port_async("/dev/twai/twai0"); +/// ... +/// } +///``` +/// +/// Example of usage (select API): +///``` +/// Esp32HardwareTwai twai; +/// void setup() { +/// ... +/// twai.hw_init(); +/// openmrn.begin(); +/// openmrn.add_can_port_select("/dev/twai/twai0"); +/// openmrn.start_executor_thread(); +/// ... +/// } +///``` +/// NOTE: For the select API it is necessary to start the executor thread in +/// the setup() method. +/// +/// NOTE: The select API is not be usable without CONFIG_VFS_SUPPORT_SELECT +/// being enabled in sdkconfig, this option is disabled automatically when +/// CONFIG_LWIP_USE_ONLY_LWIP_SELECT is enabled. CONFIG_VFS_SUPPORT_SELECT is +/// enabled by default in arduino-esp32. +class Esp32HardwareTwai : public Singleton +{ +public: + /// Constructor. + /// + /// @param rx is the GPIO pin connected to the CAN transceiver RX pin. + /// @param tx is the GPIO pin connected to the CAN transceiver TX pin. + /// @param report controls the periodic reporting of the TWAI driver + /// statistics, default is enabled. + /// @param rx_buffer_size is the number of @ref can_frame to queue before + /// frames will be dropped/lost, default is defined in + /// @var _sym_can_rx_buffer_size. + /// @param tx_buffer_size is the number of @ref can_frame to queue before + /// blocking will occur when transmitting, default is defined in + /// @var _sym_can_tx_buffer_size. + /// @param path is the VFS mount point for the TWAI driver, default is + /// "/dev/twai". + /// @param clock_out is the GPIO pin that can be used for an external clock + /// pin, default is disabled (-1). When enabled this will have a pre-scaled + /// clock signal. + /// @param bus_status is the GPIO pin that can be used for a bus status + /// indicator, default is disabled (-1). When enabled this pin will be set + /// LOW (0v) when the TWAI driver is in a "Bus Off" state and will be set + /// HIGH (3.3v) otherwise. + /// @param isr_preferred_core is the preferred core to run the TWAI ISR on, + /// for single core SoCs this will have no effect. + /// + /// NOTE: The CAN transceiver must internally loopback TX to RX, failure to + /// do so will be interpreted as an arbitration loss or bit error. + Esp32HardwareTwai(int rx, int tx + , bool report = true + , size_t rx_buffer_size = config_can_rx_buffer_size() + , size_t tx_buffer_size = config_can_tx_buffer_size() + , const char *path = "/dev/twai" + , int clock_out = GPIO_NUM_NC + , int bus_status = GPIO_NUM_NC + , uint32_t isr_preferred_core = DEFAULT_ISR_CORE); + + /// Destructor. + ~Esp32HardwareTwai(); + + /// Initializes the TWAI hardware and VFS adapter. + /// + /// NOTE: This must be called prior to adding the TWAI driver to the + /// @ref SimpleCanStack. + void hw_init(); + + /// Retrieves the current driver statistics. + /// + /// @param stats @ref esp32_twai_stats_t buffer to be filled with the + /// current statistics. + void get_driver_stats(esp32_twai_stats_t *stats); + +private: + /// Default constructor. + Esp32HardwareTwai(); + + /// GPIO pin connected to the external transceiver RX pin. + const int rxPin_; + + /// GPIO pin connected to the external transceiver TX pin. + const int txPin_; + + /// GPIO pin that generates an external clock signal. + const int extClockPin_; + + /// GPIO pin connected to an external bus status indicator. + const int busStatusPin_; + + /// Core which the TWAI ISR should be bound to. + const uint32_t preferredIsrCore_; + +#if SOC_CPU_CORES_NUM > 1 + /// Default core for running the TWAI ISR. + static constexpr uint32_t DEFAULT_ISR_CORE = APP_CPU_NUM; +#else + /// Default core for running the TWAI ISR. + static constexpr uint32_t DEFAULT_ISR_CORE = PRO_CPU_NUM; +#endif // SOC_CPU_CORES_NUM > 1 + + /// VFS Mount point. + const char *vfsPath_; + + DISALLOW_COPY_AND_ASSIGN(Esp32HardwareTwai); +}; + +} // namespace openmrn_arduino + +using openmrn_arduino::esp32_twai_stats_t; +using openmrn_arduino::Esp32HardwareTwai; + +#endif // _FREERTOS_DRIVERS_ESP32_ESP32HARDWARETWAI_HXX_ \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32Ledc.cxx b/src/freertos_drivers/esp32/Esp32Ledc.cxx new file mode 100644 index 000000000..0d856f064 --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32Ledc.cxx @@ -0,0 +1,53 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32Ledc.cxx + * + * ESP-IDF LEDC adapter that exposes a PWM interface. + * + * @author Mike Dunston + * @date 1 June 2021 + */ + +// Ensure we only compile this code for the ESP32 family of MCUs. +#if defined(ESP32) + +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) + +#include "Esp32Ledc.hxx" + +namespace openmrn_arduino +{ + +pthread_once_t Esp32Ledc::ledcFadeOnce_ = PTHREAD_ONCE_INIT; + +} // namespace openmrn_arduino + +#endif // IDF v4.3+ + +#endif // ESP32 \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32Ledc.hxx b/src/freertos_drivers/esp32/Esp32Ledc.hxx new file mode 100644 index 000000000..72d5bfff8 --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32Ledc.hxx @@ -0,0 +1,326 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32Ledc.hxx + * + * ESP-IDF LEDC adapter that exposes a PWM interface. + * + * @author Mike Dunston + * @date 1 June 2021 + */ + +#ifndef _DRIVERS_ESP32LEDC_HXX_ +#define _DRIVERS_ESP32LEDC_HXX_ + +#include "freertos_drivers/arduino/PWM.hxx" +#include "utils/logging.h" +#include "utils/macros.h" +#include "utils/Uninitialized.hxx" + +#include + +namespace openmrn_arduino +{ + +/// ESP32 LEDC provider for PWM like output on GPIO pins. +/// +/// This class allows creation of up to eight PWM outputs using a single PWM +/// frequency. All outputs in a single @ref Esp32Ledc instance will share the +/// same PWM frequency and will be assigned a channel sequentially. +/// +/// When the more than one PWM frequency is needed for outputs it is required +/// to create multiple @ref Esp32Ledc instances, each with a unique LEDC timer +/// (LEDC_TIMER_0 through LEDC_TIMER_3) and a unique first channel +/// (LED_CHANNEL_0 through LED_CHANNEL_5 or LED_CHANNEL_7 depending on ESP32 +/// variant). +/// +/// The ESP32-C3 only supports six channels, whereas other variants support +/// eight. Creating more than one @ref Esp32Ledc instance will not increase the +/// number of outputs. +/// +/// Example of usage: +///``` +/// OpenMRN openmrn(NODE_ID); +/// Esp32Ledc ledc({16, // Channel 0 +/// 17, // Channel 1 +/// 18}); // Channel 2 +/// ServoConsumer servo_0(openmrn.stack()->node(), cfg.seg().servo(), 1000, +/// ledc.get_channel(1)); +/// PWMGPO led_0(ledc.get_channel(0), 2500, 0); +/// void setup() { +/// ... +/// ledc.hw_init(); +/// openmrn.begin(); +/// openmrn.stack()->set_tx_activity_led(&led_0); +/// ledc.fade_channel_over_time(2, 2500, SEC_TO_MSEC(5)); +/// ... +/// } +///``` +class Esp32Ledc +{ +public: + /// Constructor. + /// + /// @param pins is the collection of output pins to use for this instance. + /// @param first_channel is the first LEDC channel to use for this + /// Esp32Ledc instance, default is LEDC_CHANNEL_0. + /// @param timer_resolution is the resolution of the LEDC timer, default is + /// 12bit. + /// @param timer_hz is the LEDC timer tick frequency, default is 5kHz. + /// @param timer_num is the LEDC timer to use, default is LEDC_TIMER_0. + /// @param timer_mode is the LED timer mode to use, default is + /// LEDC_LOW_SPEED_MODE. + /// @param timer_clock is the LEDC timer clock source, default is + /// LEDC_AUTO_CLK. + /// + /// Note: For @param timer_mode an additional value of LEDC_HIGH_SPEED_MODE + /// is supported *ONLY* on the base ESP32 variant. Other variants of the + /// ESP32 only support LEDC_LOW_SPEED_MODE. + Esp32Ledc(const std::initializer_list &pins, + const ledc_channel_t first_channel = LEDC_CHANNEL_0, + const ledc_timer_bit_t timer_resolution = LEDC_TIMER_12_BIT, + const uint32_t timer_hz = 5000, + const ledc_timer_t timer_num = LEDC_TIMER_0, + const ledc_mode_t timer_mode = LEDC_LOW_SPEED_MODE, + const ledc_clk_cfg_t timer_clock = LEDC_AUTO_CLK) + : firstChannel_(first_channel) + , pins_(pins) + { + // Ensure the pin count is valid and within range of usable channels. + HASSERT(pins_.size() > 0 && + pins_.size() < (LEDC_CHANNEL_MAX - first_channel)); + memset(&timerConfig_, 0, sizeof(ledc_timer_config_t)); + // timerConfig_.speed_mode will be assigned the SOC default mode, which + // is either HIGH speed or LOW speed depending on the hardware support. + timerConfig_.duty_resolution = timer_resolution; + timerConfig_.freq_hz = timer_hz; + timerConfig_.speed_mode = timer_mode; + timerConfig_.timer_num = timer_num; + timerConfig_.clk_cfg = timer_clock; + } + + /// Initializes the LEDC peripheral. + /// + /// @param pins are the gpio pins to assign to the LEDC channels. + /// + /// NOTE: Depending on the target ESP32 device the number of LEDC channels + /// available will be either six or eight. Exceeding this number of pins + /// will generate a runtime failure. + void hw_init() + { + LOG(INFO, + "[Esp32Ledc:%d] Configuring timer (resolution:%d, frequency:%d)", + timerConfig_.timer_num, + (1 << (uint8_t)timerConfig_.duty_resolution) - 1, + timerConfig_.freq_hz); + ESP_ERROR_CHECK(ledc_timer_config(&timerConfig_)); + size_t count = 0; + for (uint8_t pin : pins_) + { + HASSERT(GPIO_IS_VALID_OUTPUT_GPIO(pin)); + + ledc_channel_t led_channel = + static_cast(firstChannel_ + count); + LOG(INFO, "[Esp32Ledc:%d] Configuring LEDC channel %d on GPIO %d", + timerConfig_.timer_num, led_channel, pin); + ledc_channel_config_t config; + memset(&config, 0, sizeof(ledc_channel_config_t)); + config.gpio_num = pin; + config.speed_mode = timerConfig_.speed_mode; + config.channel = led_channel; + config.timer_sel = timerConfig_.timer_num; + ESP_ERROR_CHECK(ledc_channel_config(&config)); + channels_[count].emplace(this, led_channel, pin); + count++; + } + } + + /// @return one PWM output. + /// @param id is the output number, zero based for this @ref Esp32Ledc + /// instance. + PWM *get_channel(unsigned id) + { + HASSERT(id <= (LEDC_CHANNEL_MAX - firstChannel_)); + return &*channels_[id]; + } + + /// Transitions a PWM output from the current duty to the target duty over + /// the provided time period. + /// + /// @param id is the output number, zero based for this @ref Esp32Ledc + /// instance. + /// @param target_duty target duty value, default is zero. + /// @param fade_period number of milliseconds to use for the + /// transition, default is 1000 milliseconds. + /// @param fade_mode controls if this call is blocking or non-blocking, + /// default is non-blocking. + /// + /// NOTE: One a fade request has been submitted to the hardware it can not + /// be canceled and no other requests can be submitted or processed until + /// the previous request has completed. + void fade_channel_over_time(unsigned id, uint32_t target_duty = 0, + uint32_t fade_period = 1000, + ledc_fade_mode_t fade_mode = LEDC_FADE_NO_WAIT) + { + HASSERT(id <= (LEDC_CHANNEL_MAX - firstChannel_)); + ledc_channel_t channel = + static_cast(firstChannel_ + id); + HASSERT(0 == pthread_once(&ledcFadeOnce_, &Esp32Ledc::ledc_fade_setup)); + ESP_ERROR_CHECK( + ledc_set_fade_time_and_start(timerConfig_.speed_mode, channel, + target_duty, fade_period, fade_mode)); + } + + /// Static entry point for configuring the LEDC hardware fade controller. + /// + /// NOTE: This method should not be invoked by the user code, it will be + /// called automatically the first time @ref fade_channel_over_time is + /// called on any instance of @ref Esp32Ledc. + static void ledc_fade_setup() + { + // allocate the interrupt as low/medium priority (C code supported) and + // the interrupt can be shared with other usages (if necessary). + const int INTR_MODE_FLAGS = + ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_SHARED; + LOG(VERBOSE, "[Esp32Ledc] Initializing LED Fade ISR"); + ESP_ERROR_CHECK(ledc_fade_func_install(INTR_MODE_FLAGS)); + } +private: + class Channel : public PWM + { + public: + Channel(Esp32Ledc *parent, ledc_channel_t channel, uint8_t pin) + : parent_(parent) + , channel_(channel) + { + } + + void set_period(uint32_t counts) override + { + parent_->set_period(counts); + } + + uint32_t get_period() override + { + return parent_->get_period(); + } + + void set_duty(uint32_t counts) override + { + parent_->set_duty(channel_, counts); + } + + uint32_t get_duty() override + { + return parent_->get_duty(channel_); + } + + uint32_t get_period_max() override + { + return parent_->get_period_max(); + } + + uint32_t get_period_min() override + { + return parent_->get_period_min(); + } + private: + Esp32Ledc *parent_; + ledc_channel_t channel_; + }; + + /// Set PWM period. + /// @param counts PWM timer frequency in Hz. + /// + /// NOTE: This will apply to *ALL* PWM outputs that use the same timer. + void set_period(uint32_t counts) + { + ESP_ERROR_CHECK( + ledc_set_freq(timerConfig_.speed_mode, timerConfig_.timer_num, + counts)); + } + + /// Get PWM period. + /// @return PWM timer frequency in Hz. + uint32_t get_period() + { + return ledc_get_freq(timerConfig_.speed_mode, timerConfig_.timer_num); + } + + /// Sets the duty cycle. + /// @param channel PWM channel to configure + /// @param counts duty cycle in counts + void set_duty(ledc_channel_t channel, uint32_t counts) + { + ESP_ERROR_CHECK( + ledc_set_duty(timerConfig_.speed_mode, channel, counts)); + } + + /// Gets the duty cycle. + /// @param channel PWM channel to retrieve the duty cycle for. + /// @return counts duty cycle in counts + uint32_t get_duty(ledc_channel_t channel) + { + return ledc_get_duty(timerConfig_.speed_mode, channel); + } + + /// Get max period supported by the underlying LEDC timer. + /// @return period in counts. + uint32_t get_period_max() + { + return ((1 << (uint8_t)timerConfig_.duty_resolution) - 1); + } + + /// Get min period supported by the underlying LEDC timer. + /// @return period in counts. + uint32_t get_period_min() + { + return 0; + } + + /// First LEDC Channel for this @ref Esp32Ledc. + const ledc_channel_t firstChannel_; + + /// LEDC Timer configuration settings. + ledc_timer_config_t timerConfig_; + + /// Protects the initialization of LEDC Fade ISR hook. + static pthread_once_t ledcFadeOnce_; + + /// @ref PWM instances connected to LEDC channels. + uninitialized channels_[LEDC_CHANNEL_MAX]; + + /// Collection of GPIO pins in use by this @ref Esp32Ledc. + std::vector pins_; + + DISALLOW_COPY_AND_ASSIGN(Esp32Ledc); +}; + +} // namespace openmrn_arduino + +using openmrn_arduino::Esp32Ledc; + +#endif // _DRIVERS_ESP32LEDC_HXX_ \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32SocInfo.cxx b/src/freertos_drivers/esp32/Esp32SocInfo.cxx new file mode 100644 index 000000000..e56611b2f --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32SocInfo.cxx @@ -0,0 +1,300 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32SocInfo.cxx + * + * Utility class for printing information about the ESP32 device currently in + * use. + * + * @author Mike Dunston + * @date 4 May 2021 + */ + +#if defined(ESP32) + +#include "freertos_drivers/esp32/Esp32SocInfo.hxx" +#include "utils/logging.h" + +#include + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,4,0) +#include +#endif // IDF v4.4+ + +namespace openmrn_arduino +{ + +#if defined(CONFIG_IDF_TARGET_ESP32) +/// ESP32 SoC reset reasons. +static constexpr const char * const RESET_REASONS[] = +{ + "unknown", // NO_MEAN 0 + "power on reset", // POWERON_RESET 1 + "unknown", // no key 2 + "software reset", // SW_RESET 3 + "watchdog reset (legacy)", // OWDT_RESET 4 + "deep sleep reset", // DEEPSLEEP_RESET 5 + "reset (SLC)", // SDIO_RESET 6 + "watchdog reset (group0)", // TG0WDT_SYS_RESET 7 + "watchdog reset (group1)", // TG1WDT_SYS_RESET 8 + "RTC system reset", // RTCWDT_SYS_RESET 9 + "Intrusion test reset", // INTRUSION_RESET 10 + "WDT Timer group reset", // TGWDT_CPU_RESET 11 + "software reset (CPU)", // SW_CPU_RESET 12 + "RTC WDT reset", // RTCWDT_CPU_RESET 13 + "software reset (CPU)", // EXT_CPU_RESET 14 + "Brownout reset", // RTCWDT_BROWN_OUT_RESET 15 + "RTC Reset (Normal)", // RTCWDT_RTC_RESET 16 +}; +#elif defined(CONFIG_IDF_TARGET_ESP32S2) +/// ESP32-S2 SoC reset reasons. +static constexpr const char * const RESET_REASONS[] = +{ + "unknown", // NO_MEAN 0 + "power on reset", // POWERON_RESET 1 + "unknown", // no key 2 + "software reset", // SW_RESET 3 + "unknown", // no key 4 + "deep sleep reset", // DEEPSLEEP_RESET 5 + "unknown", // no key 6 + "watchdog reset (group0)", // TG0WDT_SYS_RESET 7 + "watchdog reset (group1)", // TG1WDT_SYS_RESET 8 + "RTC system reset", // RTCWDT_SYS_RESET 9 + "Intrusion test reset", // INTRUSION_RESET 10 + "WDT Timer group0 reset", // TG0WDT_CPU_RESET 11 + "software reset (CPU)", // RTC_SW_CPU_RESET 12 + "RTC WDT reset", // RTCWDT_CPU_RESET 13 + "unknown", // no key 14 + "Brownout reset", // RTCWDT_BROWN_OUT_RESET 15 + "RTC Reset (Normal)", // RTCWDT_RTC_RESET 16 + "WDT Timer group1 reset", // TG1WDT_CPU_RESET 17 + "WDT Reset", // SUPER_WDT_RESET 18 + "RTC Reset (Glitch)", // GLITCH_RTC_RESET 19 +}; +#elif defined(CONFIG_IDF_TARGET_ESP32C3) +/// ESP32-C3 SoC reset reasons. +static constexpr const char * const RESET_REASONS[] = +{ + "unknown", // NO_MEAN 0 + "power on reset", // POWERON_RESET 1 + "unknown", // no key 2 + "software reset", // SW_RESET 3 + "unknown", // no key 4 + "deep sleep reset", // DEEPSLEEP_RESET 5 + "unknown", // no key 6 + "watchdog reset (group0)", // TG0WDT_SYS_RESET 7 + "watchdog reset (group1)", // TG1WDT_SYS_RESET 8 + "RTC system reset", // RTCWDT_SYS_RESET 9 + "Intrusion test reset", // INTRUSION_RESET 10 + "WDT Timer group0 reset", // TG0WDT_CPU_RESET 11 + "software reset (CPU)", // RTC_SW_CPU_RESET 12 + "RTC WDT reset", // RTCWDT_CPU_RESET 13 + "unknown", // no key 14 + "Brownout reset", // RTCWDT_BROWN_OUT_RESET 15 + "RTC Reset (Normal)", // RTCWDT_RTC_RESET 16 + "WDT Timer group1 reset", // TG1WDT_CPU_RESET 17 + "WDT Reset", // SUPER_WDT_RESET 18 + "RTC Reset (Glitch)", // GLITCH_RTC_RESET 19 + "eFuse Reset", // EFUSE_RESET 20 + "USB UART Reset", // USB_UART_CHIP_RESET 21 + "USB JTAG Reset", // USB_JTAG_CHIP_RESET 22 + "Power Glitch Reset", // POWER_GLITCH_RESET 23 +}; +#elif defined(CONFIG_IDF_TARGET_ESP32S3) +/// ESP32-S3 SoC reset reasons. +static constexpr const char * const RESET_REASONS[] = +{ + "unknown", // NO_MEAN 0 + "power on reset", // POWERON_RESET 1 + "unknown", // no key 2 + "software reset", // RTC_SW_SYS_RESET 3 + "unknown", // no key 4 + "deep sleep reset", // DEEPSLEEP_RESET 5 + "unknown", // no key 6 + "watchdog reset (group0)", // TG0WDT_SYS_RESET 7 + "watchdog reset (group1)", // TG1WDT_SYS_RESET 8 + "RTC system reset", // RTCWDT_SYS_RESET 9 + "Intrusion test reset", // INTRUSION_RESET 10 + "WDT Timer group0 reset", // TG0WDT_CPU_RESET 11 + "software reset (CPU)", // RTC_SW_CPU_RESET 12 + "RTC WDT reset", // RTCWDT_CPU_RESET 13 + "unknown", // no key 14 + "Brownout reset", // RTCWDT_BROWN_OUT_RESET 15 + "RTC Reset (Normal)", // RTCWDT_RTC_RESET 16 + "WDT Timer group1 reset", // TG1WDT_CPU_RESET 17 + "WDT Reset", // SUPER_WDT_RESET 18 + "RTC Reset (Glitch)", // GLITCH_RTC_RESET 19 + "eFuse Reset", // EFUSE_RESET 20 + "USB UART Reset", // USB_UART_CHIP_RESET 21 + "USB JTAG Reset", // USB_JTAG_CHIP_RESET 22 + "Power Glitch Reset", // POWER_GLITCH_RESET 23 +}; +#elif defined(CONFIG_IDF_TARGET_ESP32H2) +/// ESP32-H2 SoC reset reasons. +static constexpr const char * const RESET_REASONS[] = +{ + "unknown", // NO_MEAN 0 + "power on reset", // POWERON_RESET 1 + "unknown", // no key 2 + "software reset", // SW_RESET 3 + "unknown", // no key 4 + "deep sleep reset", // DEEPSLEEP_RESET 5 + "reset (SLC)", // SDIO_RESET 6 + "watchdog reset (group0)", // TG0WDT_SYS_RESET 7 + "watchdog reset (group1)", // TG1WDT_SYS_RESET 8 + "RTC system reset", // RTCWDT_SYS_RESET 9 + "Intrusion test reset", // INTRUSION_RESET 10 + "WDT Timer group0 reset", // TG0WDT_CPU_RESET 11 + "software reset (CPU)", // RTC_SW_CPU_RESET 12 + "RTC WDT reset", // RTCWDT_CPU_RESET 13 + "unknown", // no key 14 + "Brownout reset", // RTCWDT_BROWN_OUT_RESET 15 + "RTC Reset (Normal)", // RTCWDT_RTC_RESET 16 + "WDT Timer group1 reset", // TG1WDT_CPU_RESET 17 + "WDT Reset", // SUPER_WDT_RESET 18 + "RTC Reset (Glitch)", // GLITCH_RTC_RESET 19 + "eFuse Reset", // EFUSE_RESET 20 + "USB UART Reset", // USB_UART_CHIP_RESET 21 + "USB JTAG Reset", // USB_JTAG_CHIP_RESET 22 + "Power Glitch Reset", // POWER_GLITCH_RESET 23 + "JTAG Reset", // JTAG_RESET 24 +}; +#elif defined(CONFIG_IDF_TARGET_ESP32C2) +/// ESP32C2 SoC reset reasons. +static constexpr const char * const RESET_REASONS[] = +{ + "unknown", // NO_MEAN 0x00 + "power on reset", // POWERON_RESET 0x01 + "unknown", // no key 0x02 + "software reset", // SW_RESET 0x03 + "unknown", // no key 0x04 + "deep sleep reset", // DEEPSLEEP_RESET 0x05 + "unknown", // no key 0x06 + "MWDT (digital) reset", // MWDT0 0x07 + "unknown", // no key 0x08 + "RTC WDT reset", // RTC_WDT 0x09 + "unknown", // no key 0x0A + "MWDT reset (CPU)", // MWDT0 0x0B + "software reset (CPU)", // SW_CPU_RESET 0x0C + "RTC WDT reset (CPU)", // RTC_WDT 0x0D + "unknown", // no key 0x0E + "Brownout reset", // RTCWDT_BROWN_OUT_RESET 0x0F + "RTC Reset (Normal)", // RTCWDT_RTC_RESET 0x10 + "unknown", // no key 0x11 + "WDT Super reset", // SUPER_WDT 0x12 + "unknown", // no key 0x13 + "eFuse CRC error", // EFUSE_CRC_ERROR 0x14 + "unknown", // no key 0x15 + "unknown", // no key 0x16 + "unknown", // no key 0x17 + "JTAG Reset" // JTAG_RESET 0x18 +}; +#endif // IDF Target + +/// Mapping of known ESP chip id values. +static constexpr const char * const CHIP_NAMES[] = +{ + // Note: this must be kept in sync with esp_chip_model_t. + "Unknown", // 0 Unknown (placeholder) + "ESP32", // 1 CHIP_ESP32 + "ESP32-S2", // 2 CHIP_ESP32S2 + "Unknown", // 3 Unknown (placeholder) + "Unknown", // 4 Unknown (placeholder) + "ESP32-C3", // 5 CHIP_ESP32C3 + "ESP32-H2", // 6 CHIP_ESP32H2 + "Unknown", // 7 Unknown (placeholder) + "Unknown", // 8 Unknown (placeholder) + "ESP32-S3", // 9 CHIP_ESP32S3 + "Unknown", // 10 Unknown (placeholder) + "Unknown", // 11 Unknown (placeholder) + "ESP32-C2", // 12 CHIP_ESP32C2 +}; + +uint8_t Esp32SocInfo::print_soc_info() +{ + // capture the reason for the CPU reset. For dual core SoCs this will + // only check the PRO CPU and not the APP CPU since they will usually + // restart one after the other. + uint8_t reset_reason = rtc_get_reset_reason(PRO_CPU_NUM); + uint8_t orig_reset_reason = reset_reason; + // Ensure the reset reason is within bounds. + if (reset_reason >= ARRAYSIZE(RESET_REASONS)) + { + reset_reason = 0; + } + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + size_t chip_model = chip_info.model; + // Ensure chip model is within bounds. + if (chip_model >= ARRAYSIZE(CHIP_NAMES)) + { + chip_model = 0; + } + + LOG(INFO, "[SoC] reset reason:%d - %s", reset_reason, + RESET_REASONS[reset_reason]); + LOG(INFO, + "[SoC] model:%s,rev:%d,cores:%d,flash:%s,WiFi:%s,BLE:%s,BT:%s", + CHIP_NAMES[chip_model], chip_info.revision, + chip_info.cores, + chip_info.features & CHIP_FEATURE_EMB_FLASH ? "Yes" : "No", + chip_info.features & CHIP_FEATURE_WIFI_BGN ? "Yes" : "No", + chip_info.features & CHIP_FEATURE_BLE ? "Yes" : "No", + chip_info.features & CHIP_FEATURE_BT ? "Yes" : "No"); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) + LOG(INFO, "[SoC] Heap: %.2fkB / %.2fkB", + heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024.0f, + heap_caps_get_total_size(MALLOC_CAP_INTERNAL) / 1024.0f); +#if CONFIG_SPIRAM_SUPPORT || BOARD_HAS_PSRAM + LOG(INFO, "[SoC] PSRAM: %.2fkB / %.2fkB", + heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024.0f, + heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024.0f); +#endif // CONFIG_SPIRAM_SUPPORT || BOARD_HAS_PSRAM + +#else // NOT IDF v4.3+ + LOG(INFO, "[SoC] Free Heap: %.2fkB", + heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024.0f); +#if CONFIG_SPIRAM_SUPPORT || BOARD_HAS_PSRAM + LOG(INFO, "[SoC] Free PSRAM: %.2fkB", + heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024.0f); +#endif // CONFIG_SPIRAM_SUPPORT || BOARD_HAS_PSRAM + +#endif // IDF v4.3+ + + LOG(INFO, "[SoC] App running from partition: %s", + esp_ota_get_running_partition()->label); + if (reset_reason != orig_reset_reason) + { + LOG(WARNING, "Reset reason mismatch: %d vs %d", reset_reason, + orig_reset_reason); + } + return reset_reason; +} + +} // namespace openmrn_arduino + +#endif // ESP32 \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32SocInfo.hxx b/src/freertos_drivers/esp32/Esp32SocInfo.hxx new file mode 100644 index 000000000..e683b8309 --- /dev/null +++ b/src/freertos_drivers/esp32/Esp32SocInfo.hxx @@ -0,0 +1,81 @@ +/** \copyright + * Copyright (c) 2021, Mike Dunston + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * \file Esp32SocInfo.hxx + * + * Utility class which provides details of the running ESP32 SoC. + * + * @author Mike Dunston + * @date 4 May 2021 + */ +#ifndef _FREERTOS_DRIVERS_ESP32_ESP32SOCINFO_HXX_ +#define _FREERTOS_DRIVERS_ESP32_ESP32SOCINFO_HXX_ + +#include + +#if defined(ESP32) + +#include "sdkconfig.h" + +#include +#if defined(CONFIG_IDF_TARGET_ESP32) +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) +#include +#else +#include +#endif // IDF v4.3+ +#elif defined(CONFIG_IDF_TARGET_ESP32S2) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32S3) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32C3) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32H2) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32C2) +#include +#endif + +namespace openmrn_arduino +{ + +/// Utility class which logs information about the currently running SoC. +class Esp32SocInfo +{ +public: + /// Logs information about the currently running SoC. + /// + /// @return Reason for the reset of the SoC. + static uint8_t print_soc_info(); +}; + +} // namespace openmrn_arduino + +using openmrn_arduino::Esp32SocInfo; + +#endif // ESP32 + +#endif // _FREERTOS_DRIVERS_ESP32_ESP32SOCINFO_HXX_ \ No newline at end of file diff --git a/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx b/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx index 72a4ba10d..8d68fc433 100644 --- a/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx +++ b/src/freertos_drivers/esp32/Esp32WiFiConfiguration.hxx @@ -74,11 +74,17 @@ public: /// Visible description for the hub/uplink enable field. static constexpr const char *CONN_MODE_DESC = - "Defines whether to allow accepting connections (according to the Hub configuration), making a connection (according to the Uplink configuration), or both."; + "Defines whether to allow accepting connections (according to the Hub " + "configuration), making a connection (according to the Uplink " + "configuration), or both.\nThis setting can be set to Disabled if the " + "ESP32 will be using the TWAI (CAN) driver instead for the connection " + "to other nodes.\nNote: it is not recommended to enable the Hub " + "functionality on single-core ESP32 models."; /// of possible keys and descriptive values to show to the user for /// the connection_mode fields. static constexpr const char *CONN_MODE_MAP = + "0Disabled" "1Uplink Only" "2Hub Only" "3Hub+Uplink"; @@ -117,25 +123,28 @@ CDI_GROUP_END(); /// CDI Configuration for an @ref Esp32WiFiManager managed node. CDI_GROUP(WiFiConfiguration); /// Allows the WiFi system to use power-saving techniques to conserve power -/// when the node is powered via battery. +/// when the node is powered via battery, this often can be left disabled. CDI_GROUP_ENTRY(sleep, openlcb::Uint8ConfigEntry, Name(Esp32WiFiConfigurationParams::WIFI_POWER_SAVE_NAME), - Description(Esp32WiFiConfigurationParams::WIFI_POWER_SAVE_DESC), Min(0), - Max(1), Default(0), MapValues(Esp32WiFiConfigurationParams::BOOLEAN_MAP)); -/// Defines configuration of hub or uplink + Description(Esp32WiFiConfigurationParams::WIFI_POWER_SAVE_DESC), + Min(0), Max(1), Default(0), /* Off */ + MapValues(Esp32WiFiConfigurationParams::BOOLEAN_MAP)); +/// Configures the connection mode as uplink, hub or both. CDI_GROUP_ENTRY(connection_mode, openlcb::Uint8ConfigEntry, Name(Esp32WiFiConfigurationParams::CONN_MODE_NAME), - Description(Esp32WiFiConfigurationParams::CONN_MODE_DESC), Min(1), Max(3), - Default(1), MapValues(Esp32WiFiConfigurationParams::CONN_MODE_MAP)); -/// CDI Configuration to enable this node to be a hub. + Description(Esp32WiFiConfigurationParams::CONN_MODE_DESC), + Min(0), Max(3), Default(1), /* Uplink only */ + MapValues(Esp32WiFiConfigurationParams::CONN_MODE_MAP)); +/// Configuration for the @ref Esp32WiFiManager managed hub. CDI_GROUP_ENTRY(hub, HubConfiguration, Name(Esp32WiFiConfigurationParams::HUB_NAME), Description(Esp32WiFiConfigurationParams::HUB_DESC)); -/// CDI Configuration for this node's connection to an uplink hub. +/// Configuration for this node's uplink connection. CDI_GROUP_ENTRY(uplink, openlcb::TcpClientConfig, Name(Esp32WiFiConfigurationParams::UPLINK_NAME), Description(Esp32WiFiConfigurationParams::UPLINK_DESC)); +CDI_GROUP_ENTRY(reserved, openlcb::BytesConfigEntry<6>, Hidden(true)); CDI_GROUP_END(); } // namespace openmrn_arduino diff --git a/src/freertos_drivers/esp32/Esp32WiFiManager.cxx b/src/freertos_drivers/esp32/Esp32WiFiManager.cxx index 1fcf009f2..9bd94acbd 100644 --- a/src/freertos_drivers/esp32/Esp32WiFiManager.cxx +++ b/src/freertos_drivers/esp32/Esp32WiFiManager.cxx @@ -32,58 +32,47 @@ * @date 4 February 2019 */ -// Ensure we only compile this code for the ESP32 +// Ensure we only compile this code on ESP32 MCUs #ifdef ESP32 #include "Esp32WiFiManager.hxx" +#include "openlcb/SimpleStack.hxx" +#include "openlcb/TcpDefs.hxx" #include "os/MDNS.hxx" #include "utils/FdUtils.hxx" +#include "utils/format_utils.hxx" +#include "utils/SocketClient.hxx" +#include "utils/SocketClientParams.hxx" -#include #include +#include #include #include - #include #include -#include - -// Starting in ESP-IDF v4.0 a few header files have been relocated so we need -// to adjust the include paths accordingly. If the __has_include preprocessor -// directive is defined we can use it to find the appropriate header files. -// If it is not usable then we will default the older header filenames. -#if defined(__has_include) - -// rom/crc.h was relocated to esp32/rom/crc.h in ESP-IDF v4.0 -// TODO: This will need to be platform specific in IDF v4.1 since this is -// exposed in unique header paths for each supported platform. Detecting the -// operating platform (ESP32, ESP32-S2, ESP32-S3, etc) can be done by checking -// for the presence of one of the following defines: -// CONFIG_IDF_TARGET_ESP32 -- ESP32 -// CONFIG_IDF_TARGET_ESP32S2 -- ESP32-S2 -// CONFIG_IDF_TARGET_ESP32S3 -- ESP32-S3 -// If none of these are defined it means the ESP-IDF version is v4.0 or -// earlier. -#if __has_include("esp32/rom/crc.h") -#include -#else -#include -#endif -// esp_wifi_internal.h was relocated to esp_private/wifi.h in ESP-IDF v4.0 -#if __has_include("esp_private/wifi.h") +// ESP-IDF v4+ has a slightly different directory structure to previous +// versions. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) +#include +#include #include -#else -#include -#endif -#else +#if defined(CONFIG_IDF_TARGET_ESP32S2) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32C3) +#include +#elif defined(CONFIG_IDF_TARGET_ESP32S3) +#include +#else // default to ESP32 +#include +#endif // CONFIG_IDF_TARGET -// We are unable to use __has_include, default to the old include paths. +#else // ESP-IDF v3.x +#include #include #include - -#endif // defined __has_include +#endif // ESP_IDF_VERSION using openlcb::NodeID; using openlcb::SimpleCanStackBase; @@ -116,9 +105,8 @@ using std::unique_ptr; /// and is used as part of the Esp32 WiFi HUB support. void mdns_publish(const char *name, const char *service, uint16_t port); -/// Removes advertisement of an mDNS service name. This is not currently -/// exposed in the MDNS class but is supported on the ESP32. -void mdns_unpublish(const char *service); +/// Removes advertisement of an mDNS service name. +void mdns_unpublish(const char *name, const char *service); /// Splits a service name since the ESP32 mDNS library requires the service /// name and service protocol to be passed in individually. @@ -133,29 +121,31 @@ void split_mdns_service_name(string *service_name, string *protocol_name); namespace openmrn_arduino { - -/// Priority to use for the wifi_manager_task. This is currently set to one -/// level higher than the arduino-esp32 loopTask. The task will be in a sleep -/// state until woken up by Esp32WiFiManager::process_wifi_event or -/// Esp32WiFiManager::apply_configuration. -static constexpr UBaseType_t WIFI_TASK_PRIORITY = 2; - -/// Priority for the task performing the mdns lookups and connections for the -/// wifi uplink. -static constexpr UBaseType_t CONNECT_TASK_PRIORITY = 3; - -/// Stack size for the wifi_manager_task. -static constexpr uint32_t WIFI_TASK_STACK_SIZE = 2560L; - -/// Stack size for the connect_executor. -static constexpr uint32_t CONNECT_TASK_STACK_SIZE = 2560L; +// When running on a unicore (ESP32-S2 or ESP32-C3) use a lower priority so the +// task will run in co-op mode with loop. When running on a multi-core SoC we +// can use a higher priority for the background task. +#if CONFIG_FREERTOS_UNICORE +/// Priority for the task performing the mdns lookups, connections for the +/// wifi uplink and any other background tasks needed by the wifi manager. +static constexpr UBaseType_t EXECUTOR_TASK_PRIORITY = 1; +#else // multi-core +/// Priority for the task performing the mdns lookups, connections for the +/// wifi uplink and any other background tasks needed by the wifi manager. +static constexpr UBaseType_t EXECUTOR_TASK_PRIORITY = 3; +#endif // CONFIG_FREERTOS_UNICORE + +/// Stack size for the background task executor. +static constexpr uint32_t EXECUTOR_TASK_STACK_SIZE = 5120L; /// Interval at which to check the WiFi connection status. -static constexpr TickType_t WIFI_CONNECT_CHECK_INTERVAL = pdMS_TO_TICKS(5000); +static constexpr uint32_t WIFI_CONNECT_CHECK_INTERVAL_SEC = 5; /// Interval at which to check if the GcTcpHub has started or not. static constexpr uint32_t HUB_STARTUP_DELAY_USEC = MSEC_TO_USEC(50); +/// Interval at which to check if the WiFi task has shutdown or not. +static constexpr uint32_t TASK_SHUTDOWN_DELAY_USEC = MSEC_TO_USEC(1); + /// Bit designator for wifi_status_event_group which indicates we are connected /// to the SSID. static constexpr int WIFI_CONNECTED_BIT = BIT0; @@ -170,26 +160,6 @@ static constexpr int WIFI_GOTIP_BIT = BIT1; /// seconds. static constexpr uint8_t MAX_CONNECTION_CHECK_ATTEMPTS = 36; -/// This is the number of consecutive IP addresses which will be available in -/// the SoftAP DHCP server IP pool. These will be allocated immediately -/// following the SoftAP IP address (default is 192.168.4.1). Default number to -/// reserve is 48 IP addresses. Only four stations can be connected to the -/// ESP32 SoftAP at any single time. -static constexpr uint8_t SOFTAP_IP_RESERVATION_BLOCK_SIZE = 48; - -/// Event handler for the ESP32 WiFi system. This will receive events from the -/// ESP-IDF event loop processor and pass them on to the Esp32WiFiManager for -/// possible processing. This is only used when Esp32WiFiManager is managing -/// both the WiFi and mDNS systems, if these are managed externally the -/// consumer is responsible for calling Esp32WiFiManager::process_wifi_event -/// when WiFi events occur. -static esp_err_t wifi_event_handler(void *context, system_event_t *event) -{ - auto wifi = static_cast(context); - wifi->process_wifi_event(event); - return ESP_OK; -} - /// Adapter class to load/store configuration via CDI class Esp32SocketParams : public DefaultSocketClientParams { @@ -199,15 +169,17 @@ class Esp32SocketParams : public DefaultSocketClientParams : configFd_(fd) , cfg_(cfg) { + // set the parameters on the parent class, all others are loaded + // on-demand. mdnsService_ = cfg_.auto_address().service_name().read(configFd_); staticHost_ = cfg_.manual_address().ip_address().read(configFd_); staticPort_ = CDI_READ_TRIMMED(cfg_.manual_address().port, configFd_); } /// @return search mode for how to locate the server. - SearchMode search_mode() override + SocketClientParams::SearchMode search_mode() override { - return (SearchMode)CDI_READ_TRIMMED(cfg_.search_mode, configFd_); + return (SocketClientParams::SearchMode)CDI_READ_TRIMMED(cfg_.search_mode, configFd_); } /// @return null or empty string if any mdns server is okay to connect @@ -302,36 +274,25 @@ class Esp32SocketParams : public DefaultSocketClientParams // With this constructor being used the Esp32WiFiManager will manage the // WiFi connection, mDNS system and the hostname of the ESP32. -Esp32WiFiManager::Esp32WiFiManager(const char *ssid - , const char *password - , SimpleCanStackBase *stack - , const WiFiConfiguration &cfg - , const char *hostname_prefix - , wifi_mode_t wifi_mode - , tcpip_adapter_ip_info_t *station_static_ip - , ip_addr_t primary_dns_server - , uint8_t soft_ap_channel - , wifi_auth_mode_t soft_ap_auth - , const char *soft_ap_password - , tcpip_adapter_ip_info_t *softap_static_ip) - : DefaultConfigUpdateListener() +Esp32WiFiManager::Esp32WiFiManager(const char *station_ssid + , const char *station_password, openlcb::SimpleStackBase *stack + , const WiFiConfiguration &cfg, wifi_mode_t wifi_mode + , uint8_t connection_mode, const char *hostname_prefix + , const char *sntp_server, const char *timezone, bool sntp_enabled + , uint8_t softap_channel, wifi_auth_mode_t softap_auth_mode + , const char *softap_ssid, const char *softap_password) + : DefaultConfigUpdateListener(), Service(&executor_) , hostname_(hostname_prefix) - , ssid_(ssid) - , password_(password) - , cfg_(cfg) - , manageWiFi_(true) - , stack_(stack) - , wifiMode_(wifi_mode) - , stationStaticIP_(station_static_ip) - , primaryDNSAddress_(primary_dns_server) - , softAPChannel_(soft_ap_channel) - , softAPAuthMode_(soft_ap_auth) - , softAPPassword_(soft_ap_password ? soft_ap_password : password) - , softAPStaticIP_(softap_static_ip) + , ssid_(station_ssid), password_(station_password), cfg_(cfg) + , stack_(stack), wifiMode_(wifi_mode), softAPChannel_(softap_channel) + , softAPAuthMode_(softap_auth_mode), softAPName_(softap_ssid) + , softAPPassword_(softap_password), sntpEnabled_(sntp_enabled) + , sntpServer_(sntp_server), timeZone_(timezone) + , connectionMode_(connection_mode) { // Extend the capacity of the hostname to make space for the node-id and // underscore. - hostname_.reserve(TCPIP_HOSTNAME_MAX_SIZE); + hostname_.reserve(MAX_HOSTNAME_LENGTH); // Generate the hostname for the ESP32 based on the provided node id. // node_id : 0x050101011425 @@ -342,29 +303,76 @@ Esp32WiFiManager::Esp32WiFiManager(const char *ssid // The maximum length hostname for the ESP32 is 32 characters so truncate // when necessary. Reference to length limitation: // https://github.com/espressif/esp-idf/blob/master/components/tcpip_adapter/include/tcpip_adapter.h#L611 - if (hostname_.length() > TCPIP_HOSTNAME_MAX_SIZE) + if (hostname_.length() > MAX_HOSTNAME_LENGTH) { - LOG(WARNING, "ESP32 hostname is too long, original hostname: %s", + LOG(WARNING, "ESP32 hostname is too long, original hostname:%s", hostname_.c_str()); - hostname_.resize(TCPIP_HOSTNAME_MAX_SIZE); - LOG(WARNING, "truncated hostname: %s", hostname_.c_str()); + hostname_.resize(MAX_HOSTNAME_LENGTH); + LOG(WARNING, "truncated hostname:%s", hostname_.c_str()); } // Release any extra capacity allocated for the hostname. hostname_.shrink_to_fit(); } -// With this constructor being used, it will be the responsibility of the -// application to manage the WiFi and mDNS systems. -Esp32WiFiManager::Esp32WiFiManager( - SimpleCanStackBase *stack, const WiFiConfiguration &cfg) - : DefaultConfigUpdateListener() - , cfg_(cfg) - , manageWiFi_(false) - , stack_(stack) +Esp32WiFiManager::~Esp32WiFiManager() +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) + // Remove our event listeners from the event loop, note that we do not stop + // the event loop. + esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID + , &Esp32WiFiManager::process_idf_event); + esp_event_handler_unregister(IP_EVENT, ESP_EVENT_ANY_ID + , &Esp32WiFiManager::process_idf_event); +#else + // Disconnect from the event loop to prevent a possible null deref. + esp_event_loop_set_cb(nullptr, nullptr); +#endif // IDF v4.1+ + + // shutdown the background task executor. + executor_.shutdown(); + + // cleanup event group + vEventGroupDelete(wifiStatusEventGroup_); + + // cleanup internal vectors/maps + ssidScanResults_.clear(); + mdnsDeferredPublish_.clear(); +} + +void Esp32WiFiManager::display_configuration() +{ + static const char * const wifiModes[] = + { + "Off", + "Station Only", + "SoftAP Only", + "Station and SoftAP", + "Unknown" + }; + + LOG(INFO, "[WiFi] Configuration Settings:"); + LOG(INFO, "[WiFi] Mode:%s (%d)", wifiModes[wifiMode_], wifiMode_); + LOG(INFO, "[WiFi] Station SSID:'%s'", ssid_.c_str()); + LOG(INFO, "[WiFi] SoftAP SSID:'%s'", softAPName_.c_str()); + LOG(INFO, "[WiFi] Hostname:%s", hostname_.c_str()); + LOG(INFO, "[WiFi] SNTP Enabled:%s, Server:%s, TimeZone:%s", + sntpEnabled_ ? "true" : "false", sntpServer_.c_str(), + timeZone_.c_str()); +} + +#ifndef CONFIG_FREERTOS_UNICORE +/// Entrypoint for Esp32WiFiManager Executor thread when running on an ESP32 +/// with more than one core. +/// +/// @param param @ref Executor to be started. +static void thread_entry(void *param) { - // Nothing to do here. + // donate our task to the executor. + static_cast *>(param)->thread_body(); + vTaskDelete(nullptr); } +#endif // !CONFIG_FREERTOS_UNICORE ConfigUpdateListener::UpdateAction Esp32WiFiManager::apply_configuration( int fd, bool initial_load, BarrierNotifiable *done) @@ -375,9 +383,11 @@ ConfigUpdateListener::UpdateAction Esp32WiFiManager::apply_configuration( // Cache the fd for later use by the wifi background task. configFd_ = fd; - configReloadRequested_ = initial_load; - // Load the CDI entry into memory to do an CRC-32 check against our last + // always load the connection mode. + connectionMode_ = CDI_READ_TRIMMED(cfg_.connection_mode, fd); + + // Load the CDI entry into memory to do an CRC32 check against our last // loaded configuration so we can avoid reloading configuration when there // are no interesting changes. unique_ptr crcbuf(new uint8_t[cfg_.size()]); @@ -393,30 +403,35 @@ ConfigUpdateListener::UpdateAction Esp32WiFiManager::apply_configuration( // Read the full configuration to the buffer for crc check. FdUtils::repeated_read(fd, crcbuf.get(), cfg_.size()); - // Calculate CRC-32 from the loaded buffer. + // Calculate CRC32 from the loaded buffer. uint32_t configCrc32 = crc32_le(0, crcbuf.get(), cfg_.size()); - LOG(VERBOSE, "existing config CRC-32: \"%s\", new CRC-32: \"%s\"", - integer_to_string(configCrc32_, 0).c_str(), - integer_to_string(configCrc32, 0).c_str()); + LOG(VERBOSE, "existing config CRC32:%d, new CRC32:%d", configCrc32_, + configCrc32); - // if this is not the initial loading of the CDI entry check the CRC-32 - // value and trigger a configuration reload if necessary. - if (!initial_load) + if (initial_load) { - if (configCrc32 != configCrc32_) - { - configReloadRequested_ = true; - // If a configuration change has been detected, wake up the - // wifi_manager_task so it can consume the change prior to the next - // wake up interval. - xTaskNotifyGive(wifiTaskHandle_); - } + // If we have more than one core start the Executor on the APP CPU (1) + // since the main OpenMRN Executor normally will run on the PRO CPU (0) +#ifndef CONFIG_FREERTOS_UNICORE + xTaskCreatePinnedToCore(&thread_entry, // entry point + "Esp32WiFiConn", // task name + EXECUTOR_TASK_STACK_SIZE, // stack size + &executor_, // entry point arg + EXECUTOR_TASK_PRIORITY, // priority + nullptr, // task handle + APP_CPU_NUM); // cpu core +#else + // start the background task executor since it will be used for any + // callback notifications that arise from starting the network stack. + executor_.start_thread( + "Esp32WiFiConn", EXECUTOR_TASK_PRIORITY, EXECUTOR_TASK_STACK_SIZE); +#endif // !CONFIG_FREERTOS_UNICORE } - else + else if (configCrc32 != configCrc32_) { - // This is the initial loading of the CDI entry, start the background - // task that will manage the node's WiFi connection(s). - start_wifi_task(); + // If a configuration change has been detected, wake up the wifi stack + // so it can consume the change. + wifiStackFlow_.notify(); } // Store the calculated CRC-32 for future use when the apply_configuration @@ -435,7 +450,9 @@ void Esp32WiFiManager::factory_reset(int fd) // General WiFi configuration settings. CDI_FACTORY_RESET(cfg_.sleep); - CDI_FACTORY_RESET(cfg_.connection_mode); + // NOTE: this is not using CDI_FACTORY_RESET so consumers can provide a + // default value as part of constructing the Esp32WiFiManager instance. + cfg_.connection_mode().write(fd, connectionMode_); // Hub specific configuration settings. CDI_FACTORY_RESET(cfg_.hub().port); @@ -463,572 +480,162 @@ void Esp32WiFiManager::factory_reset(int fd) CDI_FACTORY_RESET(cfg_.uplink().reconnect); } -// Processes a WiFi system event -void Esp32WiFiManager::process_wifi_event(system_event_t *event) +#if defined(ESP_IDF_VERSION) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) +void Esp32WiFiManager::process_idf_event(void *arg, esp_event_base_t event_base + , int32_t event_id, void *event_data) { - LOG(VERBOSE, "Esp32WiFiManager::process_wifi_event(%d)", event->event_id); - - // We only are interested in this event if we are managing the - // WiFi and MDNS systems and our mode includes STATION. - if (event->event_id == SYSTEM_EVENT_STA_START && manageWiFi_ && - (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_STA)) + LOG(VERBOSE, "Esp32WiFiManager::process_idf_event(%s, %d, %p)", event_base + , event_id, event_data); + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { - // Set the generated hostname prior to connecting to the SSID - // so that it shows up with the generated hostname instead of - // the default "Espressif". - LOG(INFO, "[WiFi] Setting ESP32 hostname to \"%s\".", - hostname_.c_str()); - ESP_ERROR_CHECK(tcpip_adapter_set_hostname( - TCPIP_ADAPTER_IF_STA, hostname_.c_str())); - uint8_t mac[6]; - esp_wifi_get_mac(WIFI_IF_STA, mac); - LOG(INFO, "[WiFi] MAC Address: %s", mac_to_string(mac).c_str()); - - if (stationStaticIP_) - { - // Stop the DHCP service before connecting, this allows us to - // specify a static IP address for the WiFi connection - LOG(INFO, "[DHCP] Stopping DHCP Client (if running)."); - ESP_ERROR_CHECK( - tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA)); - - LOG(INFO, - "[WiFi] Configuring Static IP address:\n" - "IP : " IPSTR "\n" - "Gateway: " IPSTR "\n" - "Netmask: " IPSTR, - IP2STR(&stationStaticIP_->ip), - IP2STR(&stationStaticIP_->gw), - IP2STR(&stationStaticIP_->netmask)); - ESP_ERROR_CHECK( - tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, - stationStaticIP_)); - - // if we do not have a primary DNS address configure the default - if (ip_addr_isany(&primaryDNSAddress_)) - { - IP4_ADDR(&primaryDNSAddress_.u_addr.ip4, 8, 8, 8, 8); - } - LOG(INFO, "[WiFi] Configuring primary DNS address to: " IPSTR, - IP2STR(&primaryDNSAddress_.u_addr.ip4)); - // set the primary server (0) - dns_setserver(0, &primaryDNSAddress_); - } - else - { - // Start the DHCP service before connecting so it hooks into - // the flow early and provisions the IP automatically. - LOG(INFO, "[DHCP] Starting DHCP Client."); - ESP_ERROR_CHECK( - tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA)); - } - - LOG(INFO, - "[WiFi] Station started, attempting to connect to SSID: %s.", - ssid_); - // Start the SSID connection process. - esp_wifi_connect(); + Singleton::instance()->on_station_started(); } - else if (event->event_id == SYSTEM_EVENT_STA_CONNECTED) + else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) { - LOG(INFO, "[WiFi] Connected to SSID: %s", ssid_); - // Set the flag that indictes we are connected to the SSID. - xEventGroupSetBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT); + Singleton::instance()->on_station_connected(); } - else if (event->event_id == SYSTEM_EVENT_STA_GOT_IP) + else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_STA_DISCONNECTED) { - // Retrieve the configured IP address from the TCP/IP stack. - tcpip_adapter_ip_info_t ip_info; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info); - LOG(INFO, "[WiFi] IP address is " IPSTR ".", IP2STR(&ip_info.ip)); - - // Start the mDNS system since we have an IP address, the mDNS system - // on the ESP32 requires that the IP address be assigned otherwise it - // will not start the UDP listener. - start_mdns_system(); - - // Set the flag that indictes we have an IPv4 address. - xEventGroupSetBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); - - // Wake up the wifi_manager_task so it can start connections - // creating connections, this will be a no-op for initial startup. - xTaskNotifyGive(wifiTaskHandle_); + Singleton::instance()->on_station_disconnected( + static_cast(event_data)->reason); } - else if (event->event_id == SYSTEM_EVENT_STA_LOST_IP) + else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { - // Clear the flag that indicates we are connected and have an - // IPv4 address. - xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); - // Wake up the wifi_manager_task so it can clean up connections. - xTaskNotifyGive(wifiTaskHandle_); + Singleton::instance()->on_softap_start(); } - else if (event->event_id == SYSTEM_EVENT_STA_DISCONNECTED) + else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STOP) { - // flag to indicate that we should print the reconnecting log message. - bool was_previously_connected = false; - - // Check if we have already connected, this event can be raised - // even before we have successfully connected during the SSID - // connect process. - if (xEventGroupGetBits(wifiStatusEventGroup_) & WIFI_CONNECTED_BIT) - { - // track that we were connected previously. - was_previously_connected = true; - - LOG(INFO, "[WiFi] Lost connection to SSID: %s (reason:%d)", ssid_ - , event->event_info.disconnected.reason); - // Clear the flag that indicates we are connected to the SSID. - xEventGroupClearBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT); - // Clear the flag that indicates we have an IPv4 address. - xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); - - // Wake up the wifi_manager_task so it can clean up - // connections. - xTaskNotifyGive(wifiTaskHandle_); - } - - // If we are managing the WiFi and MDNS systems we need to - // trigger the reconnection process at this point. - if (manageWiFi_) - { - if (was_previously_connected) - { - LOG(INFO, "[WiFi] Attempting to reconnect to SSID: %s.", - ssid_); - } - else - { - LOG(INFO, - "[WiFi] Connection failed, reconnecting to SSID: %s.", - ssid_); - } - esp_wifi_connect(); - } + Singleton::instance()->on_softap_stop(); } - else if (event->event_id == SYSTEM_EVENT_AP_START && manageWiFi_) + else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_AP_STACONNECTED) { - // Set the generated hostname prior to connecting to the SSID - // so that it shows up with the generated hostname instead of - // the default "Espressif". - LOG(INFO, "[SoftAP] Setting ESP32 hostname to \"%s\".", - hostname_.c_str()); - ESP_ERROR_CHECK(tcpip_adapter_set_hostname( - TCPIP_ADAPTER_IF_AP, hostname_.c_str())); - - uint8_t mac[6]; - esp_wifi_get_mac(WIFI_IF_AP, mac); - LOG(INFO, "[SoftAP] MAC Address: %s", mac_to_string(mac).c_str()); - - // If the SoftAP is not configured to use a static IP it will default - // to 192.168.4.1. - if (softAPStaticIP_ && wifiMode_ != WIFI_MODE_STA) - { - // Stop the DHCP server so we can reconfigure it. - LOG(INFO, "[SoftAP] Stopping DHCP Server (if running)."); - ESP_ERROR_CHECK(tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP)); - - LOG(INFO, - "[SoftAP] Configuring Static IP address:\n" - "IP : " IPSTR "\n" - "Gateway: " IPSTR "\n" - "Netmask: " IPSTR, - IP2STR(&softAPStaticIP_->ip), - IP2STR(&softAPStaticIP_->gw), - IP2STR(&softAPStaticIP_->netmask)); - ESP_ERROR_CHECK( - tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, - softAPStaticIP_)); - - // Convert the Soft AP Static IP to a uint32 for manipulation - uint32_t apIP = ntohl(ip4_addr_get_u32(&softAPStaticIP_->ip)); - - // Default configuration is for DHCP addresses to follow - // immediately after the static ip address of the Soft AP. - ip4_addr_t first_ip, last_ip; - ip4_addr_set_u32(&first_ip, htonl(apIP + 1)); - ip4_addr_set_u32(&last_ip, - htonl(apIP + SOFTAP_IP_RESERVATION_BLOCK_SIZE)); - - dhcps_lease_t dhcp_lease { - true, // enable dhcp lease functionality - first_ip, // first ip to assign - last_ip, // last ip to assign - }; - - LOG(INFO, - "[SoftAP] Configuring DHCP Server for IPs: " IPSTR " - " IPSTR, - IP2STR(&dhcp_lease.start_ip), IP2STR(&dhcp_lease.end_ip)); - ESP_ERROR_CHECK( - tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, - TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, - (void *)&dhcp_lease, - sizeof(dhcps_lease_t))); - - // Start the DHCP server so it can provide IP addresses to stations - // when they connect. - LOG(INFO, "[SoftAP] Starting DHCP Server."); - ESP_ERROR_CHECK( - tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP)); - } - - // If we are not operating in SoftAP mode only we can start the mDNS - // system now, otherwise we need to defer it until the station has - // received it's IP address to avoid reinitializing the mDNS system. - if (wifiMode_ == WIFI_MODE_AP) - { - start_mdns_system(); - } + auto sta_data = static_cast(event_data); + Singleton::instance()->on_softap_station_connected( + sta_data->mac, sta_data->aid); } - else if (event->event_id == SYSTEM_EVENT_AP_STACONNECTED) + else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_AP_STADISCONNECTED) { - LOG(INFO, "[SoftAP aid:%d] %s connected.", - event->event_info.sta_connected.aid, - mac_to_string(event->event_info.sta_connected.mac).c_str()); + auto sta_data = static_cast(event_data); + Singleton::instance()->on_softap_station_disconnected( + sta_data->mac, sta_data->aid); } - else if (event->event_id == SYSTEM_EVENT_AP_STADISCONNECTED) + else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { - LOG(INFO, "[SoftAP aid:%d] %s disconnected.", - event->event_info.sta_disconnected.aid, - mac_to_string(event->event_info.sta_connected.mac).c_str()); + auto scan_data = static_cast(event_data); + Singleton::instance()->on_wifi_scan_completed( + scan_data->status, scan_data->number); } - else if (event->event_id == SYSTEM_EVENT_SCAN_DONE) + else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { - { - OSMutexLock l(&ssidScanResultsLock_); - uint16_t num_found{0}; - esp_wifi_scan_get_ap_num(&num_found); - LOG(VERBOSE, "[WiFi] %d SSIDs found via scan", num_found); - ssidScanResults_.resize(num_found); - esp_wifi_scan_get_ap_records(&num_found, ssidScanResults_.data()); -#if LOGLEVEL >= VERBOSE - for (int i = 0; i < num_found; i++) - { - LOG(VERBOSE, "SSID: %s, RSSI: %d, channel: %d" - , ssidScanResults_[i].ssid - , ssidScanResults_[i].rssi, ssidScanResults_[i].primary); - } -#endif - } - if (ssidCompleteNotifiable_) - { - ssidCompleteNotifiable_->notify(); - ssidCompleteNotifiable_ = nullptr; - } + ip_event_got_ip_t *data = static_cast(event_data); + Singleton::instance()->on_station_ip_assigned( + htonl(data->ip_info.ip.addr)); } - + else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { - OSMutexLock l(&eventCallbacksLock_); - // Pass the event received from ESP-IDF to any registered callbacks. - for(auto callback : eventCallbacks_) - { - callback(event); - } + Singleton::instance()->on_station_ip_lost(); } } -void Esp32WiFiManager::enable_verbose_logging() -{ - esp32VerboseLogging_ = true; - enable_esp_wifi_logging(); -} - -// If the Esp32WiFiManager is setup to manage the WiFi system, the following -// steps are executed: -// 1) Start the TCP/IP adapter. -// 2) Hook into the ESP event loop so we receive WiFi events. -// 3) Initialize the WiFi system. -// 4) Set the WiFi mode to STATION (WIFI_STA) -// 5) Configure the WiFi system to store parameters only in memory to avoid -// potential corruption of entries in NVS. -// 6) Configure the WiFi system for SSID/PW. -// 7) Set the hostname based on the generated hostname. -// 8) Connect to WiFi and wait for IP assignment. -// 9) Verify that we connected and received a IP address, if not log a FATAL -// message and give up. -void Esp32WiFiManager::start_wifi_system() +#else +// Processes a WiFi system event +esp_err_t Esp32WiFiManager::process_wifi_event(void *ctx, system_event_t *event) { - // Create the event group used for tracking connected/disconnected status. - // This is used internally regardless of if we manage the rest of the WiFi - // or mDNS systems. - wifiStatusEventGroup_ = xEventGroupCreate(); + LOG(VERBOSE, "Esp32WiFiManager::process_wifi_event(%d)", event->event_id); - // If we do not need to manage the WiFi and mDNS systems exit early. - if (!manageWiFi_) + if (event->event_id == SYSTEM_EVENT_STA_START) { - return; + Singleton::instance()->on_station_started(); } - - // Initialize the TCP/IP adapter stack. - LOG(INFO, "[WiFi] Starting TCP/IP stack"); - tcpip_adapter_init(); - - // Install event loop handler. - ESP_ERROR_CHECK(esp_event_loop_init(wifi_event_handler, this)); - - // Start the WiFi adapter. - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - LOG(INFO, "[WiFi] Initializing WiFi stack"); - - // Disable NVS storage for the WiFi driver - cfg.nvs_enable = false; - - // override the defaults coming from arduino-esp32, the ones below improve - // throughput and stability of TCP/IP, for more info on these values, see: - // https://github.com/espressif/arduino-esp32/issues/2899 and - // https://github.com/espressif/arduino-esp32/pull/2912 - // - // Note: these numbers are slightly higher to allow compatibility with the - // WROVER chip and WROOM-32 chip. The increase results in ~2kb less heap - // at runtime. - // - // These do not require recompilation of arduino-esp32 code as these are - // used in the WIFI_INIT_CONFIG_DEFAULT macro, they simply need to be redefined. - cfg.static_rx_buf_num = 16; - cfg.dynamic_rx_buf_num = 32; - cfg.rx_ba_win = 16; - - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - - if (esp32VerboseLogging_) + else if (event->event_id == SYSTEM_EVENT_STA_CONNECTED) { - enable_esp_wifi_logging(); + Singleton::instance()->on_station_connected(); } - - wifi_mode_t requested_wifi_mode = wifiMode_; - if (wifiMode_ == WIFI_MODE_AP) + else if (event->event_id == SYSTEM_EVENT_STA_DISCONNECTED) { - // override the wifi mode from AP only to AP+STA so we can perform wifi - // scans on demand. - requested_wifi_mode = WIFI_MODE_APSTA; + Singleton::instance()->on_station_disconnected( + event->event_info.disconnected.reason); } - // Set the requested WiFi mode. - ESP_ERROR_CHECK(esp_wifi_set_mode(requested_wifi_mode)); - - // This disables storage of SSID details in NVS which has been shown to be - // problematic at times for the ESP32, it is safer to always pass fresh - // config and have the ESP32 resolve the details at runtime rather than - // use a cached set from NVS. - esp_wifi_set_storage(WIFI_STORAGE_RAM); - - // If we want to host a SoftAP configure it now. - if (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_AP) + else if (event->event_id == SYSTEM_EVENT_STA_GOT_IP) { - wifi_config_t conf; - bzero(&conf, sizeof(wifi_config_t)); - conf.ap.authmode = softAPAuthMode_; - conf.ap.beacon_interval = 100; - conf.ap.channel = softAPChannel_; - conf.ap.max_connection = 4; - if (wifiMode_ == WIFI_MODE_AP) - { - // Configure the SSID for the Soft AP based on the SSID passed to - // the Esp32WiFiManager constructor. - strcpy(reinterpret_cast(conf.ap.ssid), ssid_); - } - else - { - // Configure the SSID for the Soft AP based on the generated - // hostname when operating in WIFI_MODE_APSTA mode. - strcpy(reinterpret_cast(conf.ap.ssid), hostname_.c_str()); - } - - if (password_ && softAPAuthMode_ != WIFI_AUTH_OPEN) - { - strcpy(reinterpret_cast(conf.ap.password), password_); - } - - LOG(INFO, "[WiFi] Configuring SoftAP (SSID: %s)", conf.ap.ssid); - ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &conf)); + Singleton::instance()->on_station_ip_assigned( + htonl(event->event_info.got_ip.ip_info.ip.addr)); } - - // If we need to connect to an SSID, configure it now. - if (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_STA) + else if (event->event_id == SYSTEM_EVENT_STA_LOST_IP) { - // Configure the SSID details for the station based on the SSID and - // password provided to the Esp32WiFiManager constructor. - wifi_config_t conf; - bzero(&conf, sizeof(wifi_config_t)); - strcpy(reinterpret_cast(conf.sta.ssid), ssid_); - if (password_) - { - strcpy(reinterpret_cast(conf.sta.password), password_); - } - - LOG(INFO, "[WiFi] Configuring Station (SSID: %s)", conf.sta.ssid); - ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &conf)); + Singleton::instance()->on_station_ip_lost(); } - - // Start the WiFi stack. This will start the SoftAP and/or connect to the - // SSID based on the configuration set above. - LOG(INFO, "[WiFi] Starting WiFi stack"); - ESP_ERROR_CHECK(esp_wifi_start()); - - // If we are using the STATION interface, this will block until the ESP32 - // starts the connection process, note it may not have an IP address - // immediately thus the need to check the connection result a few times - // before giving up with a FATAL error. - if (wifiMode_ == WIFI_MODE_APSTA || wifiMode_ == WIFI_MODE_STA) + else if (event->event_id == SYSTEM_EVENT_AP_START) { - uint8_t attempt = 0; - EventBits_t bits = 0; - uint32_t bit_mask = WIFI_CONNECTED_BIT; - while (++attempt <= MAX_CONNECTION_CHECK_ATTEMPTS) - { - // If we have connected to the SSID we then are waiting for IP - // address. - if (bits & WIFI_CONNECTED_BIT) - { - LOG(INFO, "[IPv4] [%d/%d] Waiting for IP address assignment.", - attempt, MAX_CONNECTION_CHECK_ATTEMPTS); - } - else - { - // Waiting for SSID connection - LOG(INFO, "[WiFi] [%d/%d] Waiting for SSID connection.", - attempt, MAX_CONNECTION_CHECK_ATTEMPTS); - } - bits = xEventGroupWaitBits(wifiStatusEventGroup_, - bit_mask, // bits we are interested in - pdFALSE, // clear on exit - pdTRUE, // wait for all bits - WIFI_CONNECT_CHECK_INTERVAL); - // Check if have connected to the SSID - if (bits & WIFI_CONNECTED_BIT) - { - // Since we have connected to the SSID we now need to track - // that we get an IP. - bit_mask |= WIFI_GOTIP_BIT; - } - // Check if we have received an IP. - if (bits & WIFI_GOTIP_BIT) - { - break; - } - } - - // Check if we successfully connected or not. If not, force a reboot. - if ((bits & WIFI_CONNECTED_BIT) != WIFI_CONNECTED_BIT) - { - LOG(FATAL, "[WiFi] Failed to connect to SSID: %s.", ssid_); - } - - // Check if we successfully connected or not. If not, force a reboot. - if ((bits & WIFI_GOTIP_BIT) != WIFI_GOTIP_BIT) - { - LOG(FATAL, "[IPv4] Timeout waiting for an IP."); - } + Singleton::instance()->on_softap_start(); + } + else if (event->event_id == SYSTEM_EVENT_AP_STOP) + { + Singleton::instance()->on_softap_stop(); + } + else if (event->event_id == SYSTEM_EVENT_AP_STACONNECTED) + { + auto sta_data = event->event_info.sta_connected; + Singleton::instance()->on_softap_station_connected( + sta_data.mac, sta_data.aid); + } + else if (event->event_id == SYSTEM_EVENT_AP_STADISCONNECTED) + { + auto sta_data = event->event_info.sta_connected; + Singleton::instance()->on_softap_station_disconnected( + sta_data.mac, sta_data.aid); + } + else if (event->event_id == SYSTEM_EVENT_SCAN_DONE) + { + auto scan_data = event->event_info.scan_done; + Singleton::instance()->on_wifi_scan_completed( + scan_data.status, scan_data.number); } + + return ESP_OK; } +#endif -// Starts a background task for the Esp32WiFiManager. -void Esp32WiFiManager::start_wifi_task() +// Adds a callback which will be called when the network is up. +void Esp32WiFiManager::register_network_up_callback( + esp_network_up_callback_t callback) { - LOG(INFO, "[WiFi] Starting WiFi Manager task"); - os_thread_create(&wifiTaskHandle_, "Esp32WiFiMgr", WIFI_TASK_PRIORITY, - WIFI_TASK_STACK_SIZE, wifi_manager_task, this); + OSMutexLock l(&networkCallbacksLock_); + networkUpCallbacks_.push_back(callback); } -// Background task for the Esp32WiFiManager. This handles all outbound -// connection attempts, configuration loading and making this node as a hub. -void *Esp32WiFiManager::wifi_manager_task(void *param) +// Adds a callback which will be called when the network is down. +void Esp32WiFiManager::register_network_down_callback( + esp_network_down_callback_t callback) { - Esp32WiFiManager *wifi = static_cast(param); - - // Start the WiFi system before proceeding with remaining tasks. - wifi->start_wifi_system(); - - while (true) - { - EventBits_t bits = xEventGroupGetBits(wifi->wifiStatusEventGroup_); - if (bits & WIFI_GOTIP_BIT) - { - // If we do not have not an uplink connection force a config reload - // to start the connection process. - if (!wifi->uplink_) - { - wifi->configReloadRequested_ = true; - } - } - else - { - // Since we do not have an IP address we need to shutdown any - // active connections since they will be invalid until a new IP - // has been provisioned. - wifi->stop_hub(); - wifi->stop_uplink(); - - // Make sure we don't try and reload configuration since we can't - // create outbound connections at this time. - wifi->configReloadRequested_ = false; - } - - // Check if there are configuration changes to pick up. - if (wifi->configReloadRequested_) - { - // Since we are loading configuration data, shutdown the hub and - // uplink if created previously. - wifi->stop_hub(); - wifi->stop_uplink(); - - if (CDI_READ_TRIMMED(wifi->cfg_.sleep, wifi->configFd_)) - { - // When sleep is enabled this will trigger the WiFi system to - // only wake up every DTIM period to receive beacon updates. - // no data loss is expected for this setting but it does delay - // receiption until the DTIM period. - ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); - } - else - { - // When sleep is disabled the WiFi radio will always be active. - // This will increase power consumption of the ESP32 but it - // will result in a more reliable behavior when the ESP32 is - // connected to an always-on power supply (ie: not a battery). - ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); - } - - bool have_hub = false; - uint8_t conn_cfg = CDI_READ_TRIMMED(wifi->cfg_.connection_mode, - wifi->configFd_); - if (conn_cfg & 2) - { - LOG(INFO, "[WiFi] Starting hub."); - // Since hub mode is enabled start the HUB creation process. - wifi->start_hub(); - have_hub = true; - } else { - LOG(INFO, "[WiFi] Hub disabled by configuration."); - } - if (conn_cfg & 1) { - LOG(INFO, "[WiFi] Starting uplink."); - wifi->start_uplink(); - } else if (!have_hub) { - LOG(INFO, "[WiFi] Starting uplink, because hub is disabled."); - wifi->start_uplink(); - } else { - LOG(INFO, "[WiFi] Uplink disabled by configuration."); - } - - wifi->configReloadRequested_ = false; - } + OSMutexLock l(&networkCallbacksLock_); + networkDownCallbacks_.push_back(callback); +} - // Sleep until we are woken up again for configuration update or WiFi - // event. - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - } +// Adds a callback which will be called when the network is initializing. +void Esp32WiFiManager::register_network_init_callback( + esp_network_init_callback_t callback) +{ + OSMutexLock l(&networkCallbacksLock_); + networkInitCallbacks_.push_back(callback); +} - return nullptr; +// Adds a callback which will be called when SNTP packets are processed. +void Esp32WiFiManager::register_network_time_callback( + esp_network_time_callback_t callback) +{ + OSMutexLock l(&networkCallbacksLock_); + networkTimeCallbacks_.push_back(callback); } // Shuts down the hub listener (if enabled and running) for this node. void Esp32WiFiManager::stop_hub() { - if (hub_) - { - mdns_unpublish(hubServiceName_); - LOG(INFO, "[HUB] Shutting down TCP/IP listener"); - hub_.reset(nullptr); - } + auto stack = static_cast(stack_); + stack->shutdown_tcp_hub_server(); } // Creates a hub listener for this node after loading configuration details. @@ -1037,11 +644,13 @@ void Esp32WiFiManager::start_hub() hubServiceName_ = cfg_.hub().service_name().read(configFd_); uint16_t hub_port = CDI_READ_TRIMMED(cfg_.hub().port, configFd_); - LOG(INFO, "[HUB] Starting TCP/IP listener on port %d", hub_port); - hub_.reset(new GcTcpHub(stack_->can_hub(), hub_port)); + LOG(INFO, "[Hub] Starting TCP/IP listener on port %d", hub_port); + auto stack = static_cast(stack_); + stack->start_tcp_hub_server(hub_port); + auto hub = stack->get_tcp_hub_server(); // wait for the hub to complete it's startup tasks - while (!hub_->is_started()) + while (!hub->is_started()) { usleep(HUB_STARTUP_DELAY_USEC); } @@ -1053,9 +662,15 @@ void Esp32WiFiManager::stop_uplink() { if (uplink_) { - LOG(INFO, "[UPLINK] Disconnecting from uplink."); + LOG(INFO, "[Uplink] Disconnecting from uplink."); uplink_->shutdown(); uplink_.reset(nullptr); + + // Mark our cached notifiable as invalid + uplinkNotifiable_ = nullptr; + + // force close the socket to ensure it is disconnected and cleaned up. + ::close(uplinkFd_); } } @@ -1065,27 +680,42 @@ void Esp32WiFiManager::start_uplink() { unique_ptr params( new Esp32SocketParams(configFd_, cfg_.uplink())); - uplink_.reset(new SocketClient(stack_->service(), &connectExecutor_, - &connectExecutor_, std::move(params), - std::bind(&Esp32WiFiManager::on_uplink_created, this, - std::placeholders::_1, std::placeholders::_2))); - if (!connectExecutorStarted_) { - connectExecutorStarted_ = true; - connectExecutor_.start_thread( - "Esp32WiFiConn", CONNECT_TASK_PRIORITY, CONNECT_TASK_STACK_SIZE); + + if (uplink_) + { + // If we already have an uplink, update the parameters it is using. + uplink_->reset_params(std::move(params)); + } + else + { + // create a new uplink and pass in the parameters. + uplink_.reset(new SocketClient(stack_->service(), &executor_, + &executor_, std::move(params), + std::bind(&Esp32WiFiManager::on_uplink_created, this, + std::placeholders::_1, std::placeholders::_2))); } } // Converts the passed fd into a GridConnect port and adds it to the stack. void Esp32WiFiManager::on_uplink_created(int fd, Notifiable *on_exit) { - LOG(INFO, "[UPLINK] Connected to hub, configuring GridConnect port."); + LOG(INFO, "[Uplink] Connected to hub, configuring GridConnect HubPort."); + + // stash the socket handle and notifiable for future use if we need to + // clean up or re-establish the connection. + uplinkFd_ = fd; + uplinkNotifiable_ = on_exit; const bool use_select = (config_gridconnect_tcp_use_select() == CONSTANT_TRUE); // create the GridConnect port from the provided socket fd. - create_gc_port_for_can_hub(stack_->can_hub(), fd, on_exit, use_select); + // NOTE: this uses a local notifiable object instead of the provided + // on_exit since it will be invalidated upon calling stop_uplink() which + // may result in a crash or other undefined behavior. + create_gc_port_for_can_hub( + static_cast(stack_)->can_hub(), + uplinkFd_, &uplinkNotifiableProxy_, use_select); // restart the stack to kick off alias allocation and send node init // packets. @@ -1170,14 +800,14 @@ void Esp32WiFiManager::mdns_publish(string service, const uint16_t port) // Schedule the publish to be done through the Executor since we may need // to retry it. - stack_->executor()->add(new CallbackExecutable([service, port]() + executor_.add(new CallbackExecutable([service, port]() { string service_name = service; string protocol_name; split_mdns_service_name(&service_name, &protocol_name); esp_err_t res = mdns_service_add( NULL, service_name.c_str(), protocol_name.c_str(), port, NULL, 0); - LOG(INFO, "[mDNS] mdns_service_add(%s.%s:%d): %s." + LOG(VERBOSE, "[mDNS] mdns_service_add(%s.%s:%d):%s." , service_name.c_str(), protocol_name.c_str(), port , esp_err_to_name(res)); // ESP_FAIL will be triggered if there is a timeout during publish of @@ -1187,13 +817,31 @@ void Esp32WiFiManager::mdns_publish(string service, const uint16_t port) if (res == ESP_FAIL) { // Send it back onto the scheduler to be retried - Singleton::instance()->mdns_publish(service - , port); + Singleton::instance()->mdns_publish( + service, port); + } + else if (res == ESP_ERR_INVALID_ARG) + { + // ESP_ERR_INVALID_ARG can be returned if the mDNS server is not UP + // which we have previously checked via mdnsInitialized_, this can + // also be returned if the service name has already been published + // since the server came up. If we see this error code returned we + // should try unpublish and then publish again. + Singleton::instance()->mdns_unpublish(service); + Singleton::instance()->mdns_publish( + service, port); + } + else if (res == ESP_OK) + { + LOG(INFO, "[mDNS] Advertising %s.%s:%d.", service_name.c_str(), + protocol_name.c_str(), port); } else { - LOG(INFO, "[mDNS] Advertising %s.%s:%d.", service_name.c_str() - , protocol_name.c_str(), port); + LOG_ERROR("[mDNS] Failed to publish:%s.%s:%d", + service_name.c_str(), protocol_name.c_str(), port); + Singleton::instance()->mdns_publish( + service, port); } })); } @@ -1217,7 +865,8 @@ void Esp32WiFiManager::mdns_unpublish(string service) , service_name.c_str(), protocol_name.c_str()); esp_err_t res = mdns_service_remove(service_name.c_str(), protocol_name.c_str()); - LOG(VERBOSE, "[mDNS] mdns_service_remove: %s.", esp_err_to_name(res)); + LOG(VERBOSE, "[mDNS] mdns_service_remove:%s.", esp_err_to_name(res)); + // TODO: should we queue up unpublish requests for future retries? } // Initializes the mDNS system on the ESP32. @@ -1240,7 +889,7 @@ void Esp32WiFiManager::start_mdns_system() // Set the mDNS hostname based on our generated hostname so it can be // found by other nodes. - LOG(INFO, "[mDNS] Setting mDNS hostname to \"%s\"", hostname_.c_str()); + LOG(INFO, "[mDNS] Setting hostname to \"%s\"", hostname_.c_str()); ESP_ERROR_CHECK(mdns_hostname_set(hostname_.c_str())); // Set the default mDNS instance name to the generated hostname. @@ -1258,13 +907,714 @@ void Esp32WiFiManager::start_mdns_system() mdnsDeferredPublish_.clear(); } -} // namespace openmrn_arduino +void Esp32WiFiManager::on_station_started() +{ + // Set the generated hostname prior to connecting to the SSID + // so that it shows up with the generated hostname instead of + // the default "Espressif". + LOG(INFO, "[Station] Setting hostname to \"%s\".", hostname_.c_str()); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) + esp_netif_set_hostname(espNetIfaces_[STATION_INTERFACE], hostname_.c_str()); +#else + ESP_ERROR_CHECK(tcpip_adapter_set_hostname( + TCPIP_ADAPTER_IF_STA, hostname_.c_str())); +#endif + uint8_t mac[6]; + esp_wifi_get_mac(WIFI_IF_STA, mac); + LOG(INFO, "[Station] MAC Address:%s", mac_to_string(mac).c_str()); + + // Start the DHCP service before connecting so it hooks into + // the flow early and provisions the IP automatically. + LOG(INFO, "[Station] Starting DHCP Client."); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) + ESP_ERROR_CHECK(esp_netif_dhcpc_start(espNetIfaces_[STATION_INTERFACE])); +#else + ESP_ERROR_CHECK(tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA)); +#endif // IDF v4.1+ -/// Maximum number of milliseconds to wait for mDNS query responses. -static constexpr uint32_t MDNS_QUERY_TIMEOUT = 2000; + LOG(INFO, "[Station] Connecting to SSID:%s.", ssid_.c_str()); + // Start the SSID connection process. + esp_wifi_connect(); -/// Maximum number of results to capture for mDNS query requests. -static constexpr size_t MDNS_MAX_RESULTS = 10; + // Schedule callbacks via the executor rather than call directly here. + { + OSMutexLock l(&networkCallbacksLock_); + for (esp_network_init_callback_t cb : networkInitCallbacks_) + { + executor_.add(new CallbackExecutable([cb] + { + cb(STATION_INTERFACE); + })); + } + } +} + +void Esp32WiFiManager::on_station_connected() +{ + LOG(INFO, "[Station] Connected to SSID:%s", ssid_.c_str()); + // Set the flag that indictes we are connected to the SSID. + xEventGroupSetBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT); +} + +void Esp32WiFiManager::on_station_disconnected(uint8_t reason) +{ + // flag to indicate that we should print the reconnecting log message. + bool was_previously_connected = false; + + // capture the current state so we can check if we were already connected + // with an IP address or still in the connecting phase. + EventBits_t event_bits = xEventGroupGetBits(wifiStatusEventGroup_); + + // Check if we have already connected, this event can be raised + // even before we have successfully connected during the SSID + // connect process. + if (event_bits & WIFI_CONNECTED_BIT) + { + // If we were previously connected and had an IP address we should + // count that as previously connected, otherwise we will just reconnect + // and not wake up the state flow since it may be waiting for an event + // and will wakeup on it's own. + was_previously_connected = event_bits & WIFI_GOTIP_BIT; + + LOG(INFO, "[Station] Lost connection to SSID:%s (reason:%d)", + ssid_.c_str(), reason); + // Clear the flag that indicates we are connected to the SSID. + xEventGroupClearBits(wifiStatusEventGroup_, WIFI_CONNECTED_BIT); + // Clear the flag that indicates we have an IPv4 address. + xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); + } + + // If we are managing the WiFi and MDNS systems we need to + // trigger the reconnection process at this point. + if (was_previously_connected) + { + LOG(INFO, "[Station] Reconnecting to SSID:%s.", + ssid_.c_str()); + wifiStackFlow_.notify(); + } + else + { + LOG(INFO, + "[Station] Connection to SSID:%s (reason:%d) failed, retrying.", + ssid_.c_str(), reason); + } + esp_wifi_connect(); + + // Schedule callbacks via the executor rather than call directly here. + { + OSMutexLock l(&networkCallbacksLock_); + for (esp_network_init_callback_t cb : networkInitCallbacks_) + { + executor_.add(new CallbackExecutable([cb] + { + cb(STATION_INTERFACE); + })); + } + } +} + +void Esp32WiFiManager::on_station_ip_assigned(uint32_t ip_address) +{ + LOG(INFO, "[Station] IP address:%s", ipv4_to_string(ip_address).c_str()); + + // Start the mDNS system since we have an IP address, the mDNS system + // on the ESP32 requires that the IP address be assigned otherwise it + // will not start the UDP listener. + start_mdns_system(); + + // Set the flag that indictes we have an IPv4 address. + xEventGroupSetBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); + + // wake up the stack to process the event + wifiStackFlow_.notify(); + + // Schedule callbacks via the executor rather than call directly here. + { + OSMutexLock l(&networkCallbacksLock_); + for (esp_network_up_callback_t cb : networkUpCallbacks_) + { + executor_.add(new CallbackExecutable([cb, ip_address] + { + cb(STATION_INTERFACE, ip_address); + })); + } + } + + configure_sntp(); + reconfigure_wifi_tx_power(); + if (statusLed_) + { + statusLed_->write(true); + } +} + +void Esp32WiFiManager::on_station_ip_lost() +{ + // Clear the flag that indicates we are connected and have an + // IPv4 address. + xEventGroupClearBits(wifiStatusEventGroup_, WIFI_GOTIP_BIT); + + // wake up the stack to process the event + wifiStackFlow_.notify(); + + // Schedule callbacks via the executor rather than call directly here. + { + OSMutexLock l(&networkCallbacksLock_); + for (esp_network_down_callback_t cb : networkDownCallbacks_) + { + executor_.add(new CallbackExecutable([cb] + { + cb(STATION_INTERFACE); + })); + } + } +} + +void Esp32WiFiManager::on_softap_start() +{ + uint32_t ip_address = 0; + uint8_t mac[6]; + esp_wifi_get_mac(WIFI_IF_AP, mac); + LOG(INFO, "[SoftAP] MAC Address:%s", mac_to_string(mac).c_str()); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) + // Set the generated hostname prior to connecting to the SSID + // so that it shows up with the generated hostname instead of + // the default "Espressif". + LOG(INFO, "[SoftAP] Setting hostname to \"%s\".", hostname_.c_str()); + ESP_ERROR_CHECK(esp_netif_set_hostname( + espNetIfaces_[SOFTAP_INTERFACE], hostname_.c_str())); + + // fetch the IP address from the adapter since it defaults to + // 192.168.4.1 but can be altered via sdkconfig. + esp_netif_ip_info_t ip_info; + ESP_ERROR_CHECK( + esp_netif_get_ip_info(espNetIfaces_[SOFTAP_INTERFACE], &ip_info)); + ip_address = ntohl(ip4_addr_get_u32(&ip_info.ip)); +#else + // Set the generated hostname prior to connecting to the SSID + // so that it shows up with the generated hostname instead of + // the default "Espressif". + LOG(INFO, "[SoftAP] Setting hostname to \"%s\".", hostname_.c_str()); + ESP_ERROR_CHECK(tcpip_adapter_set_hostname( + TCPIP_ADAPTER_IF_AP, hostname_.c_str())); + + // fetch the IP address from the adapter since it defaults to + // 192.168.4.1 but can be altered via sdkconfig. + tcpip_adapter_ip_info_t ip_info; + ESP_ERROR_CHECK(tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP + , &ip_info)); + ip_address = ntohl(ip4_addr_get_u32(&ip_info.ip)); +#endif // IDF v4.1+ + + LOG(INFO, "[SoftAP] IP address:%s", ipv4_to_string(ip_address).c_str()); + + // If we are operating only in SoftAP mode we can start any background + // services that would also be started when the station interface is ready + // and has an IP address. + if (wifiMode_ == WIFI_MODE_AP) + { + start_mdns_system(); + reconfigure_wifi_tx_power(); + if (connectionMode_ & CONN_MODE_HUB_BIT) + { + start_hub(); + } + } + + // Schedule callbacks via the executor rather than call directly here. + { + OSMutexLock l(&networkCallbacksLock_); + for (esp_network_up_callback_t cb : networkUpCallbacks_) + { + executor_.add(new CallbackExecutable([cb, ip_address] + { + cb(SOFTAP_INTERFACE, htonl(ip_address)); + })); + } + } +} + +void Esp32WiFiManager::on_softap_stop() +{ + if (wifiMode_ == WIFI_MODE_AP) + { + stop_uplink(); + stop_hub(); + } + + // Schedule callbacks via the executor rather than call directly here. + { + OSMutexLock l(&networkCallbacksLock_); + for (esp_network_down_callback_t cb : networkDownCallbacks_) + { + executor_.add(new CallbackExecutable([cb] + { + cb(SOFTAP_INTERFACE); + })); + } + } +} + +void Esp32WiFiManager::on_softap_station_connected(uint8_t mac[6], uint8_t aid) +{ + LOG(INFO, "[SoftAP aid:%d] %s connected.", aid, + mac_to_string(mac).c_str()); +} + +void Esp32WiFiManager::on_softap_station_disconnected(uint8_t mac[6], + uint8_t aid) +{ + LOG(INFO, "[SoftAP aid:%d] %s disconnected.", aid, + mac_to_string(mac).c_str()); +} + +void Esp32WiFiManager::on_wifi_scan_completed(uint32_t status, uint8_t count) +{ + OSMutexLock l(&ssidScanResultsLock_); + if (status) + { + LOG_ERROR("[WiFi] SSID scan failed!"); + } + else + { + uint16_t num_found = count; + esp_wifi_scan_get_ap_num(&num_found); + LOG(VERBOSE, "[WiFi] %d SSIDs found via scan", num_found); + ssidScanResults_.resize(num_found); + esp_wifi_scan_get_ap_records(&num_found, ssidScanResults_.data()); +#if LOGLEVEL >= VERBOSE + for (int i = 0; i < num_found; i++) + { + LOG(VERBOSE, "SSID:%s, RSSI:%d, channel:%d" + , ssidScanResults_[i].ssid + , ssidScanResults_[i].rssi, ssidScanResults_[i].primary); + } +#endif + } + if (ssidCompleteNotifiable_) + { + ssidCompleteNotifiable_->notify(); + ssidCompleteNotifiable_ = nullptr; + } +} + +// SNTP callback hook to schedule callbacks. +void Esp32WiFiManager::sync_time(time_t now) +{ + OSMutexLock l(&networkCallbacksLock_); + for (esp_network_time_callback_t cb : networkTimeCallbacks_) + { + executor_.add(new CallbackExecutable([cb,now] + { + cb(now); + })); + } +} + +static void sntp_update_received(struct timeval *tv) +{ + time_t new_time = tv->tv_sec; + LOG(INFO, "[SNTP] Received time update, new localtime:%s" + , ctime(&new_time)); + Singleton::instance()->sync_time(new_time); +} + +void Esp32WiFiManager::configure_sntp() +{ + if (sntpEnabled_ && !sntpConfigured_) + { + sntpConfigured_ = true; + LOG(INFO, "[SNTP] Polling %s for time updates", sntpServer_.c_str()); + sntp_setoperatingmode(SNTP_OPMODE_POLL); + // IDF v3.3 does not offer const correctness so we need to drop const + // when setting the hostname for SNTP. + sntp_setservername(0, const_cast(sntpServer_.c_str())); + sntp_set_time_sync_notification_cb(sntp_update_received); + sntp_init(); + + if (!timeZone_.empty()) + { + LOG(INFO, "[TimeZone] %s", timeZone_.c_str()); + setenv("TZ", timeZone_.c_str(), 1); + tzset(); + } + } +} + +void Esp32WiFiManager::reconfigure_wifi_radio_sleep() +{ + wifi_ps_type_t current_mode = WIFI_PS_NONE; + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_get_ps(¤t_mode)); + uint8_t sleepEnabled = CDI_READ_TRIMMED(cfg_.sleep, configFd_); + + if (sleepEnabled && current_mode != WIFI_PS_MIN_MODEM) + { + LOG(INFO, "[WiFi] Enabling radio power saving mode"); + // When sleep is enabled this will trigger the WiFi system to + // only wake up every DTIM period to receive beacon updates. + // no data loss is expected for this setting but it does delay + // receiption until the DTIM period. + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); + } + else if (!sleepEnabled && current_mode != WIFI_PS_NONE) + { + LOG(INFO, "[WiFi] Disabling radio power saving mode"); + // When sleep is disabled the WiFi radio will always be active. + // This will increase power consumption of the ESP32 but it + // will result in a more reliable behavior when the ESP32 is + // connected to an always-on power supply (ie: not a battery). + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); + } +} + +void Esp32WiFiManager::reconfigure_wifi_tx_power() +{ + ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(wifiTXPower_)); +} + +Esp32WiFiManager::WiFiStackFlow::WiFiStackFlow(Esp32WiFiManager * parent) + : StateFlowBase(parent), parent_(parent), + wifiConnectBitMask_(WIFI_CONNECTED_BIT) +{ + start_flow(STATE(startup)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::startup() +{ + if (parent_->wifiMode_ != WIFI_MODE_NULL) + { + return yield_and_call(STATE(init_interface)); + } + return yield_and_call(STATE(noop)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::noop() +{ + return wait_and_call(STATE(noop)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::init_interface() +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) + // create default interfaces for station and SoftAP, ethernet is not used + // today. + ESP_ERROR_CHECK(esp_netif_init()); + + // create the event loop. + esp_err_t err = esp_event_loop_create_default(); + + // The esp_event_loop_create_default() method will return either ESP_OK if + // the event loop was created or ESP_ERR_INVALID_STATE if one already + // exists. + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) + { + LOG(FATAL, "[WiFi] Failed to initialize the default event loop:%s", + esp_err_to_name(err)); + } + + parent_->espNetIfaces_[STATION_INTERFACE] = + esp_netif_create_default_wifi_sta(); + parent_->espNetIfaces_[SOFTAP_INTERFACE] = + esp_netif_create_default_wifi_ap(); + + // Connect our event listeners. + esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &Esp32WiFiManager::process_idf_event, nullptr); + esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, + &Esp32WiFiManager::process_idf_event, nullptr); +#else // NOT IDF v4.1+ + // Initialize the TCP/IP adapter stack. + LOG(INFO, "[WiFi] Starting TCP/IP stack"); + tcpip_adapter_init(); + + // Install event loop handler. + ESP_ERROR_CHECK( + esp_event_loop_init(&Esp32WiFiManager::process_wifi_event, nullptr)); +#endif // IDF v4.1+ + + return yield_and_call(STATE(init_wifi)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::init_wifi() +{ + // Start the WiFi adapter. + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + LOG(INFO, "[WiFi] Initializing WiFi stack"); + + // Disable NVS storage for the WiFi driver + cfg.nvs_enable = false; + + // override the defaults coming from arduino-esp32, the ones below improve + // throughput and stability of TCP/IP, for more info on these values, see: + // https://github.com/espressif/arduino-esp32/issues/2899 and + // https://github.com/espressif/arduino-esp32/pull/2912 + // + // Note: these numbers are slightly higher to allow compatibility with the + // WROVER chip and WROOM-32 chip. The increase results in ~2kb less heap + // at runtime. + // + // These do not require recompilation of arduino-esp32 code as these are + // used in the WIFI_INIT_CONFIG_DEFAULT macro, they simply need to be redefined. + if (cfg.static_rx_buf_num < 16) + { + cfg.static_rx_buf_num = 16; + } + if (cfg.dynamic_rx_buf_num < 32) + { + cfg.dynamic_rx_buf_num = 32; + } + if (cfg.rx_ba_win < 16) + { + cfg.rx_ba_win = 16; + } + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + if (parent_->verboseLogging_) + { + parent_->enable_esp_wifi_logging(); + } + + // Create the event group used for tracking connected/disconnected status. + // This is used internally regardless of if we manage the rest of the WiFi + // or mDNS systems. + parent_->wifiStatusEventGroup_ = xEventGroupCreate(); + + wifi_mode_t requested_wifi_mode = parent_->wifiMode_; + if (parent_->wifiMode_ == WIFI_MODE_AP) + { + // override the wifi mode from AP only to AP+STA so we can perform wifi + // scans on demand. + requested_wifi_mode = WIFI_MODE_APSTA; + } + // Set the requested WiFi mode. + ESP_ERROR_CHECK(esp_wifi_set_mode(requested_wifi_mode)); + + // This disables storage of SSID details in NVS which has been shown to be + // problematic at times for the ESP32, it is safer to always pass fresh + // config and have the ESP32 resolve the details at runtime rather than + // use a cached set from NVS. + esp_wifi_set_storage(WIFI_STORAGE_RAM); + + if (parent_->wifiMode_ == WIFI_MODE_APSTA || + parent_->wifiMode_ == WIFI_MODE_AP) + { + return yield_and_call(STATE(configure_softap)); + } + return yield_and_call(STATE(configure_station)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::configure_station() +{ + // Configure the SSID details for the station based on the SSID and + // password provided to the Esp32WiFiManager constructor. + wifi_config_t conf; + memset(&conf, 0, sizeof(wifi_config_t)); + strcpy(reinterpret_cast(conf.sta.ssid), parent_->ssid_.c_str()); + if (!parent_->password_.empty()) + { + strcpy(reinterpret_cast(conf.sta.password), + parent_->password_.c_str()); + } + + LOG(INFO, "[WiFi] Configuring Station (SSID:%s)", conf.sta.ssid); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &conf)); +#else + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &conf)); +#endif // IDF v4.0+ + + return yield_and_call(STATE(start_wifi)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::configure_softap() +{ + wifi_config_t conf; + memset(&conf, 0, sizeof(wifi_config_t)); + conf.ap.authmode = parent_->softAPAuthMode_; + conf.ap.beacon_interval = 100; + conf.ap.channel = parent_->softAPChannel_; + conf.ap.max_connection = 4; + + if (!parent_->softAPName_.empty()) + { + // Configure the SSID for the Soft AP based on the SSID passed + // to the Esp32WiFiManager constructor. + strcpy(reinterpret_cast(conf.ap.ssid), + parent_->softAPName_.c_str()); + } + else if (parent_->wifiMode_ == WIFI_MODE_AP && !parent_->ssid_.empty()) + { + strcpy(reinterpret_cast(conf.ap.ssid), + parent_->ssid_.c_str()); + } + else + { + // Configure the SSID for the Soft AP based on the generated + // hostname when operating in WIFI_MODE_APSTA mode. + strcpy(reinterpret_cast(conf.ap.ssid), + parent_->hostname_.c_str()); + } + + if (parent_->softAPAuthMode_ != WIFI_AUTH_OPEN) + { + if (!parent_->softAPPassword_.empty()) + { + strcpy(reinterpret_cast(conf.ap.password), + parent_->softAPPassword_.c_str()); + } + else if (!parent_->password_.empty()) + { + strcpy(reinterpret_cast(conf.ap.password), + parent_->password_.c_str()); + } + else + { + LOG(WARNING, + "[WiFi] SoftAP password is blank, using OPEN auth mode."); + parent_->softAPAuthMode_ = WIFI_AUTH_OPEN; + } + } + + LOG(INFO, "[WiFi] Configuring SoftAP (SSID:%s)", conf.ap.ssid); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,3,0) + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &conf)); +#else + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &conf)); +#endif // IDF v4.0+ + + // If we are only enabling the SoftAP we can transition to starting the + // WiFi stack. + if (parent_->wifiMode_ == WIFI_MODE_AP) + { + return yield_and_call(STATE(start_wifi)); + } + // Configure the station interface + return yield_and_call(STATE(configure_station)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::start_wifi() +{ + // Start the WiFi stack. This will start the SoftAP and/or connect to the + // SSID based on the configuration set above. + LOG(INFO, "[WiFi] Starting WiFi stack"); + ESP_ERROR_CHECK(esp_wifi_start()); + + // force WiFi power to maximum during startup. + ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(84)); + + if (parent_->wifiMode_ != WIFI_MODE_AP && parent_->waitForStationConnect_) + { + return sleep_and_call(&timer_, + SEC_TO_NSEC(WIFI_CONNECT_CHECK_INTERVAL_SEC), + STATE(wait_for_connect)); + } + return wait_and_call(STATE(reload)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::wait_for_connect() +{ + EventBits_t bits = + xEventGroupWaitBits(parent_->wifiStatusEventGroup_, + wifiConnectBitMask_, pdFALSE, pdTRUE, 0); + // If we need the STATION interface *AND* configured to wait until + // successfully connected to the SSID this code block will wait for up to + // approximately three minutes for an IP address to be assigned. In most + // cases this completes in under thirty seconds. If there is a connection + // failure the esp32 will be restarted via a FATAL error being logged. + if (++wifiConnectAttempts_ <= MAX_CONNECTION_CHECK_ATTEMPTS) + { + // If we have connected to the SSID we then are waiting for IP + // address. + if (bits & WIFI_CONNECTED_BIT) + { + LOG(INFO, "[IPv4] [%d/%d] Waiting for IP address assignment.", + wifiConnectAttempts_, MAX_CONNECTION_CHECK_ATTEMPTS); + } + else + { + // Waiting for SSID connection + LOG(INFO, "[WiFi] [%d/%d] Waiting for SSID connection.", + wifiConnectAttempts_, MAX_CONNECTION_CHECK_ATTEMPTS); + } + // Check if have connected to the SSID + if (bits & WIFI_CONNECTED_BIT) + { + // Since we have connected to the SSID we now need to track + // that we get an IP. + wifiConnectBitMask_ |= WIFI_GOTIP_BIT; + } + // Check if we have received an IP. + if (bits & WIFI_GOTIP_BIT) + { + return yield_and_call(STATE(reload)); + } + return sleep_and_call(&timer_, + SEC_TO_NSEC(WIFI_CONNECT_CHECK_INTERVAL_SEC), + STATE(wait_for_connect)); + } + // Check if we successfully connected or not. If not, force a reboot. + if ((bits & WIFI_CONNECTED_BIT) != WIFI_CONNECTED_BIT) + { + LOG(FATAL, "[WiFi] Failed to connect to SSID:%s.", + parent_->ssid_.c_str()); + } + + // Check if we successfully connected or not. If not, force a reboot. + if ((bits & WIFI_GOTIP_BIT) != WIFI_GOTIP_BIT) + { + LOG(FATAL, "[IPv4] Timeout waiting for an IP."); + } + + return yield_and_call(STATE(reload)); +} + +StateFlowBase::Action Esp32WiFiManager::WiFiStackFlow::reload() +{ + if (parent_->wifiMode_ == WIFI_MODE_STA || + parent_->wifiMode_ == WIFI_MODE_APSTA) + { + EventBits_t bits = xEventGroupGetBits(parent_->wifiStatusEventGroup_); + if (!(bits & WIFI_GOTIP_BIT)) + { + // Since we do not have an IP address we need to shutdown any + // active connections since they will be invalid until a new IP + // has been provisioned. + parent_->stop_hub(); + parent_->stop_uplink(); + return wait_and_call(STATE(reload)); + } + } + + parent_->reconfigure_wifi_radio_sleep(); + if (parent_->connectionMode_ & CONN_MODE_HUB_BIT) + { + parent_->start_hub(); + } + else + { + parent_->stop_hub(); + } + + if (parent_->connectionMode_ & CONN_MODE_UPLINK_BIT) + { + parent_->start_uplink(); + } + else + { + parent_->stop_uplink(); + } + return wait_and_call(STATE(reload)); +} + +} // namespace openmrn_arduino + +/// Maximum number of milliseconds to wait for mDNS query responses. +static constexpr uint32_t MDNS_QUERY_TIMEOUT = 2000; + +/// Maximum number of results to capture for mDNS query requests. +static constexpr size_t MDNS_MAX_RESULTS = 10; // Advertises an mDNS service name. void mdns_publish(const char *name, const char *service, uint16_t port) @@ -1274,7 +1624,7 @@ void mdns_publish(const char *name, const char *service, uint16_t port) } // Removes advertisement of an mDNS service name. -void mdns_unpublish(const char *service) +void mdns_unpublish(const char *name, const char *service) { Singleton::instance()->mdns_unpublish(service); } @@ -1294,7 +1644,6 @@ void split_mdns_service_name(string *service_name, string *protocol_name) } } - // EAI_AGAIN may not be defined on the ESP32 #ifndef EAI_AGAIN #ifdef TRY_AGAIN @@ -1354,7 +1703,7 @@ int mdns_lookup( { // failed to find any matches LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL, - "[mDNS] No matches found for service: %s.", + "[mDNS] No matches found for service:%s.", service); return EAI_AGAIN; } @@ -1373,7 +1722,7 @@ int mdns_lookup( if (ipaddr->addr.type == IPADDR_TYPE_V4) { LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL, - "[mDNS] Found %s as providing service: %s on port %d.", + "[mDNS] Found %s as providing service:%s on port %d.", res->hostname, service, res->port); inet_addr_from_ip4addr( &sa_in->sin_addr, &ipaddr->addr.u_addr.ip4); @@ -1391,8 +1740,7 @@ int mdns_lookup( if (!match_found) { LOG(ESP32_WIFIMGR_MDNS_LOOKUP_LOG_LEVEL, - "[mDNS] No matches found for service: %s.", - service); + "[mDNS] No matches found for service:%s.", service); return EAI_AGAIN; } @@ -1414,11 +1762,34 @@ int mdns_lookup( /// @return zero for success, -1 for failure. int getifaddrs(struct ifaddrs **ifap) { +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) + esp_netif_ip_info_t ip_info; +#else tcpip_adapter_ip_info_t ip_info; +#endif // IDF v5+ /* start with something "safe" in case we bail out early */ *ifap = nullptr; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5,0,0) + + // Lookup the interface by it's internal name assigned by ESP-IDF. + esp_netif_t *iface = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (iface == nullptr) + { + // Station interface was not found. + errno = ENODEV; + return -1; + } + + // retrieve TCP/IP address from the interface + if (esp_netif_get_ip_info(iface, &ip_info) != ESP_OK) + { + // Failed to retrieve IP address. + errno = EADDRNOTAVAIL; + return -1; + } +#else // IDF v4 (or lower) if (!tcpip_adapter_is_netif_up(TCPIP_ADAPTER_IF_STA)) { // Station TCP/IP interface is not up @@ -1426,6 +1797,10 @@ int getifaddrs(struct ifaddrs **ifap) return -1; } + // retrieve TCP/IP address from the interface + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info); +#endif // IDF v5+ + // allocate memory for various pieces of ifaddrs std::unique_ptr ia(new struct ifaddrs); if (ia.get() == nullptr) @@ -1449,9 +1824,6 @@ int getifaddrs(struct ifaddrs **ifap) } bzero(ifa_addr.get(), sizeof(struct sockaddr)); - // retrieve TCP/IP address from the interface - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip_info); - // copy address into ifaddrs structure struct sockaddr_in *addr_in = (struct sockaddr_in *)ifa_addr.get(); addr_in->sin_family = AF_INET; diff --git a/src/freertos_drivers/esp32/Esp32WiFiManager.hxx b/src/freertos_drivers/esp32/Esp32WiFiManager.hxx index 9ba82fe1e..a1996c8dd 100644 --- a/src/freertos_drivers/esp32/Esp32WiFiManager.hxx +++ b/src/freertos_drivers/esp32/Esp32WiFiManager.hxx @@ -36,41 +36,96 @@ #define _FREERTOS_DRIVERS_ESP32_ESP32WIFIMGR_HXX_ #include "freertos_drivers/esp32/Esp32WiFiConfiguration.hxx" +#include "executor/Executor.hxx" +#include "executor/Service.hxx" +#include "executor/StateFlow.hxx" #include "openlcb/ConfigRepresentation.hxx" #include "openlcb/ConfiguredTcpConnection.hxx" -#include "openlcb/SimpleStack.hxx" -#include "openlcb/TcpDefs.hxx" #include "utils/ConfigUpdateListener.hxx" #include "utils/GcTcpHub.hxx" -#include "utils/Singleton.hxx" -#include "utils/SocketClient.hxx" -#include "utils/SocketClientParams.hxx" #include "utils/macros.h" +#include "utils/Singleton.hxx" #include +#include #include #include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) +#include +#else +#include +#endif // IDF v4.1+ + +namespace openlcb +{ + class SimpleStackBase; +} + +class Gpio; +class SocketClient; + namespace openmrn_arduino { +/// ESP32 network interfaces. +/// +/// At this time only the Station and SoftAP interfaces are supported. There +/// are no plans to support the Ethernet interface at this time. +typedef enum : uint8_t +{ + /// This is used for the Station WiFi interface. + STATION_INTERFACE = 0, + + /// This is used for the SoftAP WiFi interface. + SOFTAP_INTERFACE, + + /// This is the maximum supported WiFi interfaces of the ESP32 and is not + /// a valid network interface for user code. + MAX_NETWORK_INTERFACES +} esp_network_interface_t; + +/// Callback function definition for the network up events. +/// +/// The first parameter is the interface that is up and read to use. +/// The second is the IP address for the interface in network byte order. +/// +/// NOTE: The callback will be invoked for SOFTAP_INTERFACE (SoftAP) upon start +/// and STATION_INTERFACE (station) only after the IP address has been received. +/// The callback will be called multiple times if both SoftAP and Station are +/// enabled. +typedef std::function esp_network_up_callback_t; + +/// Callback function definition for the network down events. +/// +/// The first parameter is the interface that is down. +/// +/// NOTE: The callback will be invoked for SOFTAP_INTERFACE (SoftAP) when the +/// interface is stopped, for STATION_INTERFACE (station) it will be called when +/// the IP address has been lost or connection to the AP has been lost. +typedef std::function esp_network_down_callback_t; + +/// Callback function definition for the network is initializing. +/// +/// The first parameter is the interface that is initializing. +/// +/// NOTE: This will be called for STATION_INTERFACE only. It will be called for +/// initial startup and reconnect events. +typedef std::function esp_network_init_callback_t; + +/// Callback function definition for network time synchronization. +/// +/// The parameter is the standard time_t structure containing the new time. +typedef std::function esp_network_time_callback_t; + /// This class provides a simple way for ESP32 nodes to manage the WiFi and /// mDNS systems of the ESP32, the node being a hub and connecting to an /// uplink node to participate in the CAN bus. -/// -/// There are two modes of operation for this class: -/// 1) Full management of WiFi, mDNS, hub and uplink. In this mode of operation -/// the Esp32WiFiManager will start the WiFi and mDNS systems automatically and -/// connect to the configured SSID as part of node initialization. If the node -/// is configured to be a hub it will be started automatically. The node's -/// uplink connection will also be managed internally. -/// 2) Management of only hub and uplink. In this mode of operation it is the -/// responsibility of the consumer to ensure that both the WiFi and mDNS -/// systems have been initialized and are running prior to calling -/// OpenMRN::begin() which will trigger the loading of the node configuration -/// which will trigger the management of the hub and uplink functionality. -class Esp32WiFiManager : public DefaultConfigUpdateListener - , public Singleton +class Esp32WiFiManager + : public DefaultConfigUpdateListener + , public Service + , public Singleton { public: /// Constructor. @@ -81,69 +136,69 @@ public: /// started after the initial loading of the CDI which occurs only after /// the application code calls OpenMRN::begin(). /// - /// @param ssid is the WiFi AP to connect to. Must stay alive forever. - /// @param password is the password for the WiFi AP being connected - /// to. Must stay alive forever. + /// @param station_ssid is the WiFi AP to connect to. + /// @param station_password is the password for the WiFi AP being connected + /// to. /// @param stack is the SimpleCanStackBase for this node. Must stay alive /// forever. /// @param cfg is the WiFiConfiguration instance used for this node. This /// will be monitored for changes and the WiFi behavior altered /// accordingly. + /// @param wifi_mode is the WiFi operating mode, defaults to WIFI_MODE_STA. + /// When set to WIFI_MODE_STA the Esp32WiFiManager will attempt to connect + /// to the configured WiFi station_ssid. When wifi_mode is set to + /// WIFI_MODE_AP the Esp32WiFiManager will create a SoftAP with the + /// configured softap_ssid, softap_password, softap_channel, and + /// softap_auth_mode. When set to WIFI_MODE_APSTA both the SoftAP and + /// STATION interfaces will be enabled. + /// @param connection_mode is used as the default value for the CDI element + /// of the same name which controls the uplink and hub operation. + /// @param sntp_server is the SNTP server to poll for time updates, this + /// defaults to pool.ntp.org. + /// @param timezone is the POSIX formatted TimeZone of the node, this + /// defaults to UTC0. + /// @param sntp_enabled Enables SNTP synchronization, defaults to false. /// @param hostname_prefix is the hostname prefix to use for this node. /// The @ref NodeID will be appended to this value. The maximum length for /// final hostname is 32 bytes. - /// @param wifi_mode is the WiFi operating mode. When set to WIFI_MODE_STA - /// the Esp32WiFiManager will attempt to connect to the provided WiFi SSID. - /// When the wifi_mode is WIFI_MODE_AP the Esp32WiFiManager will create an - /// AP with the provided SSID and PASSWORD. When the wifi_mode is - /// WIFI_MODE_APSTA the Esp32WiFiManager will connect to the provided WiFi - /// AP and create an AP with the SSID of "" and the provided - /// password. Note, the password for the AP will not be used if - /// soft_ap_auth is set to WIFI_AUTH_OPEN (default). - /// @param station_static_ip is the static IP configuration to use for the - /// Station WiFi connection. If not specified DHCP will be used instead. - /// @param primary_dns_server is the primary DNS server to use when a - /// static IP address is being used. If left as the default (ip_addr_any) - /// the Esp32WiFiManager will use 8.8.8.8 if using a static IP address. - /// @param soft_ap_channel is the WiFi channel to use for the SoftAP. - /// @param soft_ap_auth is the authentication mode for the AP when + /// @param softap_channel is the WiFi channel to use for the SoftAP. + /// @param softap_auth_mode is the authentication mode for the AP when /// wifi_mode is set to WIFI_MODE_AP or WIFI_MODE_APSTA. - /// @param soft_ap_password will be used as the password for the SoftAP, - /// if null and soft_ap_auth is not WIFI_AUTH_OPEN password will be used. - /// If provided, this must stay alive forever. - /// @param softap_static_ip is the static IP configuration for the SoftAP, - /// when not specified the SoftAP will have an IP address of 192.168.4.1. - /// - /// Note: Both ssid and password must remain in memory for the duration of - /// node uptime. - Esp32WiFiManager(const char *ssid - , const char *password - , openlcb::SimpleCanStackBase *stack + /// @param softap_ssid is the name for the SoftAP, if null the node + /// hostname will be used. + /// @param softap_password will be used as the password for the SoftAP, + /// if null and softap_auth_mode is not WIFI_AUTH_OPEN station_password + /// will be used. + Esp32WiFiManager(const char *station_ssid + , const char *station_password + , openlcb::SimpleStackBase *stack , const WiFiConfiguration &cfg - , const char *hostname_prefix = "esp32_" , wifi_mode_t wifi_mode = WIFI_MODE_STA - , tcpip_adapter_ip_info_t *station_static_ip = nullptr - , ip_addr_t primary_dns_server = ip_addr_any - , uint8_t soft_ap_channel = 1 - , wifi_auth_mode_t soft_ap_auth = WIFI_AUTH_OPEN - , const char *soft_ap_password = nullptr - , tcpip_adapter_ip_info_t *softap_static_ip = nullptr + , uint8_t connection_mode = CONN_MODE_UPLINK_BIT + , const char *hostname_prefix = "esp32_" + , const char *sntp_server = "pool.ntp.org" + , const char *timezone = "UTC0" + , bool sntp_enabled = false + , uint8_t softap_channel = 1 + , wifi_auth_mode_t softap_auth_mode = WIFI_AUTH_OPEN + , const char *softap_ssid = "" + , const char *softap_password = "" ); - /// Constructor. - /// - /// With this constructor the ESP32 WiFi and MDNS systems will not be - /// managed by the Esp32WiFiManager class, only the inbound and outbound - /// connections will be managed. This variation should only be used when - /// the application code starts the the WiFi and MDNS systems before - /// calling OpenMRN::begin(). + /// Destructor. + ~Esp32WiFiManager(); + + /// Display the configuration settings in use. + void display_configuration(); + + /// Configures a @ref Gpio to be used as a visual indication of the current + /// WiFi status. /// - /// @param stack is the SimpleCanStackBase for this node. - /// @param cfg is the WiFiConfiguration instance used for this node. This - /// will be monitored for changes and the WiFi behavior altered - /// accordingly. - Esp32WiFiManager( - openlcb::SimpleCanStackBase *stack, const WiFiConfiguration &cfg); + /// @param led is the @ref Gpio instance connected to the LED. + void set_status_led(const Gpio *led = nullptr) + { + statusLed_ = led; + } /// Updates the WiFiConfiguration settings used by this node. /// @@ -165,6 +220,18 @@ public: /// @param fd is the file descriptor used for the configuration settings. void factory_reset(int fd) override; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) + /// Processes an event coming from the ESP-IDF default event loop. + /// + /// @param ctx context parameter (unused). + /// @param event_base Determines the category of event being sent. + /// @param event_id Specific event from the event_base being sent. + /// @param event_data Data related to the event being sent, may be null. + /// + /// NOTE: This is not intended to be called by the user. + static void process_idf_event(void *ctx, esp_event_base_t event_base + , int32_t event_id, void *event_data); +#else /// Processes an ESP-IDF WiFi event based on the event raised by the /// ESP-IDF event loop processor. This should be used when the /// Esp32WiFiManager is not managing the WiFi or MDNS systems so that @@ -174,23 +241,20 @@ public: /// esp_event_loop. Note that ESP-IDF only supports one callback being /// registered. /// + /// @param ctx context parameter (unused). /// @param event is the system_event_t raised by ESP-IDF. - void process_wifi_event(system_event_t *event); - - /// Adds a callback to receive WiFi events as they are received/processed - /// by the Esp32WiFiManager. /// - /// @param callback is the callback to invoke when events are received, - /// the only parameter is the system_event_t that was received. - void add_event_callback(std::function callback) - { - OSMutexLock l(&eventCallbacksLock_); - eventCallbacks_.emplace_back(std::move(callback)); - } + /// NOTE: This is not intended to be called by the user. + static esp_err_t process_wifi_event(void *ctx, system_event_t *event); +#endif /// If called, sets the ESP32 wifi stack to log verbose information to the - /// ESP32 serial port. - void enable_verbose_logging(); + /// console. + void enable_verbose_logging() + { + verboseLogging_ = true; + enable_esp_wifi_logging(); + } /// Starts a scan for available SSIDs. /// @@ -223,19 +287,79 @@ public: /// @param service is the service name to remove from advertising. void mdns_unpublish(std::string service); -private: - /// Default constructor. - Esp32WiFiManager(); + /// Forces the Esp32WiFiManager to wait until SSID connection completes. + /// + /// @param enable when true will force the Esp32WiFiManager to wait for + /// successful SSID connection (including IP assignemnt), when false and + /// the Esp32WiFiManager will not check the SSID connection process. + /// + /// The default behavior is to wait for SSID connection to complete when + /// the WiFi mode is WIFI_MODE_STA or WIFI_MODE_APSTA. When operating in + /// WIFI_MODE_APSTA mode the application may opt to present a configuration + /// portal to allow reconfiguration of the SSID. + void wait_for_ssid_connect(bool enable) + { + waitForStationConnect_ = enable; + } - /// Starts the WiFi system and initiates the SSID connection process. + /// Configures the WiFi maximum transmit power setting. + /// + /// @param power is the maximum transmit power in 0.25dBm units, range is + /// 8-84 (2-20dBm). /// - /// Note: This is a blocking call and will reboot the node if the WiFi - /// connection is not successful after ~3min. - void start_wifi_system(); + /// NOTE: This should be called as early as possible, once the Station or + /// SoftAP has been started this setting will not be used. + void set_tx_power(uint8_t power) + { + HASSERT(power >= 8 && power <= 84); + wifiTXPower_ = power; + } + + /// Registers a callback for when the WiFi connection is up. + /// + /// @param callback The callback to invoke when the WiFi connection is + /// up. + void register_network_up_callback(esp_network_up_callback_t callback); - /// Starts the Esp32WiFiManager, this manages the WiFi subsystem as well as - /// all interactions with other nodes. - void start_wifi_task(); + /// Registers a callback for when the WiFi connection is down. + /// + /// @param callback The callback to invoke when the WiFi connection is + /// down. + void register_network_down_callback(esp_network_down_callback_t callback); + + /// Registers a callback for when WiFi interfaces are being initialized. + /// + /// @param callback The callback to invoke when the WiFi interface is + /// initializing. + /// + /// NOTE: this will not be invoked for ESP_IF_WIFI_AP since there are no + /// events raised between enabling the interface and when it is ready. + void register_network_init_callback(esp_network_init_callback_t callback); + + /// Registers a callback for when SNTP updates are received. + /// + /// @param callback The callback to invoke when SNTP updates are received. + void register_network_time_callback(esp_network_time_callback_t callback); + + /// Time synchronization callback for SNTP. + /// + /// @param now is the current time. + /// + /// NOTE: This is not intended to be called by the user. + void sync_time(time_t now); + + /// @return the Executor used by the Esp32WiFiManager. + /// + /// This can be used for other background tasks that should run + /// periodically but not from the main OpenMRN stack Executor. + Executor<1> *executor() + { + return &executor_; + } + +private: + /// Default constructor. + Esp32WiFiManager(); /// Background task used by the Esp32WiFiManager to maintain health of any /// connections to other nodes. @@ -272,53 +396,121 @@ private: /// Initializes the mDNS system if it hasn't already been initialized. void start_mdns_system(); - /// Handle for the wifi_manager_task that manages the WiFi stack, including - /// periodic health checks of the connected hubs or clients. - os_thread_t wifiTaskHandle_; + /// Event handler called when the ESP32 Station interface has started. + /// + /// This will handle configuration of any static IP address, hostname, DNS + /// and initiating the SSID connection process. + void on_station_started(); + + /// Event handler called when the ESP32 Station interface has connected to + /// an SSID. + void on_station_connected(); + + /// Event handler called when the ESP32 Station interface has lost it's + /// connection to the SSID or failed to connect. + /// + /// @param reason The reason for the disconnected event. + void on_station_disconnected(uint8_t reason); + + /// Event handler called when the ESP32 Station interface has received an + /// IP address (DHCP or static). + void on_station_ip_assigned(uint32_t ip_address); + + /// Event handler called when the ESP32 Station interface has lost it's + /// assigned IP address. + void on_station_ip_lost(); + + /// Event handler called when the ESP32 SoftAP interface has started. + /// + /// This will handle the configuration of the SoftAP Static IP (if used). + void on_softap_start(); + + /// Event handler called when the ESP32 SoftAP interface has shutdown. + void on_softap_stop(); + + /// Event handler called when a station connects to the ESP32 SoftAP. + /// + /// @param mac Station MAC address. + /// @param aid Station access point identifier. + void on_softap_station_connected(uint8_t mac[6], uint8_t aid); + + /// Event handler called when a station disconnects from the ESP32 SoftAP. + /// + /// @param mac Station MAC address. + /// @param aid Station access point identifier. + void on_softap_station_disconnected(uint8_t mac[6], uint8_t aid); + + /// Event handler called when a WiFi scan operation completes. + /// + /// @param status is the status of the WiFi scan request. + /// @param count is the number of access points found. + void on_wifi_scan_completed(uint32_t status, uint8_t count); + + /// Configures SNTP and TimeZone (if enabled). + void configure_sntp(); + + /// Reconfigures the WiFi radio sleep mode. + void reconfigure_wifi_radio_sleep(); + + /// Reconfigures the WiFi radio transmit power. + /// + /// NOTE: This will only be called after the station connection has been + /// established or after the SoftAP has been started. Before these events + /// the transmit power will be configured to the maximum value. + void reconfigure_wifi_tx_power(); /// Dynamically generated hostname for this node, esp32_{node-id}. This is /// also used for the SoftAP SSID name (if enabled). std::string hostname_; /// User provided SSID to connect to. - const char *ssid_; + std::string ssid_; /// User provided password for the SSID to connect to. - const char *password_; + std::string password_; /// Persistent configuration that will be used for this node's WiFi usage. const WiFiConfiguration cfg_; - /// This is internally used to enable the management of the WiFi stack, in - /// some environments this may be managed externally. - const bool manageWiFi_; - /// OpenMRN stack for the Arduino system. - openlcb::SimpleCanStackBase *stack_; - - /// WiFi operating mode. - wifi_mode_t wifiMode_{WIFI_MODE_STA}; + openlcb::SimpleStackBase *stack_; - /// Static IP Address configuration for the Station connection. - tcpip_adapter_ip_info_t *stationStaticIP_{nullptr}; + /// WiFi connection status indicator @ref Gpio instance. + const Gpio *statusLed_{nullptr}; - /// Primary DNS Address to use when configured for Static IP. - ip_addr_t primaryDNSAddress_{ip_addr_any}; + /// WiFi operating mode. + const wifi_mode_t wifiMode_; /// Channel to use for the SoftAP interface. - uint8_t softAPChannel_{1}; + const uint8_t softAPChannel_; /// Authentication mode to use for the SoftAP. If not set to WIFI_AUTH_OPEN /// @ref softAPPassword_ will be used. - wifi_auth_mode_t softAPAuthMode_{WIFI_AUTH_OPEN}; + wifi_auth_mode_t softAPAuthMode_; + + /// User provided name for the SoftAP when active, defaults to + /// @ref hostname_ when null. + /// NOTE: Only used when @ref wifiMode_ is set to WIFI_MODE_AP or + /// WIFI_MODE_APSTA. + std::string softAPName_; /// User provided password for the SoftAP when active, defaults to /// @ref password when null and softAPAuthMode_ is not WIFI_AUTH_OPEN. - const char *softAPPassword_; + /// NOTE: Only used when @ref wifiMode_ is set to WIFI_MODE_AP or + /// WIFI_MODE_APSTA. + std::string softAPPassword_; + + /// Enables SNTP polling. + const bool sntpEnabled_; - /// Static IP Address configuration for the SoftAP. - /// Default static IP provided by ESP-IDF is 192.168.4.1. - tcpip_adapter_ip_info_t *softAPStaticIP_{nullptr}; + /// SNTP server address. + std::string sntpServer_; + + /// TimeZone of the node. + std::string timeZone_; + + /// Tracks if SNTP has been configured. + bool sntpConfigured_{false}; /// Cached copy of the file descriptor passed into apply_configuration. /// This is internally used by the wifi_manager_task to processed deferred @@ -329,26 +521,35 @@ private: /// which may require the wifi_manager_task to reload config. uint32_t configCrc32_{0}; - /// Internal flag to request the wifi_manager_task reload configuration. - bool configReloadRequested_{true}; + /// If true, the esp32 will block startup until the SSID connection has + /// successfully completed and upon failure (or timeout) the esp32 will be + /// restarted. + bool waitForStationConnect_{true}; + + /// If true, request esp32 wifi to do verbose logging. + bool verboseLogging_{false}; - /// if true, request esp32 wifi to do verbose logging. - bool esp32VerboseLogging_{false}; + /// Defines the WiFi connection mode to operate in. + uint8_t connectionMode_{CONN_MODE_UPLINK_BIT}; - /// @ref GcTcpHub for this node's hub if enabled. - std::unique_ptr hub_; + /// Maximum WiFi transmit power setting. + uint8_t wifiTXPower_{84}; /// mDNS service name being advertised by the hub, if enabled. std::string hubServiceName_; - /// @ref SocketClient for this node's uplink. + /// @ref SocketClient for this node's uplink connection. std::unique_ptr uplink_; - /// Collection of registered WiFi event callback handlers. - std::vector> eventCallbacks_; + /// Socket handle used by the uplink connection. + int uplinkFd_{-1}; - /// Protects eventCallbacks_ vector. - OSMutex eventCallbacksLock_; + /// Notifiable handle provided by the @ref SocketClient once a connection + /// has been established. This will be called by @ref UplinkNotifiable when + /// not null and the connection needs to be re-established. This will be + /// set to null when the uplink is intentionally disconnected by + /// configuration updates. + Notifiable *uplinkNotifiable_{nullptr}; /// Internal event group used to track the IP assignment events. EventGroupHandle_t wifiStatusEventGroup_; @@ -368,21 +569,153 @@ private: /// Internal flag for tracking that the mDNS system has been initialized. bool mdnsInitialized_{false}; - /// True if we have started the connect executor thread. - bool connectExecutorStarted_{false}; - - /// Executor to use for the uplink connections. - Executor<1> connectExecutor_{NO_THREAD()}; + /// Executor to use for the uplink connections and callbacks. + Executor<1> executor_{NO_THREAD()}; /// Internal holder for mDNS entries which could not be published due to /// mDNS not being initialized yet. std::map mdnsDeferredPublish_; + /// Protects the networkUpCallbacks_, networkDownCallbacks_, + /// networkInitCallbacks_ and networkTimeCallbacks_ vectors. + OSMutex networkCallbacksLock_; + + /// Holder for callbacks to invoke when the WiFi connection is up. + std::vector networkUpCallbacks_; + + /// Holder for callbacks to invoke when the WiFi connection is down. + std::vector networkDownCallbacks_; + + /// Holder for callbacks to invoke when the WiFi subsystem has started. + std::vector networkInitCallbacks_; + + /// Holder for callbacks to invoke when network time synchronizes. + std::vector networkTimeCallbacks_; + + /// Maximum length of the hostname for the ESP32. + static constexpr uint8_t MAX_HOSTNAME_LENGTH = 32; + + /// Constant used to determine if the Uplink mode should be enabled. + static constexpr uint8_t CONN_MODE_UPLINK_BIT = BIT(0); + + /// Constant used to determine if the Hub mode should be enabled. + static constexpr uint8_t CONN_MODE_HUB_BIT = BIT(1); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,1,0) + /// Network interfaces that are managed by Esp32WiFiManager. + esp_netif_t *espNetIfaces_[MAX_NETWORK_INTERFACES] + { + nullptr, nullptr + }; +#endif // IDF v4.1+ + + /// This class provides a proxy for the @ref Notifiable provided by the + /// @ref SocketClient. This is necessary to ensure the @ref Notifiable does + /// not get invalidated when the @ref SocketClient is deleted prior to the + /// socket being closed. + class UplinkNotifiable : public Notifiable + { + public: + /// Constructor. + /// + /// @param parent @ref Esp32WiFiManager instance that this notifiable + /// will interact with for uplink disconnect events. + UplinkNotifiable(Esp32WiFiManager *parent) : parent_(parent) + { + } + + /// Proxies the notify() call to the uplink if it is not null. + virtual void notify() override + { + // if we have a valid notifiable from the uplink forward the notify + // call, otherwise this method is a no-op. + if (parent_->uplinkNotifiable_ != nullptr) + { + parent_->uplinkNotifiable_->notify(); + } + } + private: + /// @ref Esp32WiFiManager instance that manages this class instance. + Esp32WiFiManager *parent_; + }; + + /// @ref UplinkNotifiable to use for uplink connections. + UplinkNotifiable uplinkNotifiableProxy_{this}; + + /// StateFlow that is responsible for startup and maintenance of the WiFi + /// stack. + class WiFiStackFlow : public StateFlowBase + { + public: + /// Constructor. + /// + /// @param parent @ref Esp32WiFiManager instance that this flow should + /// maintain. + WiFiStackFlow(Esp32WiFiManager *parent); + private: + /// @ref StateFlowTimer used for periodic wakeup of + /// @ref wait_for_connect. + StateFlowTimer timer_{this}; + + /// @ref Esp32WiFiManager instance that is being maintained. + Esp32WiFiManager * parent_; + + /// Number of attempts to connect to the SSID before timing out. Only + /// applicable for the station interface via @ref wait_for_connect. + uint8_t wifiConnectAttempts_{0}; + + /// Bit mask used for checking WiFi connection process events in + /// @ref wait_for_connect. + uint32_t wifiConnectBitMask_; + + /// Initial state for this flow. + STATE_FLOW_STATE(startup); + + /// No-op state used when the WiFi system is not enabled and events are + /// received. + STATE_FLOW_STATE(noop); + + /// Initializes the ESP32 WiFi interfaces that are maintained by this + /// flow. + STATE_FLOW_STATE(init_interface); + + /// Initializes the ESP32 WiFi subsystems prior to configuration. + STATE_FLOW_STATE(init_wifi); + + /// Configures the ESP32 WiFi Station interface. + STATE_FLOW_STATE(configure_station); + + /// Configures the ESP32 WiFi SoftAP interface. + STATE_FLOW_STATE(configure_softap); + + /// Starts the ESP32 WiFi subsystem which will trigger the startup of + /// the SoftAP interface (if configured) and Station interface (if + /// configured). + STATE_FLOW_STATE(start_wifi); + + /// Re-entrant state that periodically checks if the Station interface + /// has successfully connected to the SSID and if it has received an IP + /// address. + STATE_FLOW_STATE(wait_for_connect); + + /// State which processes a configuration reload or the initial + /// configuration of the hub and uplink tasks (if either are enabled). + STATE_FLOW_STATE(reload); + }; + + /// Instance of @ref WiFiStackFlow used for WiFi maintenance. + WiFiStackFlow wifiStackFlow_{this}; + DISALLOW_COPY_AND_ASSIGN(Esp32WiFiManager); }; } // namespace openmrn_arduino using openmrn_arduino::Esp32WiFiManager; +using openmrn_arduino::esp_network_interface_t; +using openmrn_arduino::esp_network_up_callback_t; +using openmrn_arduino::esp_network_down_callback_t; +using openmrn_arduino::esp_network_init_callback_t; +using openmrn_arduino::esp_network_time_callback_t; #endif // _FREERTOS_DRIVERS_ESP32_ESP32WIFIMGR_HXX_ diff --git a/src/openlcb/ApplicationChecksum.hxx b/src/openlcb/ApplicationChecksum.hxx index 8b918dd4c..0512a966a 100644 --- a/src/openlcb/ApplicationChecksum.hxx +++ b/src/openlcb/ApplicationChecksum.hxx @@ -35,7 +35,11 @@ #ifndef _OPENLCB_APPLICATIONCHECKSUM_HXX_ #define _OPENLCB_APPLICATIONCHECKSUM_HXX_ +#ifdef ESP32 +#include "bootloader_hal.h" +#else #include "freertos/bootloader_hal.h" +#endif extern "C" { /** @returns true if the application checksum currently in flash is correct. */ diff --git a/src/openlcb/Bootloader.hxx b/src/openlcb/Bootloader.hxx index 1c06aef10..3f8aaff01 100644 --- a/src/openlcb/Bootloader.hxx +++ b/src/openlcb/Bootloader.hxx @@ -43,7 +43,11 @@ #include #include +#ifdef ESP32 +#include "bootloader_hal.h" +#else #include "freertos/bootloader_hal.h" +#endif #include "openlcb/Defs.hxx" #include "openlcb/CanDefs.hxx" #include "openlcb/DatagramDefs.hxx" diff --git a/src/openlcb/BroadcastTimeAlarm.hxx b/src/openlcb/BroadcastTimeAlarm.hxx index a3598dc58..66ef3c29f 100644 --- a/src/openlcb/BroadcastTimeAlarm.hxx +++ b/src/openlcb/BroadcastTimeAlarm.hxx @@ -235,7 +235,7 @@ private: } else if (clock_->is_running()) { - long long real_expires; + long long real_expires = 0; bool result = clock_->real_nsec_until_fast_time_abs(expires_, &real_expires); diff --git a/src/openlcb/BroadcastTimeServer.cxx b/src/openlcb/BroadcastTimeServer.cxx index 02fcb7bae..b732b523d 100644 --- a/src/openlcb/BroadcastTimeServer.cxx +++ b/src/openlcb/BroadcastTimeServer.cxx @@ -451,7 +451,7 @@ class BroadcastTimeServerSync expires -= tm->tm_sec ? (tm->tm_sec + 2) : 2; } - long long real_expires; + long long real_expires = 0; bool result = server_->real_nsec_until_fast_time_abs(expires, &real_expires); HASSERT(result); diff --git a/src/openlcb/EventHandlerContainer.hxx b/src/openlcb/EventHandlerContainer.hxx index 192ce259e..17f45433c 100644 --- a/src/openlcb/EventHandlerContainer.hxx +++ b/src/openlcb/EventHandlerContainer.hxx @@ -40,6 +40,7 @@ #include #include #include +#include #include #ifndef LOGLEVEL diff --git a/src/openlcb/MemoryConfig.cxx b/src/openlcb/MemoryConfig.cxx index cd4f0a843..d7b59c0ec 100644 --- a/src/openlcb/MemoryConfig.cxx +++ b/src/openlcb/MemoryConfig.cxx @@ -41,6 +41,8 @@ #include "openmrn_features.h" #include "utils/logging.h" #ifdef __FreeRTOS__ +#include "freertos/can_ioctl.h" +#elif defined(ESP32) #include "can_ioctl.h" #endif diff --git a/src/openlcb/MultiConfiguredPC.hxx b/src/openlcb/MultiConfiguredPC.hxx index f9dc3095a..707241209 100644 --- a/src/openlcb/MultiConfiguredPC.hxx +++ b/src/openlcb/MultiConfiguredPC.hxx @@ -71,8 +71,8 @@ CDI_GROUP_END(); CDI_GROUP(PCConfig); enum class ActionConfig : uint8_t { - OUTPUT = 0, - INPUT = 1 + DOUTPUT = 0, + DINPUT = 1 }; CDI_GROUP_ENTRY(action, Uint8ConfigEntry, Default(1), MapValues(PC_ACTION_MAP), @@ -223,7 +223,7 @@ public: EventRegistry::instance()->register_handler( EventRegistryEntry(this, cfg_event_on, i * 2 + 1), 0); uint8_t action = cfg_ref.action().read(fd); - if (action == (uint8_t)PCConfig::ActionConfig::OUTPUT) + if (action == (uint8_t)PCConfig::ActionConfig::DOUTPUT) { pins_[i]->set_direction(Gpio::Direction::DOUTPUT); producedEvents_[i * 2] = 0; diff --git a/src/openlcb/SNIPClient.hxx b/src/openlcb/SNIPClient.hxx index b2264d71e..8e917f07c 100644 --- a/src/openlcb/SNIPClient.hxx +++ b/src/openlcb/SNIPClient.hxx @@ -76,7 +76,7 @@ struct SNIPClientRequest : public CallableFlowRequestBase #if !defined(GTEST) || !defined(SNIP_CLIENT_TIMEOUT_NSEC) /// Specifies how long to wait for a SNIP request to get a response. Writable /// for unittesting purposes. -static constexpr long long SNIP_CLIENT_TIMEOUT_NSEC = MSEC_TO_NSEC(1500); +static constexpr long long SNIP_CLIENT_TIMEOUT_NSEC = MSEC_TO_NSEC(2000); #endif class SNIPClient : public CallableFlow diff --git a/src/openlcb/ServoConsumer.hxx b/src/openlcb/ServoConsumer.hxx index f2dc4860a..9783244c6 100644 --- a/src/openlcb/ServoConsumer.hxx +++ b/src/openlcb/ServoConsumer.hxx @@ -1,8 +1,13 @@ #ifndef _OPENLCB_SERVOCONSUMER_HXX_ #define _OPENLCB_SERVOCONSUMER_HXX_ +#if defined(ARDUINO) || defined(ESP32) +#include "freertos_drivers/arduino/DummyGPIO.hxx" +#include "freertos_drivers/arduino/PWM.hxx" +#else #include "freertos_drivers/common/DummyGPIO.hxx" #include "freertos_drivers/common/PWM.hxx" +#endif #include "openlcb/ServoConsumerConfig.hxx" #include "os/MmapGpio.hxx" #include diff --git a/src/openlcb/SimpleStack.hxx b/src/openlcb/SimpleStack.hxx index 53c8b2d54..c5e7281c8 100644 --- a/src/openlcb/SimpleStack.hxx +++ b/src/openlcb/SimpleStack.hxx @@ -39,6 +39,7 @@ #include "executor/Executor.hxx" #include "nmranet_config.h" +#include "openmrn_features.h" #include "openlcb/AliasAllocator.hxx" #include "openlcb/ConfigRepresentation.hxx" #include "openlcb/ConfigUpdateFlow.hxx" @@ -59,7 +60,7 @@ #include "utils/GridConnectHub.hxx" #include "utils/HubDevice.hxx" #include "utils/HubDeviceNonBlock.hxx" -#ifdef __FreeRTOS__ +#ifdef OPENMRN_FEATURE_FD_CAN_DEVICE #include "utils/HubDeviceSelect.hxx" #endif @@ -328,7 +329,7 @@ public: additionalComponents_.emplace_back(port); } -#ifdef __FreeRTOS__ +#ifdef OPENMRN_FEATURE_FD_CAN_DEVICE /// Adds a CAN bus port with asynchronous driver API. /// /// @deprecated: most current FreeRTOS drivers use the the select-based @@ -354,7 +355,7 @@ public: auto *port = new HubDeviceSelect(can_hub(), fd, on_error); additionalComponents_.emplace_back(port); } -#endif +#endif // OPENMRN_FEATURE_FD_CAN_DEVICE /// Adds a gridconnect port to the CAN bus. void add_gridconnect_port(const char *path, Notifiable *on_exit = nullptr); @@ -394,6 +395,12 @@ public: return gcHubServer_.get(); } + /// Shuts down the GridConnect Hub server, if previously started. + void shutdown_tcp_hub_server() + { + gcHubServer_.reset(nullptr); + } + /// Connects to a CAN hub using TCP with the gridconnect protocol. void connect_tcp_gridconnect_hub(const char *host, int port) { diff --git a/src/os/OSSelectWakeup.cxx b/src/os/OSSelectWakeup.cxx index 2fa97bf57..6d1fef562 100644 --- a/src/os/OSSelectWakeup.cxx +++ b/src/os/OSSelectWakeup.cxx @@ -82,13 +82,15 @@ int OSSelectWakeup::select(int nfds, fd_set *readfds, exceptfds = &newexcept; } FD_SET(vfsFd_, exceptfds); - if (vfsFd_ >= nfds) { + if (vfsFd_ >= nfds) + { nfds = vfsFd_ + 1; } #endif //ESP32 struct timeval timeout; - timeout.tv_sec = deadline_nsec / 1000000000; - timeout.tv_usec = (deadline_nsec / 1000) % 1000000; + // divide in two steps to avoid overflow on ESP32 + timeout.tv_sec = (deadline_nsec / 1000) / 1000000LL; + timeout.tv_usec = (deadline_nsec / 1000) % 1000000LL; int ret = ::select(nfds, readfds, writefds, exceptfds, &timeout); #elif !defined(OPENMRN_FEATURE_SINGLE_THREADED) @@ -106,41 +108,51 @@ int OSSelectWakeup::select(int nfds, fd_set *readfds, } #ifdef ESP32 -#include -#include -#include +#include "freertos_includes.h" +#include #include +#include +#include +#include /// Protects the initialization of vfs_id. static pthread_once_t vfs_init_once = PTHREAD_ONCE_INIT; + /// This per-thread key will store the OSSelectWakeup object that has been /// locked to any given calling thread. static pthread_key_t select_wakeup_key; -static int wakeup_fd; +/// This is the VFS FD that will be returned for ::open(). +/// +/// NOTE: The ESP VFS layer will ensure uniqueness and pass this FD back into +/// all VFS APIs we implement. +static constexpr int WAKEUP_VFS_FD = 0; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,0,0) extern "C" { void *sys_thread_sem_get(); void sys_sem_signal(void *); void sys_sem_signal_isr(void *); } +#endif // NOT IDF v4.0+ -static int esp_wakeup_open(const char * path, int flags, int mode) { - // This virtual FS has only one fd, 0. - return 0; -} - -static void esp_end_select() -{ - OSSelectWakeup *parent = - (OSSelectWakeup *)pthread_getspecific(select_wakeup_key); - HASSERT(parent); - parent->esp_end_select(); -} - -/// This function is called inline from the ESP32's select implementation. It is -/// passed in as a function pointer to the VFS API. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,0,0) +/// This function is called by the ESP32's select implementation. It is passed +/// in as a function pointer to the VFS API. +/// @param nfds see standard select API +/// @param readfds see standard select API +/// @param writefds see standard select API +/// @param exceptfds see standard select API +/// @param signal_sem is the semaphore object to trigger when the select should +/// wake up early. +/// @param end_select_args are the arguments to pass to end_select upon wakeup. +static esp_err_t esp_start_select(int nfds, fd_set *readfds, fd_set *writefds, + fd_set *exceptfds, esp_vfs_select_sem_t signal_sem, void **end_select_args) +#else // NOT IDF v4.0+ +/// This function is called by the ESP32's select implementation. It is passed +/// in as a function pointer to the VFS API. /// @param nfds see standard select API /// @param readfds see standard select API /// @param writefds see standard select API @@ -150,48 +162,108 @@ static void esp_end_select() /// semaphore. By the API contract this pointer needs to be passed into /// esp_vfs_select_triggered. static esp_err_t esp_start_select(int nfds, fd_set *readfds, fd_set *writefds, - fd_set *exceptfds, SemaphoreHandle_t *signal_sem) + fd_set *exceptfds, esp_vfs_select_sem_t signal_sem) +#endif // IDF v4.0+ +{ + OSSelectWakeup *parent = + (OSSelectWakeup *)pthread_getspecific(select_wakeup_key); + HASSERT(parent); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,0,0) + LOG(VERBOSE, "esp start select %p (thr %p parent %p)", signal_sem.sem, + os_thread_self(), parent); +#else // NOT IDF v4.0+ + LOG(VERBOSE, "esp start select %p (thr %p parent %p)", signal_sem, + os_thread_self(), parent); +#endif // IDF v4.0+ + + // Check if our VFS FD is included in exceptfds before tracking that we + // should possibly wake up early. + if (FD_ISSET(WAKEUP_VFS_FD, exceptfds)) + { + parent->esp_start_select(readfds, writefds, exceptfds, signal_sem); + } + return ESP_OK; +} + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,0,0) +/// This function is called inline from the ESP32's select implementation. It is +/// passed in as a function pointer to the VFS API. +/// +/// @param arg is the value that was provided as part of esp_start_select, this +/// is not used today. +/// @return always returns ESP_OK as this is a no-op. +static esp_err_t esp_end_select(void *arg) +#else // NOT IDF v4.0+ +/// This function is called inline from the ESP32's select implementation. It is +/// passed in as a function pointer to the VFS API. +static void esp_end_select() +#endif // IDF v4.0+ { OSSelectWakeup *parent = (OSSelectWakeup *)pthread_getspecific(select_wakeup_key); - LOG(VERBOSE, "esp start select %p (thr %p parent %p)", signal_sem, os_thread_self(), parent); HASSERT(parent); - parent->esp_start_select(signal_sem); + LOG(VERBOSE, "esp end select (thr %p parent %p)", os_thread_self(), + parent); + parent->esp_end_select(); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,0,0) return ESP_OK; +#endif // IDF v4.0+ } -void OSSelectWakeup::esp_start_select(void *signal_sem) +/// This function is called by the ESP32's select implementation. +/// @param signal_sem is the semaphore container provided by the VFS layer that +/// can be used to wake up the select() call early. +void OSSelectWakeup::esp_start_select(fd_set *readfds, fd_set *writefds, + fd_set *exceptfds, esp_vfs_select_sem_t signal_sem) { AtomicHolder h(this); espSem_ = signal_sem; - woken_ = false; + // store the fd_set for the except set since this is guaranteed to be set + // in OSSelectWakeup::select. Other fd_sets are not guaranteed or necessary + // to be stored/checked here. + exceptFds_ = exceptfds; + exceptFdsOrig_ = *exceptfds; + FD_ZERO(exceptFds_); } +/// This function marks the stored semaphore as invalid which indicates no +/// active select() call we are interested in. void OSSelectWakeup::esp_end_select() { AtomicHolder h(this); - woken_ = true; + // zero out the copy so we don't unintentionally wake up when the semaphore + // is no longer valid. + FD_ZERO(&exceptFdsOrig_); } +/// This function will trigger the ESP32 to wake up from any pending select() +/// call. void OSSelectWakeup::esp_wakeup() { - if (woken_) - { - return; - } AtomicHolder h(this); - if (woken_) + + // If our VFS FD is not set in the except fd_set we can exit early. + if (!FD_ISSET(WAKEUP_VFS_FD, &exceptFdsOrig_)) { return; } - woken_ = true; -#if 0 - esp_vfs_select_triggered((SemaphoreHandle_t *)espSem_); -#else - LOG(VERBOSE, "wakeup es %p %u lws %p", espSem_, *(unsigned*)espSem_, lwipSem_); + + // Mark the VFS implementation FD for the wakeup call. Note that this + // should not use vfsFd_ since the fd_set will contain the VFS specific FD + // and not the system global FD. + FD_SET(WAKEUP_VFS_FD, exceptFds_); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,0,0) + LOG(VERBOSE, "wakeup es %p %u", espSem_.sem, *(unsigned*)espSem_.sem); + esp_vfs_select_triggered(espSem_); +#else // IDF v3.x + LOG(VERBOSE, "wakeup es %p %u", espSem_, *(unsigned*)espSem_); if (espSem_) { - esp_vfs_select_triggered((SemaphoreHandle_t *)espSem_); + // Mark the VFS implementation FD for the wakeup call. Note that this + // should not use vfsFd_ since the fd_set will contain the VFS specific + // FD and not the system global FD. + esp_vfs_select_triggered(espSem_); } else { @@ -202,36 +274,33 @@ void OSSelectWakeup::esp_wakeup() // calling thread, not the target thread to wake up. sys_sem_signal(lwipSem_); } -#endif +#endif // IDF >= v4.0 } +/// This function will trigger the ESP32 to wake up from any pending select() +/// call from within an ISR context. void OSSelectWakeup::esp_wakeup_from_isr() { - if (woken_) - { - return; - } AtomicHolder h(this); - if (woken_) - { - return; - } - woken_ = true; BaseType_t woken = pdFALSE; -#if 0 - esp_vfs_select_triggered_isr((SemaphoreHandle_t *)espSem_, &woken); - if (woken == pdTRUE) + + // If our VFS FD is not set in the except fd_set we can exit early. + if (!FD_ISSET(WAKEUP_VFS_FD, &exceptFdsOrig_)) { - portYIELD_FROM_ISR(); + return; } -#else + + // Mark the VFS implementation FD for the wakeup call. Note that this + // should not use vfsFd_ since the fd_set will contain the VFS specific FD + // and not the system global FD. + FD_SET(WAKEUP_VFS_FD, exceptFds_); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4,0,0) + esp_vfs_select_triggered_isr(espSem_, &woken); +#else // IDF v3.x if (espSem_) { - esp_vfs_select_triggered_isr((SemaphoreHandle_t *)espSem_, &woken); - if (woken == pdTRUE) - { - portYIELD_FROM_ISR(); - } + esp_vfs_select_triggered_isr(espSem_, &woken); } else { @@ -242,7 +311,18 @@ void OSSelectWakeup::esp_wakeup_from_isr() // calling thread, not the target thread to wake up. sys_sem_signal_isr(lwipSem_); } -#endif +#endif // IDF >= v4.0 + + if (woken == pdTRUE) + { + portYIELD_FROM_ISR(); + } +} + +static int esp_wakeup_open(const char * path, int flags, int mode) +{ + // This virtual FS has only one fd, 0. + return WAKEUP_VFS_FD; } static void esp_vfs_init() @@ -250,27 +330,34 @@ static void esp_vfs_init() esp_vfs_t vfs; memset(&vfs, 0, sizeof(vfs)); vfs.flags = ESP_VFS_FLAG_DEFAULT; - vfs.start_select = &esp_start_select; - vfs.end_select = &esp_end_select; - vfs.open = &esp_wakeup_open; + vfs.start_select = esp_start_select; + vfs.end_select = esp_end_select; + vfs.open = esp_wakeup_open; ESP_ERROR_CHECK(esp_vfs_register("/dev/wakeup", &vfs, nullptr)); HASSERT(0 == pthread_key_create(&select_wakeup_key, nullptr)); - wakeup_fd = ::open("/dev/wakeup/0", 0, 0); - HASSERT(wakeup_fd >= 0); - LOG(VERBOSE, "VFSINIT wakeup fd %d", wakeup_fd); } void OSSelectWakeup::esp_allocate_vfs_fd() { +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,0,0) lwipSem_ = sys_thread_sem_get(); - pthread_once(&vfs_init_once, &esp_vfs_init); - vfsFd_ = wakeup_fd; - pthread_setspecific(select_wakeup_key, this); - LOG(VERBOSE, "VFSALLOC wakeup fd %d (thr %p test %p)", vfsFd_, os_thread_self(), pthread_getspecific(select_wakeup_key)); +#endif // IDF < v4.0 + + HASSERT(0 == pthread_once(&vfs_init_once, &esp_vfs_init)); + vfsFd_ = ::open("/dev/wakeup/0", 0, 0); + HASSERT(vfsFd_ >= 0); + HASSERT(0 == pthread_setspecific(select_wakeup_key, this)); + LOG(VERBOSE, "VFSALLOC wakeup fd %d (thr %p test %p)", vfsFd_, + os_thread_self(), pthread_getspecific(select_wakeup_key)); } void OSSelectWakeup::esp_deallocate_vfs_fd() { + if (vfsFd_ >= 0) + { + ::close(vfsFd_); + } + vfsFd_ = -1; } #endif // ESP32 diff --git a/src/os/OSSelectWakeup.hxx b/src/os/OSSelectWakeup.hxx index 5d284256e..a6c8aca87 100644 --- a/src/os/OSSelectWakeup.hxx +++ b/src/os/OSSelectWakeup.hxx @@ -54,6 +54,28 @@ #include #endif +#ifdef ESP32 +#include "sdkconfig.h" + +#ifdef CONFIG_VFS_SUPPORT_TERMIOS +// remove defines added by arduino-esp32 core/esp32/binary.h which are +// duplicated in sys/termios.h which may be included by esp_vfs.h +#undef B110 +#undef B1000000 +#endif // CONFIG_VFS_SUPPORT_TERMIOS + +#include +#include + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,0,0) +// SemaphoreHandle_t is defined by inclusion of esp_vfs.h so no additional +// includes are necessary. +/// Alias for the internal data type used by ESP-IDF select() calls. +typedef SemaphoreHandle_t * esp_vfs_select_sem_t; +#endif // IDF v3.x + +#endif // ESP32 + /// Signal handler that does nothing. @param sig ignored. void empty_signal_handler(int sig); @@ -190,19 +212,34 @@ private: void esp_wakeup(); void esp_wakeup_from_isr(); public: - void esp_start_select(void* signal_sem); + void esp_start_select(fd_set *readfds, fd_set *writefds, fd_set *exceptfds, + esp_vfs_select_sem_t signal_sem); void esp_end_select(); private: /// FD for waking up select in ESP32 VFS implementation. int vfsFd_{-1}; - /// Semaphore for waking up LWIP select. + + /// Semaphore provided by the ESP32 VFS layer to use for waking up the + /// ESP32 early from the select() call. + esp_vfs_select_sem_t espSem_; + + /// FD set provided by the ESP32 VFS layer to use when waking up early from + /// select, this tracks which FDs have an error (or exception). + fd_set *exceptFds_; + + /// Copy of the initial state of the except FD set provided by the ESP32 VFS + /// layer. This is used for checking if we need to set the bit for the FD + /// when waking up early from select(). + fd_set exceptFdsOrig_; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,0,0) + /// Semaphore for waking up LwIP select. void* lwipSem_{nullptr}; - /// Semaphore for waking up ESP32 select. - void* espSem_{nullptr}; - /// true if we have already woken up select. protected by Atomic *this. - bool woken_{true}; -#endif +#endif // IDF < v4.0 + +#endif // ESP32 + #if OPENMRN_HAVE_PSELECT /** This signal is used for the wakeup kill in a pthreads OS. */ static const int WAKEUP_SIG = SIGUSR1; diff --git a/src/os/stack_malloc.c b/src/os/stack_malloc.c index f68cab878..c667aa476 100644 --- a/src/os/stack_malloc.c +++ b/src/os/stack_malloc.c @@ -32,7 +32,9 @@ */ #include -#if defined(__FreeRTOS__) +#include "openmrn_features.h" + +#if OPENMRN_FEATURE_THREAD_FREERTOS const void *__attribute__((weak)) stack_malloc(unsigned long length); const void *stack_malloc(unsigned long length) diff --git a/src/utils/Atomic.hxx b/src/utils/Atomic.hxx index 143b0f794..05d0f71aa 100644 --- a/src/utils/Atomic.hxx +++ b/src/utils/Atomic.hxx @@ -95,34 +95,12 @@ public: /// Locks the specific critical section. void lock() { - // This should really use portENTER_CRITICAL_SAFE() but that is not - // available prior to ESP-IDF 3.3 which is not available in the - // arduino-esp32 environment generally. The below code is the - // implementation of that macro. - if (xPortInIsrContext()) - { - portENTER_CRITICAL_ISR(&mux); - } - else - { - portENTER_CRITICAL(&mux); - } + portENTER_CRITICAL_SAFE(&mux); } /// Unlocks the specific critical section. void unlock() { - // This should really use portEXIT_CRITICAL_SAFE() but that is not - // available prior to ESP-IDF 3.3 which is not available in the - // arduino-esp32 environment generally. The below code is the - // implementation of that macro. - if (xPortInIsrContext()) - { - portEXIT_CRITICAL_ISR(&mux); - } - else - { - portEXIT_CRITICAL(&mux); - } + portEXIT_CRITICAL_SAFE(&mux); } private: diff --git a/src/utils/AutoSyncFileFlow.hxx b/src/utils/AutoSyncFileFlow.hxx index 57623ed85..fe5b3ba06 100644 --- a/src/utils/AutoSyncFileFlow.hxx +++ b/src/utils/AutoSyncFileFlow.hxx @@ -37,6 +37,7 @@ #include "executor/Service.hxx" #include "executor/StateFlow.hxx" +#include "utils/StringPrintf.hxx" /// Simple state flow to configure automatic calls to fsync on a single file /// handle at regular intervals. diff --git a/src/utils/GcTcpHub.cxx b/src/utils/GcTcpHub.cxx index 99ec6e1fe..fe87501a0 100644 --- a/src/utils/GcTcpHub.cxx +++ b/src/utils/GcTcpHub.cxx @@ -61,7 +61,8 @@ void GcTcpHub::notify() GcTcpHub::GcTcpHub(CanHubFlow *can_hub, int port) : canHub_(can_hub) , tcpListener_(port, - std::bind(&GcTcpHub::on_new_connection, this, std::placeholders::_1)) + std::bind(&GcTcpHub::on_new_connection, this, std::placeholders::_1), + "GcTcpHub") { } diff --git a/src/utils/HubDeviceNonBlock.hxx b/src/utils/HubDeviceNonBlock.hxx index 747e78d50..56caf5203 100644 --- a/src/utils/HubDeviceNonBlock.hxx +++ b/src/utils/HubDeviceNonBlock.hxx @@ -34,18 +34,24 @@ #ifndef _UTILS_HUBDEVICENONBLOCK_HXX_ #define _UTILS_HUBDEVICENONBLOCK_HXX_ -// Nonblocking hubdevice only works on FreeRTOS. -#ifdef __FreeRTOS__ +#include "openmrn_features.h" +#ifdef OPENMRN_FEATURE_FD_CAN_DEVICE #include #include #include #include "executor/StateFlow.hxx" +#ifdef __FreeRTOS__ #include "freertos/can_ioctl.h" +#else +#include "can_ioctl.h" +#endif #include "utils/Hub.hxx" +#ifdef __FreeRTOS__ extern int ioctl(int fd, unsigned long int key, ...); +#endif // __FreeRTOS__ template class HubDeviceNonBlock : public Destructable, private Atomic, public Service { @@ -221,5 +227,5 @@ protected: WriteFlow writeFlow_; }; -#endif // __FreeRTOS__ +#endif // OPENMRN_FEATURE_FD_CAN_DEVICE #endif // _UTILS_HUBDEVICENONBLOCK_HXX_ diff --git a/src/utils/constants.hxx b/src/utils/constants.hxx index 0a2592ad3..19121567f 100644 --- a/src/utils/constants.hxx +++ b/src/utils/constants.hxx @@ -68,13 +68,15 @@ typedef unsigned char \ _do_not_add_declare_and_default_const_to_the_same_file_for_##name; -/// Defines the default value of a constant. Use this is a single .cxx file and +/// Defines the default value of a constant. Use this in a single .cxx file and /// make sure NOT to include the header that has the respective DECLARE_CONST /// macros. Best not to incude anything at all. /// /// @param name name of the constant. /// @param value is what the default value should be. -#define DEFAULT_CONST(name, value) \ +#define DEFAULT_CONST(name, value) DEFAULT_CONST_(name, value) + +#define DEFAULT_CONST_(name, value) \ EXTERNC extern const int __attribute__((__weak__)) _sym_##name = value; \ EXTERNCEND \ /** internal guard */ \ @@ -86,7 +88,9 @@ /// /// @param name name of the constant. /// @param value is what the actual value should be. -#define OVERRIDE_CONST(name, value) \ +#define OVERRIDE_CONST(name, value) OVERRIDE_CONST_(name, value) + +#define OVERRIDE_CONST_(name, value) \ EXTERNC extern const int _sym_##name; \ const int _sym_##name = value; \ EXTERNCEND @@ -102,14 +106,18 @@ return (ptrdiff_t)(&_sym_##name); \ } -#define DEFAULT_CONST(name, value) \ +#define DEFAULT_CONST(name, value) DEFAULT_CONST_(name, value) + +#define DEFAULT_CONST_(name, value) \ typedef signed char \ _do_not_add_declare_and_default_const_to_the_same_file_for_##name; \ asm(".global _sym_" #name " \n"); \ asm(".weak _sym_" #name " \n"); \ asm(".set _sym_" #name ", " #value " \n"); -#define OVERRIDE_CONST(name, value) \ +#define OVERRIDE_CONST(name, value) OVERRIDE_CONST_(name, value) + +#define OVERRIDE_CONST_(name, value) \ asm(".global _sym_" #name " \n"); \ asm(".set _sym_" #name ", " #value " \n"); diff --git a/src/utils/macros.h b/src/utils/macros.h index 142b7f9c5..ac425dc2d 100644 --- a/src/utils/macros.h +++ b/src/utils/macros.h @@ -89,7 +89,7 @@ extern const char* g_death_file; #define DIE(MSG) abort() -#elif defined(ESP_NONOS) || defined(ARDUINO) +#elif defined(ESP_NONOS) || defined(ARDUINO) || defined(ESP32) #include #include @@ -188,15 +188,15 @@ extern const char* g_death_file; #define C_STATIC_ASSERT(expr, name) \ typedef unsigned char __attribute__((unused)) __static_assert_##name[expr ? 0 : -1] -#if !defined(ESP_NONOS) && !defined(ESP32) +#if defined(ARDUINO_ARCH_ESP32) +#include +#elif !defined(ESP_NONOS) /// Declares (on the ESP8266) that the current function is not executed too /// often and should be placed in the SPI flash. #define ICACHE_FLASH_ATTR /// Declares (on the ESP8266) that the current function is executed /// often and should be placed in the instruction RAM. #define ICACHE_RAM_ATTR -#elif defined(ESP32) -#include #endif /// Retrieve a parent pointer from a member class variable. UNSAFE. diff --git a/src/utils/stdio_logging.h b/src/utils/stdio_logging.h index e168755ab..22db9acd2 100644 --- a/src/utils/stdio_logging.h +++ b/src/utils/stdio_logging.h @@ -6,7 +6,8 @@ extern "C" { #endif -#if defined(__linux__) || defined(__MACH__) || defined(__EMSCRIPTEN__) +#if defined(__linux__) || defined(__MACH__) || defined(__EMSCRIPTEN__) || \ + defined(ESP32) #define LOGWEAK __attribute__((weak)) #else #define LOGWEAK