Skip to content

Commit e376527

Browse files
committed
feat(modem): Add support for multiple connection in AT based example
1 parent 1ceb42c commit e376527

File tree

10 files changed

+407
-147
lines changed

10 files changed

+407
-147
lines changed

components/esp_modem/examples/modem_tcp_client/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,18 @@ To enable this mode, please set `EXAMPLE_CUSTOM_TCP_TRANSPORT=y`
2222
This configuration could be used with any network library, which is connecting to a localhost endpoint instead of remote one. This example creates a localhost listener which basically mimics the remote endpoint by forwarding the traffic between the library and the TCP/socket layer of the modem (which is already secure if the TLS is used in the network library)
2323

2424
![with localhost listener](at_client_localhost.png)
25+
26+
### Multi-connection support
27+
28+
This example supports opening multiple TCP connections concurrently when the modem firmware allows it.
29+
30+
- ESP-AT: Multi-connection mode is enabled via `AT+CIPMUX=1`. The example assigns a unique link ID per DCE instance and includes the link ID in `CIPSTART/CIPSEND/CIPRECVDATA` commands.
31+
- BG96/SIM7600: The example uses module-specific multi-connection syntax (for example `QIOPEN/CIPOPEN` with a connection ID) and tracks link IDs internally.
32+
33+
How it works:
34+
- The `sock_dce` layer creates multiple DCE instances over a shared DTE. A lightweight mutex coordinates access to the UART so only one DCE issues AT commands at a time.
35+
- Asynchronous URCs (for example `+IPD`, `+QIURC`, `+CIPRXGET: 1,<cid>`) wake the corresponding DCE which then performs receive operations for its link.
36+
37+
Usage:
38+
- `app_main` starts two DCE tasks to demonstrate concurrent connections. Adjust the number of DCE instances as needed.
39+
- For ESP-AT, ensure your firmware supports `CIPMUX=1` and passive receive (`CIPRECVTYPE`).

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ menu "Example Configuration"
4444
help
4545
Set APN (Access Point Name), a logical name to choose data network
4646

47+
config EXAMPLE_USE_TLS
48+
bool "Use TLS for MQTT broker"
49+
default n
50+
help
51+
Enable TLS for connection to the MQTT broker.
52+
53+
config EXAMPLE_BROKER_HOST
54+
string "MQTT broker host"
55+
default "test.mosquitto.org"
56+
help
57+
Hostname or IP address of the MQTT broker.
58+
4759
menu "UART Configuration"
4860
config EXAMPLE_MODEM_UART_TX_PIN
4961
int "TXD Pin Number"

components/esp_modem/examples/modem_tcp_client/main/command/sock_dce.cpp

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
#include <algorithm>
78
#include <charconv>
89
#include <sys/socket.h>
910
#include "esp_vfs.h"
@@ -14,6 +15,29 @@
1415
namespace sock_dce {
1516

1617
constexpr auto const *TAG = "sock_dce";
18+
constexpr auto WAIT_TO_IDLE_TIMEOUT = 5000;
19+
20+
// Definition of the static member variables
21+
std::vector<DCE *> DCE::dce_list{};
22+
bool DCE::network_init = false;
23+
int Responder::s_link_id = 0;
24+
SemaphoreHandle_t Responder::s_dte_mutex{};
25+
26+
// Constructor - add this DCE instance to the static list
27+
DCE::DCE(std::shared_ptr<esp_modem::DTE> dte_arg, const esp_modem_dce_config *config)
28+
: Module(std::move(dte_arg), config)
29+
{
30+
dce_list.push_back(this);
31+
}
32+
33+
// Destructor - remove this DCE instance from the static list
34+
DCE::~DCE()
35+
{
36+
auto it = std::find(dce_list.begin(), dce_list.end(), this);
37+
if (it != dce_list.end()) {
38+
dce_list.erase(it);
39+
}
40+
}
1741

1842

1943
bool DCE::perform_sock()
@@ -61,13 +85,26 @@ bool DCE::perform_sock()
6185

6286
void DCE::perform_at(uint8_t *data, size_t len)
6387
{
64-
ESP_LOG_BUFFER_HEXDUMP(TAG, data, len, ESP_LOG_VERBOSE);
88+
if (state != status::RECEIVING) {
89+
std::string_view resp_sv((char *)data, len);
90+
at.check_urc(state, resp_sv);
91+
if (state == status::IDLE) {
92+
return;
93+
}
94+
}
95+
// Trace incoming AT bytes when handling a response; use DEBUG level
96+
ESP_LOG_BUFFER_HEXDUMP(TAG, data, len, ESP_LOG_DEBUG);
6597
switch (at.process_data(state, data, len)) {
6698
case Responder::ret::OK:
99+
// Release DTE access for this link after processing data
100+
ESP_LOGD(TAG, "GIVE data %d", at.link_id);
101+
xSemaphoreGive(at.s_dte_mutex);
67102
state = status::IDLE;
68103
signal.set(IDLE);
69104
return;
70105
case Responder::ret::FAIL:
106+
ESP_LOGD(TAG, "GIVE data %d", at.link_id);
107+
xSemaphoreGive(at.s_dte_mutex);
71108
state = status::FAILED;
72109
signal.set(IDLE);
73110
return;
@@ -82,10 +119,14 @@ void DCE::perform_at(uint8_t *data, size_t len)
82119
std::string_view response((char *)data, len);
83120
switch (at.check_async_replies(state, response)) {
84121
case Responder::ret::OK:
122+
ESP_LOGD(TAG, "GIVE command %d", at.link_id);
123+
xSemaphoreGive(at.s_dte_mutex);
85124
state = status::IDLE;
86125
signal.set(IDLE);
87126
return;
88127
case Responder::ret::FAIL:
128+
ESP_LOGD(TAG, "GIVE command %d", at.link_id);
129+
xSemaphoreGive(at.s_dte_mutex);
89130
state = status::FAILED;
90131
signal.set(IDLE);
91132
return;
@@ -121,7 +162,7 @@ bool DCE::at_to_sock()
121162
uint64_t data;
122163
read(data_ready_fd, &data, sizeof(data));
123164
ESP_LOGD(TAG, "select read: modem data available %" PRIu64, data);
124-
if (!signal.wait(IDLE, 1000)) {
165+
if (!signal.wait(IDLE, WAIT_TO_IDLE_TIMEOUT)) {
125166
ESP_LOGE(TAG, "Failed to get idle");
126167
close_sock();
127168
return false;
@@ -131,6 +172,10 @@ bool DCE::at_to_sock()
131172
close_sock();
132173
return false;
133174
}
175+
// Take DTE mutex before issuing receive on this link
176+
ESP_LOGD(TAG, "TAKE RECV %d", at.link_id);
177+
xSemaphoreTake(at.s_dte_mutex, portMAX_DELAY);
178+
ESP_LOGD(TAG, "TAKEN RECV %d", at.link_id);
134179
state = status::RECEIVING;
135180
at.start_receiving(at.get_buf_len());
136181
return true;
@@ -139,7 +184,7 @@ bool DCE::at_to_sock()
139184
bool DCE::sock_to_at()
140185
{
141186
ESP_LOGD(TAG, "socket read: data available");
142-
if (!signal.wait(IDLE, 1000)) {
187+
if (!signal.wait(IDLE, WAIT_TO_IDLE_TIMEOUT)) {
143188
ESP_LOGE(TAG, "Failed to get idle");
144189
close_sock();
145190
return false;
@@ -149,6 +194,10 @@ bool DCE::sock_to_at()
149194
close_sock();
150195
return false;
151196
}
197+
// Take DTE mutex before issuing send on this link
198+
ESP_LOGD(TAG, "TAKE SEND %d", at.link_id);
199+
xSemaphoreTake(at.s_dte_mutex, portMAX_DELAY);
200+
ESP_LOGD(TAG, "TAKEN SEND %d", at.link_id);
152201
state = status::SENDING;
153202
int len = ::recv(sock, at.get_buf(), at.get_buf_len(), 0);
154203
if (len < 0) {
@@ -201,7 +250,7 @@ void DCE::start_listening(int port)
201250
}
202251
int opt = 1;
203252
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
204-
ESP_LOGI(TAG, "Socket created");
253+
ESP_LOGD(TAG, "Socket created");
205254
struct sockaddr_in addr = { };
206255
addr.sin_family = AF_INET;
207256
addr.sin_port = htons(port);
@@ -213,7 +262,7 @@ void DCE::start_listening(int port)
213262
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
214263
return;
215264
}
216-
ESP_LOGI(TAG, "Socket bound, port %d", 1883);
265+
ESP_LOGD(TAG, "Socket bound, port %d", 1883);
217266
err = listen(listen_sock, 1);
218267
if (err != 0) {
219268
ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
@@ -224,12 +273,12 @@ void DCE::start_listening(int port)
224273

225274
bool DCE::connect(std::string host, int port)
226275
{
227-
dte->on_read(nullptr);
228-
tcp_close();
229-
dte->on_read([this](uint8_t *data, size_t len) {
230-
this->perform_at(data, len);
231-
return esp_modem::command_result::TIMEOUT;
232-
});
276+
data_ready_fd = eventfd(0, EFD_SUPPORT_ISR);
277+
assert(data_ready_fd > 0);
278+
// Take DTE mutex before starting connect for this link
279+
ESP_LOGD(TAG, "TAKE CONNECT %d", at.link_id);
280+
xSemaphoreTake(at.s_dte_mutex, portMAX_DELAY);
281+
ESP_LOGD(TAG, "TAKEN CONNECT %d", at.link_id);
233282
if (!at.start_connecting(host, port)) {
234283
ESP_LOGE(TAG, "Unable to start connecting");
235284
dte->on_read(nullptr);
@@ -241,12 +290,15 @@ bool DCE::connect(std::string host, int port)
241290

242291
bool DCE::init()
243292
{
293+
if (network_init) {
294+
return true;
295+
}
296+
network_init = true;
297+
Responder::s_dte_mutex = xSemaphoreCreateBinary();
298+
xSemaphoreGive(at.s_dte_mutex);
244299
esp_vfs_eventfd_config_t config = ESP_VFS_EVENTD_CONFIG_DEFAULT();
245300
esp_vfs_eventfd_register(&config);
246301

247-
data_ready_fd = eventfd(0, EFD_SUPPORT_ISR);
248-
assert(data_ready_fd > 0);
249-
250302
dte->on_read(nullptr);
251303
const int retries = 5;
252304
int i = 0;
@@ -287,6 +339,10 @@ bool DCE::init()
287339
esp_modem::Task::Delay(5000);
288340
}
289341
ESP_LOGI(TAG, "Got IP %s", ip_addr.c_str());
342+
dte->on_read([](uint8_t *data, size_t len) {
343+
read_callback(data, len);
344+
return esp_modem::command_result::TIMEOUT;
345+
});
290346
return true;
291347
}
292348

components/esp_modem/examples/modem_tcp_client/main/command/sock_dce.hpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class Responder {
3434
sock(s), data_ready_fd(ready_fd), dte(dte_arg) {}
3535
ret process_data(status state, uint8_t *data, size_t len);
3636
ret check_async_replies(status state, std::string_view &response);
37+
ret check_urc(status state, std::string_view &response);
3738

3839
void start_sending(size_t len);
3940
void start_receiving(size_t len);
@@ -63,13 +64,19 @@ class Responder {
6364
return total_len;
6465
}
6566

67+
// Unique link identifier used to target multi-connection AT commands
68+
int link_id{s_link_id++};
69+
// Shared mutex guarding DTE access across concurrent DCE instances
70+
static SemaphoreHandle_t s_dte_mutex;
6671
private:
72+
static int s_link_id;
6773
static constexpr size_t buffer_size = 512;
6874

6975
bool on_read(char *data, size_t len)
7076
{
7177
#ifndef CONFIG_EXAMPLE_CUSTOM_TCP_TRANSPORT
7278
::send(sock, data, len, 0);
79+
printf("sending %d\n", len);
7380
#else
7481
::memcpy(&buffer[actual_read], data, len);
7582
actual_read += len;
@@ -101,6 +108,8 @@ class Responder {
101108
class DCE : public Module {
102109
using Module::Module;
103110
public:
111+
DCE(std::shared_ptr<esp_modem::DTE> dte_arg, const esp_modem_dce_config *config);
112+
~DCE();
104113

105114
/**
106115
* @brief Opens network in AT command mode
@@ -163,6 +172,10 @@ class DCE : public Module {
163172
return 0;
164173
}
165174
at.clear_offsets();
175+
// Take DTE mutex before issuing receive on this link
176+
ESP_LOGD("TAG", "TAKE RECV %d", at.link_id);
177+
xSemaphoreTake(at.s_dte_mutex, portMAX_DELAY);
178+
ESP_LOGD("TAG", "TAKEN RECV %d", at.link_id);
166179
state = status::RECEIVING;
167180
uint64_t data;
168181
read(data_ready_fd, &data, sizeof(data));
@@ -184,6 +197,10 @@ class DCE : public Module {
184197
if (!wait_to_idle(timeout_ms)) {
185198
return -1;
186199
}
200+
// Take DTE mutex before issuing send on this link
201+
ESP_LOGD("TAG", "TAKE SEND %d", at.link_id);
202+
xSemaphoreTake(at.s_dte_mutex, portMAX_DELAY);
203+
ESP_LOGD("TAG", "TAKEN SEND %d", at.link_id);
187204
state = status::SENDING;
188205
memcpy(at.get_buf(), buffer, len_to_send);
189206
ESP_LOG_BUFFER_HEXDUMP("dce", at.get_buf(), len, ESP_LOG_VERBOSE);
@@ -224,6 +241,14 @@ class DCE : public Module {
224241
}
225242
return -1;
226243
}
244+
static std::vector<DCE *> dce_list;
245+
static bool network_init;
246+
static void read_callback(uint8_t *data, size_t len)
247+
{
248+
for (auto dce : dce_list) {
249+
dce->perform_at(data, len);
250+
}
251+
}
227252
private:
228253
esp_modem::SignalGroup signal;
229254
void close_sock();

0 commit comments

Comments
 (0)