diff --git a/components/esp_modem/include/cxx_include/esp_modem_api.hpp b/components/esp_modem/include/cxx_include/esp_modem_api.hpp index 33a76ef017..d8b531999a 100644 --- a/components/esp_modem/include/cxx_include/esp_modem_api.hpp +++ b/components/esp_modem/include/cxx_include/esp_modem_api.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -40,6 +40,15 @@ using dte_config = ::esp_modem_dte_config; */ std::shared_ptr create_uart_dte(const dte_config *config); +/** + * @brief Create a UART terminal without wrapping it in a DTE + * + * Useful for building custom DTE subclasses that need a standard UART terminal. + * @param config DTE configuration (uses the uart_config member) + * @return unique ptr to Terminal on success, nullptr on failure + */ +std::unique_ptr create_uart_terminal(const dte_config *config); + /** * @brief Create VFS DTE * @param config DTE configuration diff --git a/components/esp_modem/include/cxx_include/esp_modem_dte.hpp b/components/esp_modem/include/cxx_include/esp_modem_dte.hpp index a78e486270..f2977ff837 100644 --- a/components/esp_modem/include/cxx_include/esp_modem_dte.hpp +++ b/components/esp_modem/include/cxx_include/esp_modem_dte.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -7,9 +7,13 @@ #pragma once #include +#if defined(__cpp_lib_atomic_shared_ptr) +#include +#endif #include #include #include +#include #include "cxx_include/esp_modem_primitives.hpp" #include "cxx_include/esp_modem_terminal.hpp" #include "cxx_include/esp_modem_types.hpp" @@ -106,6 +110,23 @@ class DTE : public CommandableIf { */ void set_error_cb(std::function f); + using transmit_hook_t = std::function; + + /** + * @brief Register hooks that fire around every DTE write operation. + * + * Useful for toggling a GPIO (DTR / sleep pin) before and after UART writes, + * covering both application AT commands and internal PPP traffic. + * + * @param before_tx Called just before bytes are sent to the terminal (nullptr to clear) + * @param after_tx Called just after bytes are sent to the terminal (nullptr to clear) + * + * @note Intended to be called during initialization before concurrent transmit (typical use). + * Internally hooks are stored as an immutable pair behind std::shared_ptr with atomic + * load/store so paired before/after stay consistent without a dedicated mutex. + */ + void set_transmit_hooks(transmit_hook_t before_tx, transmit_hook_t after_tx); + #ifdef CONFIG_ESP_MODEM_URC_HANDLER /** * @brief Allow setting a line callback for all incoming data @@ -207,6 +228,14 @@ class DTE : public CommandableIf { [[nodiscard]] bool exit_cmux(); /*!< Exit of CMUX mode and cleanup */ void exit_cmux_internal(); /*!< Cleanup CMUX */ + struct TransmitHooks { + transmit_hook_t before; + transmit_hook_t after; + }; + + /** Loads the current hook pair; nullptr if unset or cleared. */ + std::shared_ptr load_transmit_hooks(); + #ifdef CONFIG_ESP_MODEM_URC_HANDLER /** * @brief Buffer state tracking for enhanced URC processing @@ -242,6 +271,11 @@ class DTE : public CommandableIf { modem_mode mode; /*!< DTE operation mode */ std::function on_data; /*!< on data callback for current terminal */ std::function user_error_cb; /*!< user callback on error event from attached terminals */ +#if defined(__cpp_lib_atomic_shared_ptr) + std::atomic> transmit_hooks_ {}; /*!< Immutable hook pair */ +#else + std::shared_ptr transmit_hooks_; /*!< Immutable hook pair; use std::atomic_{load,store} */ +#endif #ifdef CONFIG_ESP_MODEM_USE_INFLATABLE_BUFFER_IF_NEEDED /** diff --git a/components/esp_modem/include/esp_modem_c_api_types.h b/components/esp_modem/include/esp_modem_c_api_types.h index c74076418a..02cefb6f09 100644 --- a/components/esp_modem/include/esp_modem_c_api_types.h +++ b/components/esp_modem/include/esp_modem_c_api_types.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -163,6 +163,29 @@ esp_err_t esp_modem_set_apn(esp_modem_dce_t *dce, const char *apn); esp_err_t esp_modem_set_urc(esp_modem_dce_t *dce, esp_err_t(*got_line_cb)(uint8_t *data, size_t len)); #endif +/** + * @brief Transmit hook callback type + * @param user_ctx Opaque context pointer registered with esp_modem_set_transmit_hooks() + */ +typedef void (*esp_modem_transmit_hook_t)(void *user_ctx); + +/** + * @brief Register hooks that fire around every DTE write operation + * + * Allows toggling a GPIO (DTR, sleep pin, etc.) before and after UART writes, + * covering both application-initiated AT commands and internal PPP traffic. + * + * @param dce Modem DCE handle + * @param before_tx Called just before bytes are sent (NULL to clear) + * @param after_tx Called just after bytes are sent (NULL to clear) + * @param user_ctx Opaque pointer forwarded to both hooks + * @return ESP_OK on success, ESP_ERR_INVALID_ARG if dce is invalid + */ +esp_err_t esp_modem_set_transmit_hooks(esp_modem_dce_t *dce, + esp_modem_transmit_hook_t before_tx, + esp_modem_transmit_hook_t after_tx, + void *user_ctx); + esp_err_t esp_modem_sqn_gm02s_connect(esp_modem_dce_t *dce, const esp_modem_PdpContext_t *pdp_context); /** diff --git a/components/esp_modem/src/esp_modem_c_api.cpp b/components/esp_modem/src/esp_modem_c_api.cpp index 869a17c6c1..0c9f4e7441 100644 --- a/components/esp_modem/src/esp_modem_c_api.cpp +++ b/components/esp_modem/src/esp_modem_c_api.cpp @@ -634,3 +634,27 @@ extern "C" esp_err_t esp_modem_hang_up(esp_modem_dce_t *dce_wrap) } return command_response_to_esp_err(dce_wrap->dce->hang_up()); } + +extern "C" esp_err_t esp_modem_set_transmit_hooks(esp_modem_dce_t *dce_wrap, + esp_modem_transmit_hook_t before_tx, + esp_modem_transmit_hook_t after_tx, + void *user_ctx) +{ + if (dce_wrap == nullptr || dce_wrap->dte == nullptr) { + return ESP_ERR_INVALID_ARG; + } + DTE::transmit_hook_t before_fn = nullptr; + DTE::transmit_hook_t after_fn = nullptr; + if (before_tx) { + before_fn = [before_tx, user_ctx]() { + before_tx(user_ctx); + }; + } + if (after_tx) { + after_fn = [after_tx, user_ctx]() { + after_tx(user_ctx); + }; + } + dce_wrap->dte->set_transmit_hooks(std::move(before_fn), std::move(after_fn)); + return ESP_OK; +} diff --git a/components/esp_modem/src/esp_modem_dte.cpp b/components/esp_modem/src/esp_modem_dte.cpp index 9c6fdd14e5..14d1eb83ee 100644 --- a/components/esp_modem/src/esp_modem_dte.cpp +++ b/components/esp_modem/src/esp_modem_dte.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -160,7 +160,14 @@ command_result DTE::command(const std::string &command, got_line_cb got_line, ui buffer_state.command_start_offset = buffer_state.total_processed; #endif command_cb.set(got_line, separator); + auto hooks = load_transmit_hooks(); + if (hooks && hooks->before) { + hooks->before(); + } primary_term->write((uint8_t *)command.c_str(), command.length()); + if (hooks && hooks->after) { + hooks->after(); + } command_cb.wait_for_line(time_ms); command_cb.set(nullptr); #ifdef CONFIG_ESP_MODEM_URC_HANDLER @@ -339,20 +346,66 @@ int DTE::read(uint8_t **d, size_t len) return actual_len; } +std::shared_ptr DTE::load_transmit_hooks() +{ +#if defined(__cpp_lib_atomic_shared_ptr) + return transmit_hooks_.load(std::memory_order_acquire); +#else + return std::atomic_load_explicit(&transmit_hooks_, std::memory_order_acquire); +#endif +} + +void DTE::set_transmit_hooks(transmit_hook_t before_tx, transmit_hook_t after_tx) +{ + std::shared_ptr p; + if (before_tx || after_tx) { + p = std::make_shared(std::move(before_tx), std::move(after_tx)); + } +#if defined(__cpp_lib_atomic_shared_ptr) + transmit_hooks_.store(std::move(p), std::memory_order_release); +#else + std::atomic_store_explicit(&transmit_hooks_, std::move(p), std::memory_order_release); +#endif +} + int DTE::write(uint8_t *data, size_t len) { - return secondary_term->write(data, len); + auto hooks = load_transmit_hooks(); + if (hooks && hooks->before) { + hooks->before(); + } + auto ret = secondary_term->write(data, len); + if (hooks && hooks->after) { + hooks->after(); + } + return ret; } int DTE::send(uint8_t *data, size_t len, int term_id) { Terminal *term = term_id == 0 ? primary_term.get() : secondary_term.get(); - return term->write(data, len); + auto hooks = load_transmit_hooks(); + if (hooks && hooks->before) { + hooks->before(); + } + auto ret = term->write(data, len); + if (hooks && hooks->after) { + hooks->after(); + } + return ret; } int DTE::write(DTE_Command command) { - return primary_term->write(command.data, command.len); + auto hooks = load_transmit_hooks(); + if (hooks && hooks->before) { + hooks->before(); + } + auto ret = primary_term->write(command.data, command.len); + if (hooks && hooks->after) { + hooks->after(); + } + return ret; } void DTE::on_read(got_line_cb on_read_cb) diff --git a/components/esp_modem/test/host_test_app/main/test_app.cpp b/components/esp_modem/test/host_test_app/main/test_app.cpp index 0cef5f552b..6b3f2cd176 100644 --- a/components/esp_modem/test/host_test_app/main/test_app.cpp +++ b/components/esp_modem/test/host_test_app/main/test_app.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include #include @@ -100,6 +102,93 @@ TEST_CASE("AT commands via socket", "[esp_modem][at]") } } +TEST_CASE("Transmit hooks", "[esp_modem][hooks]") +{ + auto dte = create_test_dte(); + REQUIRE(dte != nullptr); + + SECTION("hooks fire on AT command") { + std::atomic before_count{0}; + std::atomic after_count{0}; + + dte->set_transmit_hooks( + [&before_count]() { + before_count++; + }, + [&after_count]() { + after_count++; + } + ); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + int rssi, ber; + CHECK(dce->get_signal_quality(rssi, ber) == command_result::OK); + CHECK(before_count > 0); + CHECK(after_count > 0); + CHECK(before_count == after_count); + } + + SECTION("hooks maintain correct ordering") { + std::vector sequence; + + dte->set_transmit_hooks( + [&sequence]() { + sequence.push_back("before"); + }, + [&sequence]() { + sequence.push_back("after"); + } + ); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + std::string imei; + CHECK(dce->get_imei(imei) == command_result::OK); + + REQUIRE(sequence.size() >= 2); + REQUIRE(sequence.size() % 2 == 0); + for (size_t i = 0; i < sequence.size(); i += 2) { + CHECK(sequence[i] == "before"); + CHECK(sequence[i + 1] == "after"); + } + } + + SECTION("hooks can be cleared") { + std::atomic count{0}; + + dte->set_transmit_hooks( + [&count]() { + count++; + }, + [&count]() { + count++; + } + ); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("internet"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + REQUIRE(dce != nullptr); + + CHECK(dce->set_command_mode() == command_result::OK); + int count_after_first = count.load(); + CHECK(count_after_first > 0); + + dte->set_transmit_hooks(nullptr, nullptr); + + std::string imei; + CHECK(dce->get_imei(imei) == command_result::OK); + CHECK(count.load() == count_after_first); + } +} + int main(int argc, char *argv[]) { return Catch::Session().run(argc, argv);