Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion components/esp_modem/include/cxx_include/esp_modem_api.hpp
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -40,6 +40,15 @@ using dte_config = ::esp_modem_dte_config;
*/
std::shared_ptr<DTE> 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<Terminal> create_uart_terminal(const dte_config *config);

/**
* @brief Create VFS DTE
* @param config DTE configuration
Expand Down
36 changes: 35 additions & 1 deletion components/esp_modem/include/cxx_include/esp_modem_dte.hpp
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include <memory>
#if defined(__cpp_lib_atomic_shared_ptr)
#include <atomic>
#endif
#include <utility>
#include <cstddef>
#include <cstdint>
#include <functional>
#include "cxx_include/esp_modem_primitives.hpp"
#include "cxx_include/esp_modem_terminal.hpp"
#include "cxx_include/esp_modem_types.hpp"
Expand Down Expand Up @@ -106,6 +110,23 @@ class DTE : public CommandableIf {
*/
void set_error_cb(std::function<void(terminal_error err)> f);

using transmit_hook_t = std::function<void()>;

/**
* @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
Expand Down Expand Up @@ -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<const TransmitHooks> load_transmit_hooks();

#ifdef CONFIG_ESP_MODEM_URC_HANDLER
/**
* @brief Buffer state tracking for enhanced URC processing
Expand Down Expand Up @@ -242,6 +271,11 @@ class DTE : public CommandableIf {
modem_mode mode; /*!< DTE operation mode */
std::function<bool(uint8_t *data, size_t len)> on_data; /*!< on data callback for current terminal */
std::function<void(terminal_error err)> user_error_cb; /*!< user callback on error event from attached terminals */
#if defined(__cpp_lib_atomic_shared_ptr)
std::atomic<std::shared_ptr<const TransmitHooks>> transmit_hooks_ {}; /*!< Immutable hook pair */
#else
std::shared_ptr<const TransmitHooks> transmit_hooks_; /*!< Immutable hook pair; use std::atomic_{load,store} */
#endif
Comment on lines +274 to +278

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@euripedesrocha Here's my workaround to get atomic load/store with shared_prt<> work across all IDF versions. (atomic_load/store_explicit vs. shared_ptr<>.load is either deprecated or not present)

Perhaps you have a better idea/suggestion how to address this?


#ifdef CONFIG_ESP_MODEM_USE_INFLATABLE_BUFFER_IF_NEEDED
/**
Expand Down
25 changes: 24 additions & 1 deletion components/esp_modem/include/esp_modem_c_api_types.h
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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);

/**
Expand Down
24 changes: 24 additions & 0 deletions components/esp_modem/src/esp_modem_c_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
61 changes: 57 additions & 4 deletions components/esp_modem/src/esp_modem_dte.cpp
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -339,20 +346,66 @@ int DTE::read(uint8_t **d, size_t len)
return actual_len;
}

std::shared_ptr<const DTE::TransmitHooks> 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<const TransmitHooks> p;
if (before_tx || after_tx) {
p = std::make_shared<TransmitHooks>(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
}
Comment thread
cursor[bot] marked this conversation as resolved.

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)
Expand Down
89 changes: 89 additions & 0 deletions components/esp_modem/test/host_test_app/main/test_app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

#include <memory>
#include <cstdlib>
#include <atomic>
#include <vector>
#include <unistd.h>
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_session.hpp>
Expand Down Expand Up @@ -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<int> before_count{0};
std::atomic<int> 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<std::string> 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<int> 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);
Expand Down
Loading