diff --git a/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt b/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt index 3788c86081..2594fe424d 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt +++ b/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt @@ -1,7 +1,11 @@ +set(module_dir "generic_module") if (CONFIG_EXAMPLE_MODEM_DEVICE_BG96) set(device_srcs sock_commands_bg96.cpp) elseif(CONFIG_EXAMPLE_MODEM_DEVICE_SIM7600) set(device_srcs sock_commands_sim7600.cpp) +elseif(CONFIG_EXAMPLE_MODEM_DEVICE_ESPAT) + set(device_srcs sock_commands_espat.cpp) + set(module_dir "espat_module") endif() if(CONFIG_ESP_MODEM_ENABLE_DEVELOPMENT_MODE) @@ -14,4 +18,4 @@ idf_component_register(SRCS "modem_client.cpp" "${command_dir}/sock_dce.cpp" "tcp_transport_at.cpp" "${device_srcs}" - INCLUDE_DIRS "." "${command_dir}") + INCLUDE_DIRS "." "${command_dir}" "${module_dir}") diff --git a/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild b/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild index 988cade41a..5911579f5c 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild +++ b/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild @@ -18,8 +18,26 @@ menu "Example Configuration" bool "SIM7600" help SIM7600 is Multi-Band LTE-TDD/LTE-FDD/HSPA+ and GSM/GPRS/EDGE module + config EXAMPLE_MODEM_DEVICE_ESPAT + bool "ESP-AT" + help + ESP-AT firmware for ESP32 modules with WiFi connectivity endchoice + if EXAMPLE_MODEM_DEVICE_ESPAT + config EXAMPLE_WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) to connect to. + + config EXAMPLE_WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2). + endif + config EXAMPLE_MODEM_APN string "Set MODEM APN" default "internet" diff --git a/components/esp_modem/examples/modem_tcp_client/main/command/sock_dce.hpp b/components/esp_modem/examples/modem_tcp_client/main/command/sock_dce.hpp index e73478d5f1..5add603f0f 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/command/sock_dce.hpp +++ b/components/esp_modem/examples/modem_tcp_client/main/command/sock_dce.hpp @@ -8,6 +8,7 @@ #include "cxx_include/esp_modem_api.hpp" #include #include "sock_commands.hpp" +#include "sock_module.hpp" #include #pragma once @@ -97,8 +98,8 @@ class Responder { std::shared_ptr &dte; }; -class DCE : public ::esp_modem::GenericModule { - using esp_modem::GenericModule::GenericModule; +class DCE : public Module { + using Module::Module; public: /** diff --git a/components/esp_modem/examples/modem_tcp_client/main/espat_module/sock_module.hpp b/components/esp_modem/examples/modem_tcp_client/main/espat_module/sock_module.hpp new file mode 100644 index 0000000000..f7184ea166 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/espat_module/sock_module.hpp @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_modem_config.h" +#include "cxx_include/esp_modem_api.hpp" + +#pragma once + +namespace sock_dce { + +class Module: public esp_modem::GenericModule { + using esp_modem::GenericModule::GenericModule; +public: + + esp_modem::command_result sync() override; + esp_modem::command_result set_echo(bool on) override; + esp_modem::command_result set_pdp_context(esp_modem::PdpContext &pdp) override; + +}; + +} diff --git a/components/esp_modem/examples/modem_tcp_client/main/generate/sock_dce.hpp b/components/esp_modem/examples/modem_tcp_client/main/generate/sock_dce.hpp index d64fceecf3..92e14013f6 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/generate/sock_dce.hpp +++ b/components/esp_modem/examples/modem_tcp_client/main/generate/sock_dce.hpp @@ -8,6 +8,7 @@ #include "cxx_include/esp_modem_api.hpp" #include #include "sock_commands.hpp" +#include "sock_module.hpp" #include #pragma once @@ -97,8 +98,8 @@ class Responder { std::shared_ptr &dte; }; -class DCE : public ::esp_modem::GenericModule { - using esp_modem::GenericModule::GenericModule; +class DCE : public Module { + using Module::Module; public: // --- ESP-MODEM command module starts here --- diff --git a/components/esp_modem/examples/modem_tcp_client/main/generic_module/sock_module.hpp b/components/esp_modem/examples/modem_tcp_client/main/generic_module/sock_module.hpp new file mode 100644 index 0000000000..6d198278af --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/generic_module/sock_module.hpp @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_modem_config.h" +#include "cxx_include/esp_modem_api.hpp" + +#pragma once + +namespace sock_dce { + +using Module = esp_modem::GenericModule; + +} diff --git a/components/esp_modem/examples/modem_tcp_client/main/modem_client.cpp b/components/esp_modem/examples/modem_tcp_client/main/modem_client.cpp index 558ba5316e..092ea16a25 100644 --- a/components/esp_modem/examples/modem_tcp_client/main/modem_client.cpp +++ b/components/esp_modem/examples/modem_tcp_client/main/modem_client.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -23,8 +23,8 @@ #include "tcp_transport_mbedtls.h" #include "tcp_transport_at.h" -#define BROKER_URL "mqtt.eclipseprojects.io" -#define BROKER_PORT 8883 +#define BROKER_URL "test.mosquitto.org" +#define BROKER_PORT 1883 static const char *TAG = "modem_client"; @@ -114,7 +114,7 @@ extern "C" void app_main(void) mqtt_config.broker.address.port = BROKER_PORT; mqtt_config.session.message_retransmit_timeout = 10000; #ifndef CONFIG_EXAMPLE_CUSTOM_TCP_TRANSPORT - mqtt_config.broker.address.uri = "mqtts://127.0.0.1"; + mqtt_config.broker.address.uri = "mqtt://127.0.0.1"; dce->start_listening(BROKER_PORT); #else mqtt_config.broker.address.uri = "mqtt://" BROKER_URL; @@ -124,14 +124,14 @@ extern "C" void app_main(void) mqtt_config.network.transport = ssl; #endif esp_mqtt_client_handle_t mqtt_client = esp_mqtt_client_init(&mqtt_config); - esp_mqtt_client_register_event(mqtt_client, static_cast(ESP_EVENT_ANY_ID), mqtt_event_handler, NULL); + esp_mqtt_client_register_event(mqtt_client, static_cast(ESP_EVENT_ANY_ID), mqtt_event_handler, nullptr); esp_mqtt_client_start(mqtt_client); #ifndef CONFIG_EXAMPLE_CUSTOM_TCP_TRANSPORT if (!dce->connect(BROKER_URL, BROKER_PORT)) { ESP_LOGE(TAG, "Failed to start DCE"); return; } - while (1) { + while (true) { while (dce->perform_sock()) { ESP_LOGV(TAG, "...performing"); } diff --git a/components/esp_modem/examples/modem_tcp_client/main/sock_commands_espat.cpp b/components/esp_modem/examples/modem_tcp_client/main/sock_commands_espat.cpp new file mode 100644 index 0000000000..a293a3f6f6 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/sock_commands_espat.cpp @@ -0,0 +1,366 @@ +/* + * SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "sock_commands.hpp" +#include "cxx_include/esp_modem_command_library_utils.hpp" +#include "sock_dce.hpp" +#include "sock_module.hpp" + +static const char *TAG = "sock_commands_espat"; + +namespace sock_dce { + +using namespace esp_modem; + +command_result Module::sync() +{ + return dce_commands::generic_command_common(dte.get(), "AT\r\n"); +} + +command_result Module::set_echo(bool on) +{ + return dce_commands::generic_command_common(dte.get(), "ATE0\r\n"); +} + +command_result Module::set_pdp_context(PdpContext &pdp) +{ + return command_result::OK; +} + +} + +namespace sock_commands { + +using namespace esp_modem; + +command_result net_open(CommandableIf *t) +{ + ESP_LOGV(TAG, "%s", __func__); + + // Set WiFi mode to station + auto ret = dce_commands::generic_command(t, "AT+CWMODE=1\r\n", "OK", "ERROR", 5000); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Failed to set WiFi mode"); + return ret; + } + + // Connect to WiFi network + std::string wifi_cmd = "AT+CWJAP=\"" CONFIG_EXAMPLE_WIFI_SSID "\",\"" CONFIG_EXAMPLE_WIFI_PASSWORD "\"\r\n"; + ret = dce_commands::generic_command(t, wifi_cmd, "OK", "ERROR", 15000); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Failed to connect to WiFi"); + return ret; + } + ESP_LOGI(TAG, "WiFi connected successfully"); + + // Set passive receive mode (1) for better control + ret = set_rx_mode(t, 1); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Failed to set preferred Rx mode"); + return ret; + } + return command_result::OK; +} + +command_result net_close(CommandableIf *t) +{ + ESP_LOGV(TAG, "%s", __func__); + // Disconnect from WiFi + auto ret = dce_commands::generic_command(t, "AT+CWQAP\r\n", "OK", "ERROR", 5000); + if (ret != command_result::OK) { + ESP_LOGW(TAG, "Failed to disconnect WiFi (may already be disconnected)"); + } + return command_result::OK; +} + +command_result tcp_open(CommandableIf *t, const std::string &host, int port, int timeout) +{ + ESP_LOGV(TAG, "%s", __func__); + + // Set single connection mode (just in case) + auto ret = dce_commands::generic_command(t, "AT+CIPMUX=0\r\n", "OK", "ERROR", 1000); + if (ret != command_result::OK) { + ESP_LOGW(TAG, "Failed to set single connection mode"); + } + + // Establish TCP connection + std::string tcp_cmd = "AT+CIPSTART=\"TCP\",\"" + host + "\"," + std::to_string(port) + "\r\n"; + ret = dce_commands::generic_command(t, tcp_cmd, "CONNECT", "ERROR", timeout); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Failed to establish TCP connection to %s:%d", host.c_str(), port); + return ret; + } + + ESP_LOGI(TAG, "TCP connection established to %s:%d", host.c_str(), port); + return command_result::OK; +} + +command_result tcp_close(CommandableIf *t) +{ + ESP_LOGV(TAG, "%s", __func__); + return dce_commands::generic_command(t, "AT+CIPCLOSE\r\n", "CLOSED", "ERROR", 5000); +} + +command_result tcp_send(CommandableIf *t, uint8_t *data, size_t len) +{ + ESP_LOGV(TAG, "%s", __func__); + // This function is not used in the current implementation + // Data sending is handled by the DCE responder + return command_result::FAIL; +} + +command_result tcp_recv(CommandableIf *t, uint8_t *data, size_t len, size_t &out_len) +{ + ESP_LOGV(TAG, "%s", __func__); + // This function is not used in the current implementation + // Data receiving is handled by the DCE responder + return command_result::FAIL; +} + +command_result get_ip(CommandableIf *t, std::string &ip) +{ + ESP_LOGV(TAG, "%s", __func__); + std::string out; + auto ret = dce_commands::at_raw(t, "AT+CIFSR\r\n", out, "OK", "ERROR", 5000); + if (ret != command_result::OK) { + return ret; + } + + // Parse station IP from response + // Expected format: +CIFSR:STAIP,"192.168.1.100" + auto pos = out.find("+CIFSR:STAIP,\""); + if (pos != std::string::npos) { + pos += 14; // Move past "+CIFSR:STAIP,\"" + auto end_pos = out.find("\"", pos); + if (end_pos != std::string::npos) { + ip = out.substr(pos, end_pos - pos); + ESP_LOGI(TAG, "Got IP address: %s", ip.c_str()); + return command_result::OK; + } + } + + ESP_LOGE(TAG, "Failed to parse IP address from response"); + return command_result::FAIL; +} + +command_result set_rx_mode(CommandableIf *t, int mode) +{ + ESP_LOGE(TAG, "%s", __func__); + // Active mode (0) sends data automatically, Passive mode (1) notifies about data for reading + std::string cmd = "AT+CIPRECVTYPE=" + std::to_string(mode) + "\r\n"; + return dce_commands::generic_command(t, cmd, "OK", "ERROR", 1000); +} + +} // sock_commands + +namespace sock_dce { + +void Responder::start_sending(size_t len) +{ + data_to_send = len; + send_stat = 0; + send_cmd("AT+CIPSEND=" + std::to_string(len) + "\r\n"); +} + +void Responder::start_receiving(size_t len) +{ + send_cmd("AT+CIPRECVDATA=" + std::to_string(len) + "\r\n"); +} + +bool Responder::start_connecting(std::string host, int port) +{ + std::string cmd = "AT+CIPSTART=\"TCP\",\"" + host + "\"," + std::to_string(port) + "\r\n"; + send_cmd(cmd); + return true; +} + +Responder::ret Responder::recv(uint8_t *data, size_t len) +{ + const int MIN_MESSAGE = 6; + size_t actual_len = 0; + auto *recv_data = (char *)data; + + if (data_to_recv == 0) { + const std::string_view head = "+CIPRECVDATA:"; + + // Find the response header + auto head_pos = std::search(recv_data, recv_data + len, head.data(), head.data() + head.size(), [](char a, char b) { + return a == b; + }); + + if (head_pos == recv_data + len) { + return ret::FAIL; + } + + // Find the end of the length field + auto next_comma = (char *)memchr(head_pos + head.size(), ',', MIN_MESSAGE); + if (next_comma == nullptr) { + return ret::FAIL; + } + + // Parse the actual length + if (std::from_chars(head_pos + head.size(), next_comma, actual_len).ec == std::errc::invalid_argument) { + ESP_LOGE(TAG, "Cannot convert length"); + return ret::FAIL; + } + + ESP_LOGD(TAG, "Received: actual len=%zu", actual_len); + if (actual_len == 0) { + ESP_LOGD(TAG, "No data received"); + return ret::FAIL; + } + + if (actual_len > buffer_size) { + ESP_LOGE(TAG, "Data too large: %zu > %zu", actual_len, buffer_size); + return ret::FAIL; + } + + // Move to the actual data after the comma + recv_data = next_comma + 1; + auto first_data_len = len - (recv_data - (char *)data); + + if (actual_len > first_data_len) { + on_read(recv_data, first_data_len); + data_to_recv = actual_len - first_data_len; + return ret::NEED_MORE_DATA; + } + on_read(recv_data, actual_len); + + } else if (data_to_recv > len) { // Continue receiving + on_read(recv_data, len); + data_to_recv -= len; + return ret::NEED_MORE_DATA; + + } else if (data_to_recv <= len) { // Last chunk + on_read(recv_data, data_to_recv); + actual_len = data_to_recv; + } + + // Look for "OK" marker after the data + char *ok_pos = nullptr; + if (actual_len + 1 + 2 /* OK */ <= len) { + ok_pos = (char *)memchr(recv_data + actual_len + 1, 'O', MIN_MESSAGE); + if (ok_pos == nullptr || ok_pos[1] != 'K') { + data_to_recv = 0; + return ret::FAIL; + } + } + + // Reset and prepare for next receive + data_to_recv = 0; + return ret::OK; +} + +Responder::ret Responder::send(uint8_t *data, size_t len) +{ + if (send_stat < 3) { + // Look for the '>' prompt + if (memchr(data, '>', len) == NULL) { + if (send_stat++ < 2) { + return ret::NEED_MORE_DATA; + } + ESP_LOGE(TAG, "Missed '>' prompt"); + return ret::FAIL; + } + + // Send the actual data + auto written = dte->write(&buffer[0], data_to_send); + if (written != data_to_send) { + ESP_LOGE(TAG, "Failed to write data: %d/%zu", written, data_to_send); + return ret::FAIL; + } + data_to_send = 0; + send_stat = 3; + } + return ret::IN_PROGRESS; +} + +Responder::ret Responder::send(std::string_view response) +{ + if (send_stat == 3) { + if (response.find("SEND OK") != std::string::npos) { + send_stat = 0; + return ret::OK; + } else if (response.find("SEND FAIL") != std::string::npos) { + ESP_LOGE(TAG, "Send failed"); + return ret::FAIL; + } else if (response.find("ERROR") != std::string::npos) { + ESP_LOGE(TAG, "Send error"); + return ret::FAIL; + } + } + return ret::IN_PROGRESS; +} + +Responder::ret Responder::connect(std::string_view response) +{ + if (response.find("CONNECT") != std::string::npos) { + ESP_LOGI(TAG, "TCP connected!"); + return ret::OK; + } + if (response.find("ERROR") != std::string::npos) { + ESP_LOGE(TAG, "Failed to connect"); + return ret::FAIL; + } + return ret::IN_PROGRESS; +} + +Responder::ret Responder::check_async_replies(status state, std::string_view &response) +{ + ESP_LOGD(TAG, "Response: %.*s", static_cast(response.size()), response.data()); + + // Handle WiFi status messages + if (response.find("WIFI CONNECTED") != std::string::npos) { + ESP_LOGI(TAG, "WiFi connected"); + } else if (response.find("WIFI DISCONNECTED") != std::string::npos) { + ESP_LOGW(TAG, "WiFi disconnected"); + } + + // Handle TCP status messages + if (response.find("CONNECT") != std::string::npos && state == status::CONNECTING) { + return connect(response); + } else if (response.find("CLOSED") != std::string::npos) { + ESP_LOGW(TAG, "TCP connection closed"); + return ret::FAIL; + } + + // Handle data notifications in active mode (if we switch to it later) + if (response.find("+IPD,") != std::string::npos) { + uint64_t data_ready = 1; + write(data_ready_fd, &data_ready, sizeof(data_ready)); + ESP_LOGD(TAG, "Data available notification"); + } + + if (state == status::SENDING) { + return send(response); + } else if (state == status::CONNECTING) { + return connect(response); + } + + return ret::IN_PROGRESS; +} + +Responder::ret Responder::process_data(status state, uint8_t *data, size_t len) +{ + if (state == status::SENDING) { + return send(data, len); + } + if (state == status::RECEIVING) { + return recv(data, len); + } + return ret::IN_PROGRESS; +} + +status Responder::pending() +{ + // For ESP-AT, we don't need a pending check like BG96 + // Just return current status + return status::SENDING; +} + +} // sock_dce diff --git a/components/esp_modem/examples/modem_tcp_client/sdkconfig.ci.default b/components/esp_modem/examples/modem_tcp_client/sdkconfig.ci.default new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/esp_modem/examples/modem_tcp_client/sdkconfig.ci.espat b/components/esp_modem/examples/modem_tcp_client/sdkconfig.ci.espat new file mode 100644 index 0000000000..1533b8934b --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/sdkconfig.ci.espat @@ -0,0 +1,2 @@ +CONFIG_IDF_TARGET="esp32" +CONFIG_EXAMPLE_MODEM_DEVICE_ESPAT=y