Skip to content

Commit 5ad7367

Browse files
committed
fix(modem): Add support for ESP-AT based tcp-client example
1 parent c340f85 commit 5ad7367

File tree

3 files changed

+359
-0
lines changed

3 files changed

+359
-0
lines changed

components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ if (CONFIG_EXAMPLE_MODEM_DEVICE_BG96)
22
set(device_srcs sock_commands_bg96.cpp)
33
elseif(CONFIG_EXAMPLE_MODEM_DEVICE_SIM7600)
44
set(device_srcs sock_commands_sim7600.cpp)
5+
elseif(CONFIG_EXAMPLE_MODEM_DEVICE_ESPAT)
6+
set(device_srcs sock_commands_espat.cpp)
57
endif()
68

79
if(CONFIG_ESP_MODEM_ENABLE_DEVELOPMENT_MODE)

components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,26 @@ menu "Example Configuration"
1818
bool "SIM7600"
1919
help
2020
SIM7600 is Multi-Band LTE-TDD/LTE-FDD/HSPA+ and GSM/GPRS/EDGE module
21+
config EXAMPLE_MODEM_DEVICE_ESPAT
22+
bool "ESP-AT"
23+
help
24+
ESP-AT firmware for ESP32 modules with WiFi connectivity
2125
endchoice
2226

27+
if EXAMPLE_MODEM_DEVICE_ESPAT
28+
config EXAMPLE_WIFI_SSID
29+
string "WiFi SSID"
30+
default "myssid"
31+
help
32+
SSID (network name) to connect to.
33+
34+
config EXAMPLE_WIFI_PASSWORD
35+
string "WiFi Password"
36+
default "mypassword"
37+
help
38+
WiFi password (WPA or WPA2).
39+
endif
40+
2341
config EXAMPLE_MODEM_APN
2442
string "Set MODEM APN"
2543
default "internet"
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
#include <charconv>
8+
#include <cstring>
9+
#include "sock_commands.hpp"
10+
#include "cxx_include/esp_modem_command_library_utils.hpp"
11+
#include "sock_dce.hpp"
12+
13+
static const char *TAG = "sock_commands_espat";
14+
15+
namespace sock_commands {
16+
17+
using namespace esp_modem;
18+
19+
command_result net_open(CommandableIf *t)
20+
{
21+
ESP_LOGV(TAG, "%s", __func__);
22+
23+
// Set WiFi mode to station
24+
auto ret = dce_commands::generic_command(t, "AT+CWMODE=1\r", "OK", "ERROR", 5000);
25+
if (ret != command_result::OK) {
26+
ESP_LOGE(TAG, "Failed to set WiFi mode");
27+
return ret;
28+
}
29+
30+
// Connect to WiFi network
31+
std::string wifi_cmd = "AT+CWJAP=\"" CONFIG_EXAMPLE_WIFI_SSID "\",\"" CONFIG_EXAMPLE_WIFI_PASSWORD "\"\r";
32+
ret = dce_commands::generic_command(t, wifi_cmd, "OK", "ERROR", 15000);
33+
if (ret != command_result::OK) {
34+
ESP_LOGE(TAG, "Failed to connect to WiFi");
35+
return ret;
36+
}
37+
38+
ESP_LOGI(TAG, "WiFi connected successfully");
39+
return command_result::OK;
40+
}
41+
42+
command_result net_close(CommandableIf *t)
43+
{
44+
ESP_LOGV(TAG, "%s", __func__);
45+
// Disconnect from WiFi
46+
auto ret = dce_commands::generic_command(t, "AT+CWQAP\r", "OK", "ERROR", 5000);
47+
if (ret != command_result::OK) {
48+
ESP_LOGW(TAG, "Failed to disconnect WiFi (may already be disconnected)");
49+
}
50+
return command_result::OK;
51+
}
52+
53+
command_result tcp_open(CommandableIf *t, const std::string &host, int port, int timeout)
54+
{
55+
ESP_LOGV(TAG, "%s", __func__);
56+
57+
// Set single connection mode (just in case)
58+
auto ret = dce_commands::generic_command(t, "AT+CIPMUX=0\r", "OK", "ERROR", 1000);
59+
if (ret != command_result::OK) {
60+
ESP_LOGW(TAG, "Failed to set single connection mode");
61+
}
62+
63+
// Establish TCP connection
64+
std::string tcp_cmd = "AT+CIPSTART=\"TCP\",\"" + host + "\"," + std::to_string(port) + "\r";
65+
ret = dce_commands::generic_command(t, tcp_cmd, "CONNECT", "ERROR", timeout);
66+
if (ret != command_result::OK) {
67+
ESP_LOGE(TAG, "Failed to establish TCP connection to %s:%d", host.c_str(), port);
68+
return ret;
69+
}
70+
71+
ESP_LOGI(TAG, "TCP connection established to %s:%d", host.c_str(), port);
72+
return command_result::OK;
73+
}
74+
75+
command_result tcp_close(CommandableIf *t)
76+
{
77+
ESP_LOGV(TAG, "%s", __func__);
78+
return dce_commands::generic_command(t, "AT+CIPCLOSE\r", "CLOSED", "ERROR", 5000);
79+
}
80+
81+
command_result tcp_send(CommandableIf *t, uint8_t *data, size_t len)
82+
{
83+
ESP_LOGV(TAG, "%s", __func__);
84+
// This function is not used in the current implementation
85+
// Data sending is handled by the DCE responder
86+
return command_result::FAIL;
87+
}
88+
89+
command_result tcp_recv(CommandableIf *t, uint8_t *data, size_t len, size_t &out_len)
90+
{
91+
ESP_LOGV(TAG, "%s", __func__);
92+
// This function is not used in the current implementation
93+
// Data receiving is handled by the DCE responder
94+
return command_result::FAIL;
95+
}
96+
97+
command_result get_ip(CommandableIf *t, std::string &ip)
98+
{
99+
ESP_LOGV(TAG, "%s", __func__);
100+
std::string out;
101+
auto ret = dce_commands::generic_get_string(t, "AT+CIFSR\r", out, 5000);
102+
if (ret != command_result::OK) {
103+
return ret;
104+
}
105+
106+
// Parse station IP from response
107+
// Expected format: +CIFSR:STAIP,"192.168.1.100"
108+
auto pos = out.find("+CIFSR:STAIP,\"");
109+
if (pos != std::string::npos) {
110+
pos += 14; // Move past "+CIFSR:STAIP,\""
111+
auto end_pos = out.find("\"", pos);
112+
if (end_pos != std::string::npos) {
113+
ip = out.substr(pos, end_pos - pos);
114+
ESP_LOGI(TAG, "Got IP address: %s", ip.c_str());
115+
return command_result::OK;
116+
}
117+
}
118+
119+
ESP_LOGE(TAG, "Failed to parse IP address from response");
120+
return command_result::FAIL;
121+
}
122+
123+
command_result set_rx_mode(CommandableIf *t, int mode)
124+
{
125+
ESP_LOGV(TAG, "%s", __func__);
126+
// Set passive receive mode (1) for better control
127+
// Active mode (0) would send +IPD automatically
128+
std::string cmd = "AT+CIPRECVTYPE=" + std::to_string(mode) + "\r";
129+
return dce_commands::generic_command(t, cmd, "OK", "ERROR", 1000);
130+
}
131+
132+
} // sock_commands
133+
134+
namespace sock_dce {
135+
136+
void Responder::start_sending(size_t len)
137+
{
138+
data_to_send = len;
139+
send_stat = 0;
140+
send_cmd("AT+CIPSEND=" + std::to_string(len) + "\r");
141+
}
142+
143+
void Responder::start_receiving(size_t len)
144+
{
145+
send_cmd("AT+CIPRECVDATA=" + std::to_string(len) + "\r");
146+
}
147+
148+
bool Responder::start_connecting(std::string host, int port)
149+
{
150+
std::string cmd = "AT+CIPSTART=\"TCP\",\"" + host + "\"," + std::to_string(port) + "\r";
151+
send_cmd(cmd);
152+
return true;
153+
}
154+
155+
Responder::ret Responder::recv(uint8_t *data, size_t len)
156+
{
157+
const int MIN_MESSAGE = 6;
158+
size_t actual_len = 0;
159+
auto *recv_data = (char *)data;
160+
161+
if (data_to_recv == 0) {
162+
const std::string_view head = "+CIPRECVDATA:";
163+
164+
// Find the response header
165+
auto head_pos = std::search(recv_data, recv_data + len, head.data(), head.data() + head.size(), [](char a, char b) {
166+
return a == b;
167+
});
168+
169+
if (head_pos == recv_data + len) {
170+
return ret::FAIL;
171+
}
172+
173+
// Find the end of the length field
174+
auto next_comma = (char *)memchr(head_pos + head.size(), ',', MIN_MESSAGE);
175+
if (next_comma == nullptr) {
176+
return ret::FAIL;
177+
}
178+
179+
// Parse the actual length
180+
if (std::from_chars(head_pos + head.size(), next_comma, actual_len).ec == std::errc::invalid_argument) {
181+
ESP_LOGE(TAG, "Cannot convert length");
182+
return ret::FAIL;
183+
}
184+
185+
ESP_LOGD(TAG, "Received: actual len=%zu", actual_len);
186+
if (actual_len == 0) {
187+
ESP_LOGD(TAG, "No data received");
188+
return ret::FAIL;
189+
}
190+
191+
if (actual_len > buffer_size) {
192+
ESP_LOGE(TAG, "Data too large: %zu > %zu", actual_len, buffer_size);
193+
return ret::FAIL;
194+
}
195+
196+
// Move to the actual data after the comma
197+
recv_data = next_comma + 1;
198+
auto first_data_len = len - (recv_data - (char *)data);
199+
200+
if (actual_len > first_data_len) {
201+
on_read(recv_data, first_data_len);
202+
data_to_recv = actual_len - first_data_len;
203+
return ret::NEED_MORE_DATA;
204+
}
205+
on_read(recv_data, actual_len);
206+
207+
} else if (data_to_recv > len) { // Continue receiving
208+
on_read(recv_data, len);
209+
data_to_recv -= len;
210+
return ret::NEED_MORE_DATA;
211+
212+
} else if (data_to_recv <= len) { // Last chunk
213+
on_read(recv_data, data_to_recv);
214+
actual_len = data_to_recv;
215+
}
216+
217+
// Look for "OK" marker after the data
218+
char *ok_pos = nullptr;
219+
if (actual_len + 1 + 2 /* OK */ <= len) {
220+
ok_pos = (char *)memchr(recv_data + actual_len + 1, 'O', MIN_MESSAGE);
221+
if (ok_pos == nullptr || ok_pos[1] != 'K') {
222+
data_to_recv = 0;
223+
return ret::FAIL;
224+
}
225+
}
226+
227+
// Reset and prepare for next receive
228+
data_to_recv = 0;
229+
return ret::OK;
230+
}
231+
232+
Responder::ret Responder::send(uint8_t *data, size_t len)
233+
{
234+
if (send_stat < 3) {
235+
// Look for the '>' prompt
236+
if (memchr(data, '>', len) == NULL) {
237+
if (send_stat++ < 2) {
238+
return ret::NEED_MORE_DATA;
239+
}
240+
ESP_LOGE(TAG, "Missed '>' prompt");
241+
return ret::FAIL;
242+
}
243+
244+
// Send the actual data
245+
auto written = dte->write(&buffer[0], data_to_send);
246+
if (written != data_to_send) {
247+
ESP_LOGE(TAG, "Failed to write data: %d/%zu", written, data_to_send);
248+
return ret::FAIL;
249+
}
250+
data_to_send = 0;
251+
send_stat = 3;
252+
}
253+
return ret::IN_PROGRESS;
254+
}
255+
256+
Responder::ret Responder::send(std::string_view response)
257+
{
258+
if (send_stat == 3) {
259+
if (response.find("SEND OK") != std::string::npos) {
260+
send_stat = 0;
261+
return ret::OK;
262+
} else if (response.find("SEND FAIL") != std::string::npos) {
263+
ESP_LOGE(TAG, "Send failed");
264+
return ret::FAIL;
265+
} else if (response.find("ERROR") != std::string::npos) {
266+
ESP_LOGE(TAG, "Send error");
267+
return ret::FAIL;
268+
}
269+
}
270+
return ret::IN_PROGRESS;
271+
}
272+
273+
Responder::ret Responder::connect(std::string_view response)
274+
{
275+
if (response.find("CONNECT") != std::string::npos) {
276+
ESP_LOGI(TAG, "TCP connected!");
277+
return ret::OK;
278+
}
279+
if (response.find("ERROR") != std::string::npos) {
280+
ESP_LOGE(TAG, "Failed to connect");
281+
return ret::FAIL;
282+
}
283+
return ret::IN_PROGRESS;
284+
}
285+
286+
Responder::ret Responder::check_async_replies(status state, std::string_view &response)
287+
{
288+
ESP_LOGD(TAG, "Response: %.*s", static_cast<int>(response.size()), response.data());
289+
290+
// Handle WiFi status messages
291+
if (response.find("WIFI CONNECTED") != std::string::npos) {
292+
ESP_LOGI(TAG, "WiFi connected");
293+
} else if (response.find("WIFI DISCONNECTED") != std::string::npos) {
294+
ESP_LOGW(TAG, "WiFi disconnected");
295+
}
296+
297+
// Handle TCP status messages
298+
if (response.find("CONNECT") != std::string::npos && state == status::CONNECTING) {
299+
return connect(response);
300+
} else if (response.find("CLOSED") != std::string::npos) {
301+
ESP_LOGW(TAG, "TCP connection closed");
302+
return ret::FAIL;
303+
}
304+
305+
// Handle data notifications in active mode (if we switch to it later)
306+
if (response.find("+IPD,") != std::string::npos) {
307+
uint64_t data_ready = 1;
308+
write(data_ready_fd, &data_ready, sizeof(data_ready));
309+
ESP_LOGD(TAG, "Data available notification");
310+
}
311+
312+
if (state == status::SENDING) {
313+
return send(response);
314+
} else if (state == status::CONNECTING) {
315+
return connect(response);
316+
}
317+
318+
return ret::IN_PROGRESS;
319+
}
320+
321+
Responder::ret Responder::process_data(status state, uint8_t *data, size_t len)
322+
{
323+
if (state == status::SENDING) {
324+
return send(data, len);
325+
}
326+
if (state == status::RECEIVING) {
327+
return recv(data, len);
328+
}
329+
return ret::IN_PROGRESS;
330+
}
331+
332+
status Responder::pending()
333+
{
334+
// For ESP-AT, we don't need a pending check like BG96
335+
// Just return current status
336+
return status::SENDING;
337+
}
338+
339+
} // sock_dce

0 commit comments

Comments
 (0)