From 7547267336d7e2e7dc2002d8cb8f14a84a68c541 Mon Sep 17 00:00:00 2001 From: David Cermak Date: Wed, 5 Oct 2022 21:17:50 +0200 Subject: [PATCH] feat(modem): Add mqtt example in AT-only mode Similar to pppos-client, but without PPP mode. This uses standard mqtt client and a loopback service that forwards the TCP traffic from localhost to the modem and vice-versa. (next step is to create a dedicated tcp-transport layer for modem) Console example uses DCE commandable to demontrate how to handle URC --- .../examples/modem_console/CMakeLists.txt | 3 +- .../modem_console/main/CMakeLists.txt | 1 + .../modem_console/main/Kconfig.projbuild | 2 +- .../modem_console/main/modem_console_main.cpp | 35 +- .../modem_console/main/my_module_dce.cpp | 114 +++++ .../modem_console/main/my_module_dce.hpp | 85 ++- .../examples/modem_tcp_client/CMakeLists.txt | 9 + .../examples/modem_tcp_client/README.md | 10 + .../modem_tcp_client/main/CMakeLists.txt | 5 + .../modem_tcp_client/main/Kconfig.projbuild | 148 ++++++ .../modem_tcp_client/main/modem_client.cpp | 140 +++++ .../modem_tcp_client/main/sock_commands.cpp | 186 +++++++ .../modem_tcp_client/main/sock_commands.hpp | 25 + .../modem_tcp_client/main/sock_dce.cpp | 483 ++++++++++++++++++ .../modem_tcp_client/main/sock_dce.hpp | 102 ++++ .../modem_tcp_client/main/socket_commands.inc | 58 +++ .../simple_cmux_client/CMakeLists.txt | 3 +- .../esp_modem_command_declare_helper.inc | 26 + 18 files changed, 1420 insertions(+), 15 deletions(-) create mode 100644 components/esp_modem/examples/modem_console/main/my_module_dce.cpp create mode 100644 components/esp_modem/examples/modem_tcp_client/CMakeLists.txt create mode 100644 components/esp_modem/examples/modem_tcp_client/README.md create mode 100644 components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt create mode 100644 components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild create mode 100644 components/esp_modem/examples/modem_tcp_client/main/modem_client.cpp create mode 100644 components/esp_modem/examples/modem_tcp_client/main/sock_commands.cpp create mode 100644 components/esp_modem/examples/modem_tcp_client/main/sock_commands.hpp create mode 100644 components/esp_modem/examples/modem_tcp_client/main/sock_dce.cpp create mode 100644 components/esp_modem/examples/modem_tcp_client/main/sock_dce.hpp create mode 100644 components/esp_modem/examples/modem_tcp_client/main/socket_commands.inc create mode 100644 components/esp_modem/include/generate/esp_modem_command_declare_helper.inc diff --git a/components/esp_modem/examples/modem_console/CMakeLists.txt b/components/esp_modem/examples/modem_console/CMakeLists.txt index 12e4daf0a..eaf4959a8 100644 --- a/components/esp_modem/examples/modem_console/CMakeLists.txt +++ b/components/esp_modem/examples/modem_console/CMakeLists.txt @@ -1,6 +1,7 @@ # The following lines of boilerplate have to be in your project's CMakeLists # in this exact order for cmake to work correctly -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) +set(CMAKE_CXX_STANDARD 17) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(modem-console) diff --git a/components/esp_modem/examples/modem_console/main/CMakeLists.txt b/components/esp_modem/examples/modem_console/main/CMakeLists.txt index 9fe560cf0..e09da6eb3 100644 --- a/components/esp_modem/examples/modem_console/main/CMakeLists.txt +++ b/components/esp_modem/examples/modem_console/main/CMakeLists.txt @@ -1,5 +1,6 @@ idf_component_register(SRCS "modem_console_main.cpp" "console_helper.cpp" + "my_module_dce.cpp" "httpget_handle.c" "ping_handle.c" REQUIRES console esp_http_client nvs_flash diff --git a/components/esp_modem/examples/modem_console/main/Kconfig.projbuild b/components/esp_modem/examples/modem_console/main/Kconfig.projbuild index f568158c4..3eb51e488 100644 --- a/components/esp_modem/examples/modem_console/main/Kconfig.projbuild +++ b/components/esp_modem/examples/modem_console/main/Kconfig.projbuild @@ -17,7 +17,7 @@ menu "Example Configuration" choice EXAMPLE_MODEM_DEVICE prompt "Choose supported modem device (DCE)" - default EXAMPLE_MODEM_DEVICE_BG96 + default EXAMPLE_MODEM_DEVICE_SHINY help Select modem device connected to the ESP DTE. config EXAMPLE_MODEM_DEVICE_SHINY diff --git a/components/esp_modem/examples/modem_console/main/modem_console_main.cpp b/components/esp_modem/examples/modem_console/main/modem_console_main.cpp index a6ff09635..2f8052e0e 100644 --- a/components/esp_modem/examples/modem_console/main/modem_console_main.cpp +++ b/components/esp_modem/examples/modem_console/main/modem_console_main.cpp @@ -89,6 +89,14 @@ void wakeup_modem(void) vTaskDelay(pdMS_TO_TICKS(2000)); } +#ifdef CONFIG_EXAMPLE_MODEM_DEVICE_SHINY +command_result handle_urc(uint8_t *data, size_t len) +{ + ESP_LOG_BUFFER_HEXDUMP("on_read", data, len, ESP_LOG_INFO); + return command_result::TIMEOUT; +} +#endif + extern "C" void app_main(void) { static RTC_RODATA_ATTR char apn_rtc[20] = DEFAULT_APN; @@ -122,19 +130,19 @@ extern "C" void app_main(void) dte_config.dte_buffer_size = CONFIG_EXAMPLE_MODEM_UART_RX_BUFFER_SIZE / 2; auto uart_dte = create_uart_dte(&dte_config); -#if CONFIG_EXAMPLE_MODEM_DEVICE_SHINY == 1 +#if defined(CONFIG_EXAMPLE_MODEM_DEVICE_SHINY) ESP_LOGI(TAG, "Initializing esp_modem for the SHINY module..."); auto dce = create_shiny_dce(&dce_config, uart_dte, esp_netif); -#elif CONFIG_EXAMPLE_MODEM_DEVICE_BG96 == 1 +#elif defined(CONFIG_EXAMPLE_MODEM_DEVICE_BG96) ESP_LOGI(TAG, "Initializing esp_modem for the BG96 module..."); auto dce = create_BG96_dce(&dce_config, uart_dte, esp_netif); -#elif CONFIG_EXAMPLE_MODEM_DEVICE_SIM800 == 1 +#elif defined(CONFIG_EXAMPLE_MODEM_DEVICE_SIM800) ESP_LOGI(TAG, "Initializing esp_modem for the SIM800 module..."); auto dce = create_SIM800_dce(&dce_config, uart_dte, esp_netif); -#elif CONFIG_EXAMPLE_MODEM_DEVICE_SIM7000 == 1 +#elif defined(CONFIG_EXAMPLE_MODEM_DEVICE_SIM7000) ESP_LOGI(TAG, "Initializing esp_modem for the SIM7000 module..."); auto dce = create_SIM7000_dce(&dce_config, uart_dte, esp_netif); -#elif CONFIG_EXAMPLE_MODEM_DEVICE_SIM7070 == 1 +#elif defined(CONFIG_EXAMPLE_MODEM_DEVICE_SIM7070) ESP_LOGI(TAG, "Initializing esp_modem for the SIM7070 module..."); auto dce = create_SIM7070_dce(&dce_config, uart_dte, esp_netif); #elif CONFIG_EXAMPLE_MODEM_DEVICE_SIM7600 == 1 @@ -303,8 +311,9 @@ extern "C" void app_main(void) const ConsoleCommand GetOperatorName("get_operator_name", "reads the operator name", no_args, [&](ConsoleCommand * c) { std::string operator_name; + int act; ESP_LOGI(TAG, "Reading operator name..."); - CHECK_ERR(dce->get_operator_name(operator_name), ESP_LOGI(TAG, "OK. Operator name: %s", operator_name.c_str())); + CHECK_ERR(dce->get_operator_name(operator_name, act), ESP_LOGI(TAG, "OK. Operator name: %s", operator_name.c_str())); }); const struct GenericCommandArgs { @@ -356,6 +365,20 @@ extern "C" void app_main(void) ESP_LOGI(TAG, "Resetting the module..."); CHECK_ERR(dce->reset(), ESP_LOGI(TAG, "OK")); }); +#ifdef CONFIG_EXAMPLE_MODEM_DEVICE_SHINY + const ConsoleCommand HandleURC("urc", "toggle urc handling", no_args, [&](ConsoleCommand * c) { + static int cnt = 0; + if (++cnt % 2) { + ESP_LOGI(TAG, "Adding URC handler"); + dce->set_on_read(handle_urc); + } else { + ESP_LOGI(TAG, "URC removed"); + dce->set_on_read(nullptr); + } + return 0; + }); +#endif + const struct SetApn { SetApn(): apn(STR1, nullptr, nullptr, "", "APN (Access Point Name)") {} CommandArgs apn; diff --git a/components/esp_modem/examples/modem_console/main/my_module_dce.cpp b/components/esp_modem/examples/modem_console/main/my_module_dce.cpp new file mode 100644 index 000000000..3d35722b5 --- /dev/null +++ b/components/esp_modem/examples/modem_console/main/my_module_dce.cpp @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* Modem console example: Custom DCE + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include "cxx_include/esp_modem_api.hpp" +#include "cxx_include/esp_modem_dce_module.hpp" +#include "generate/esp_modem_command_declare.inc" +#include "my_module_dce.hpp" + +using namespace esp_modem; + +// +// Define preprocessor's forwarding to dce_commands definitions +// + +// Helper macros to handle multiple arguments of declared API +#define ARGS0 +#define ARGS1 , p1 +#define ARGS2 , p1 , p2 +#define ARGS3 , p1 , p2 , p3 +#define ARGS4 , p1 , p2 , p3, p4 +#define ARGS5 , p1 , p2 , p3, p4, p5 +#define ARGS6 , p1 , p2 , p3, p4, p5, p6 + +#define _ARGS(x) ARGS ## x +#define ARGS(x) _ARGS(x) + +#define CMD_OK (1) +#define CMD_FAIL (2) + +// +// Repeat all declarations and forward to the AT commands defined in esp_modem::dce_commands:: namespace +// +#define ESP_MODEM_DECLARE_DCE_COMMAND(name, return_type, arg_nr, ...) \ + return_type Shiny::DCE::name(__VA_ARGS__) { return esp_modem::dce_commands::name(this ARGS(arg_nr) ); } + +DECLARE_ALL_COMMAND_APIS(return_type name(...) ) + +#undef ESP_MODEM_DECLARE_DCE_COMMAND + +std::unique_ptr create_shiny_dce(const esp_modem::dce_config *config, + std::shared_ptr dte, + esp_netif_t *netif) +{ + return Shiny::Factory::create(config, std::move(dte), netif); +} + +/** + * @brief Definition of the command API, which makes the Shiny::DCE "command-able class" + * @param cmd Command to send + * @param got_line Recv line callback + * @param time_ms timeout in ms + * @param separator line break separator + * @return OK, FAIL or TIMEOUT + */ +command_result Shiny::DCE::command(const std::string &cmd, got_line_cb got_line, uint32_t time_ms, const char separator) +{ + if (!handling_urc) { + return dte->command(cmd, got_line, time_ms, separator); + } + handle_cmd = got_line; + signal.clear(CMD_OK | CMD_FAIL); + esp_modem::DTE_Command command{cmd}; + dte->write(command); + signal.wait_any(CMD_OK | CMD_FAIL, time_ms); + handle_cmd = nullptr; + if (signal.is_any(CMD_OK)) { + return esp_modem::command_result::OK; + } + if (signal.is_any(CMD_FAIL)) { + return esp_modem::command_result::FAIL; + } + return esp_modem::command_result::TIMEOUT; +} + +/** + * @brief Handle received data + * + * @param data Data received from the device + * @param len Length of the data + * @return standard command return code (OK|FAIL|TIMEOUT) + */ +command_result Shiny::DCE::handle_data(uint8_t *data, size_t len) +{ + if (std::memchr(data, '\n', len)) { + if (handle_urc) { + handle_urc(data, len); + } + if (handle_cmd) { + auto ret = handle_cmd(data, len); + if (ret == esp_modem::command_result::TIMEOUT) { + return command_result::TIMEOUT; + } + if (ret == esp_modem::command_result::OK) { + signal.set(CMD_OK); + } + if (ret == esp_modem::command_result::FAIL) { + signal.set(CMD_FAIL); + } + } + } + return command_result::TIMEOUT; +} diff --git a/components/esp_modem/examples/modem_console/main/my_module_dce.hpp b/components/esp_modem/examples/modem_console/main/my_module_dce.hpp index 907a5a744..43205bfb2 100644 --- a/components/esp_modem/examples/modem_console/main/my_module_dce.hpp +++ b/components/esp_modem/examples/modem_console/main/my_module_dce.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Unlicense OR CC0-1.0 */ @@ -11,6 +11,8 @@ #pragma once +#include + #include "cxx_include/esp_modem_dce_factory.hpp" #include "cxx_include/esp_modem_dce_module.hpp" @@ -28,13 +30,84 @@ public: } }; +namespace Shiny { + +using namespace esp_modem; + +class DCE : public esp_modem::DCE_T, public CommandableIf { +public: + using DCE_T::DCE_T; + + command_result + command(const std::string &cmd, got_line_cb got_line, uint32_t time_ms) override + { + return command(cmd, got_line, time_ms, '\n'); + } + + command_result + command(const std::string &cmd, got_line_cb got_line, uint32_t time_ms, const char separator) override; + + int write(uint8_t *data, size_t len) override + { + return dte->write(data, len); + } + + void on_read(got_line_cb on_data) override + { + return dte->on_read(on_data); + } + +#define ESP_MODEM_DECLARE_DCE_COMMAND(name, return_type, num, ...) \ + esp_modem::return_type name(__VA_ARGS__); + + DECLARE_ALL_COMMAND_APIS(forwards name(...)) + +#undef ESP_MODEM_DECLARE_DCE_COMMAND + + void set_on_read(esp_modem::got_line_cb on_read_cb) + { + if (on_read_cb == nullptr) { + handling_urc = false; + handle_urc = nullptr; + dte->on_read(nullptr); + return; + } + handle_urc = std::move(on_read_cb); + dte->on_read([this](uint8_t *data, size_t len) { + this->handle_data(data, len); + return command_result::TIMEOUT; + }); + handling_urc = true; + } + +private: + got_line_cb handle_urc{nullptr}; + got_line_cb handle_cmd{nullptr}; + SignalGroup signal; + bool handling_urc {false}; + + command_result handle_data(uint8_t *data, size_t len); + +}; + +class Factory: public ::esp_modem::dce_factory::Factory { +public: + + static std::unique_ptr create(const esp_modem::dce_config *config, + std::shared_ptr dte, + esp_netif_t *netif) + { + return build_generic_DCE>(config, std::move(dte), netif); + } + +}; + +} // namespace Shiny + /** * @brief Helper create method which employs the DCE factory for creating DCE objects templated by a custom module * @return unique pointer of the resultant DCE */ -std::unique_ptr create_shiny_dce(const esp_modem::dce_config *config, +std::unique_ptr create_shiny_dce(const esp_modem::dce_config *config, std::shared_ptr dte, - esp_netif_t *netif) -{ - return esp_modem::dce_factory::Factory::build_unique(config, std::move(dte), netif); -} + esp_netif_t *netif); diff --git a/components/esp_modem/examples/modem_tcp_client/CMakeLists.txt b/components/esp_modem/examples/modem_tcp_client/CMakeLists.txt new file mode 100644 index 000000000..2901da172 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/CMakeLists.txt @@ -0,0 +1,9 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.8) +set(CMAKE_CXX_STANDARD 17) + +set(EXTRA_COMPONENT_DIRS "../..") + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(modem_tcp_client) diff --git a/components/esp_modem/examples/modem_tcp_client/README.md b/components/esp_modem/examples/modem_tcp_client/README.md new file mode 100644 index 000000000..a95643571 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/README.md @@ -0,0 +1,10 @@ +# Modem TCP client + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +## Overview +This example demonstrates how to act as a MQTT client using modem's TCP commands (provided, the device supports "socket" related commands) + +### Supported IDF versions + +This example is supported from IDF `v4.4`. diff --git a/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt b/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt new file mode 100644 index 000000000..3a1289c52 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS "modem_client.cpp" + "sock_dce.cpp" + "sock_commands.cpp" + INCLUDE_DIRS ".") +target_compile_options(${COMPONENT_LIB} PRIVATE "-Wno-format") diff --git a/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild b/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild new file mode 100644 index 000000000..7f423343c --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/Kconfig.projbuild @@ -0,0 +1,148 @@ +menu "Example Configuration" + + choice EXAMPLE_MODEM_DEVICE + prompt "Choose supported modem device (DCE)" + default EXAMPLE_MODEM_DEVICE_BG96 + help + Select modem device connected to the ESP DTE. + config EXAMPLE_MODEM_DEVICE_SIM800 + bool "SIM800" + help + SIMCom SIM800L is a GSM/GPRS module. + It supports Quad-band 850/900/1800/1900MHz. + config EXAMPLE_MODEM_DEVICE_BG96 + bool "BG96" + help + Quectel BG96 is a series of LTE Cat M1/Cat NB1/EGPRS module. + config EXAMPLE_MODEM_DEVICE_SIM7600 + bool "SIM7600" + help + SIM7600 is Multi-Band LTE-TDD/LTE-FDD/HSPA+ and GSM/GPRS/EDGE module + endchoice + + config EXAMPLE_MODEM_APN + string "Set MODEM APN" + default "internet" + help + Set APN (Access Point Name), a logical name to choose data network + + config EXAMPLE_MODEM_PPP_AUTH_USERNAME + string "Set username for authentication" + default "espressif" + depends on !EXAMPLE_MODEM_PPP_AUTH_NONE + help + Set username for PPP Authentication. + + config EXAMPLE_MODEM_PPP_AUTH_PASSWORD + string "Set password for authentication" + default "esp32" + depends on !EXAMPLE_MODEM_PPP_AUTH_NONE + help + Set password for PPP Authentication. + + config EXAMPLE_MODEM_PPP_AUTH_NONE + bool "Skip PPP authentication" + default n + help + Set to true for the PPP client to skip authentication + + config EXAMPLE_SEND_MSG + bool "Short message (SMS)" + default n + help + Select this, the modem will send a short message before power off. + + if EXAMPLE_SEND_MSG + config EXAMPLE_SEND_MSG_PEER_PHONE_NUMBER + string "Peer Phone Number (with area code)" + default "+8610086" + help + Enter the peer phone number that you want to send message to. + endif + + config EXAMPLE_NEED_SIM_PIN + bool "SIM PIN needed" + default n + help + Enable to set SIM PIN before starting the example + + config EXAMPLE_SIM_PIN + string "Set SIM PIN" + default "1234" + depends on EXAMPLE_NEED_SIM_PIN + help + Pin to unlock the SIM + + menu "UART Configuration" + config EXAMPLE_MODEM_UART_TX_PIN + int "TXD Pin Number" + default 25 + range 0 31 + help + Pin number of UART TX. + + config EXAMPLE_MODEM_UART_RX_PIN + int "RXD Pin Number" + default 26 + range 0 31 + help + Pin number of UART RX. + + config EXAMPLE_MODEM_UART_RTS_PIN + int "RTS Pin Number" + default 27 + range 0 31 + help + Pin number of UART RTS. + + config EXAMPLE_MODEM_UART_CTS_PIN + int "CTS Pin Number" + default 23 + range 0 31 + help + Pin number of UART CTS. + + config EXAMPLE_MODEM_UART_EVENT_TASK_STACK_SIZE + int "UART Event Task Stack Size" + range 2000 6000 + default 2048 + help + Stack size of UART event task. + + config EXAMPLE_MODEM_UART_EVENT_TASK_PRIORITY + int "UART Event Task Priority" + range 3 22 + default 5 + help + Priority of UART event task. + + config EXAMPLE_MODEM_UART_EVENT_QUEUE_SIZE + int "UART Event Queue Size" + range 10 40 + default 30 + help + Length of UART event queue. + + config EXAMPLE_MODEM_UART_PATTERN_QUEUE_SIZE + int "UART Pattern Queue Size" + range 10 40 + default 20 + help + Length of UART pattern queue. + + config EXAMPLE_MODEM_UART_TX_BUFFER_SIZE + int "UART TX Buffer Size" + range 256 2048 + default 512 + help + Buffer size of UART TX buffer. + + config EXAMPLE_MODEM_UART_RX_BUFFER_SIZE + int "UART RX Buffer Size" + range 256 2048 + default 1024 + help + Buffer size of UART RX buffer. + endmenu + +endmenu 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 new file mode 100644 index 000000000..d843c4699 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/modem_client.cpp @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +/* PPPoS Client Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_netif.h" +#include "esp_netif_ppp.h" +#include "mqtt_client.h" +#include "esp_modem_config.h" +#include "cxx_include/esp_modem_api.hpp" +#include "sock_dce.hpp" +#include "esp_log.h" + +#define BROKER_URL "mqtt.eclipseprojects.io" + +static const char *TAG = "modem_client"; +static EventGroupHandle_t event_group = NULL; +static const int CONNECT_BIT = BIT0; +static const int GOT_DATA_BIT = BIT2; + +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id); + esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) { + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + msg_id = esp_mqtt_client_subscribe(client, "/topic/esp-pppos", 0); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + break; + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + break; + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); + msg_id = esp_mqtt_client_publish(client, "/topic/esp-pppos", "esp32-pppos", 0, 0, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); + break; + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_PUBLISHED: + ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + xEventGroupSetBits(event_group, GOT_DATA_BIT); + break; + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + break; + default: + ESP_LOGI(TAG, "MQTT other event id: %d", event->event_id); + break; + } +} + +extern "C" void app_main(void) +{ + + /* Init and register system/core components */ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + event_group = xEventGroupCreate(); + + /* Configure and create the UART DTE */ + esp_modem_dte_config_t dte_config = ESP_MODEM_DTE_DEFAULT_CONFIG(); + /* setup UART specific configuration based on kconfig options */ + dte_config.uart_config.tx_io_num = CONFIG_EXAMPLE_MODEM_UART_TX_PIN; + dte_config.uart_config.rx_io_num = CONFIG_EXAMPLE_MODEM_UART_RX_PIN; + dte_config.uart_config.rts_io_num = CONFIG_EXAMPLE_MODEM_UART_RTS_PIN; + dte_config.uart_config.cts_io_num = CONFIG_EXAMPLE_MODEM_UART_CTS_PIN; + dte_config.uart_config.rx_buffer_size = CONFIG_EXAMPLE_MODEM_UART_RX_BUFFER_SIZE; + dte_config.uart_config.tx_buffer_size = CONFIG_EXAMPLE_MODEM_UART_TX_BUFFER_SIZE; + dte_config.uart_config.event_queue_size = CONFIG_EXAMPLE_MODEM_UART_EVENT_QUEUE_SIZE; + dte_config.task_stack_size = CONFIG_EXAMPLE_MODEM_UART_EVENT_TASK_STACK_SIZE * 2; + dte_config.task_priority = CONFIG_EXAMPLE_MODEM_UART_EVENT_TASK_PRIORITY; + dte_config.dte_buffer_size = CONFIG_EXAMPLE_MODEM_UART_RX_BUFFER_SIZE / 2; + + auto dte = esp_modem::create_uart_dte(&dte_config); + assert(dte); + + /* Configure the DCE */ + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG(CONFIG_EXAMPLE_MODEM_APN); + + /* create the DCE and initialize network manually (using AT commands) */ + auto dce = sock_dce::create(&dce_config, std::move(dte)); + if (!dce->init_network()) { + ESP_LOGE(TAG, "Failed to setup network"); + return; + } + + dce->init(8883); + esp_mqtt_client_config_t mqtt_config = {}; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + mqtt_config.broker.address.uri = "mqtts://127.0.0.1"; + mqtt_config.session.message_retransmit_timeout = 10000; +#else + mqtt_config.uri = "mqtt://127.0.0.1"; + mqtt_config.message_retransmit_timeout = 10000; +#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_start(mqtt_client); + if (!dce->start(BROKER_URL, 8883)) { + ESP_LOGE(TAG, "Failed to start DCE"); + return; + } + while (1) { + while (dce->perform()) { + ESP_LOGD(TAG, "...performing"); + } + ESP_LOGE(TAG, "Loop exit.. retrying"); + // handle disconnections errors + if (!dce->init_network()) { + ESP_LOGE(TAG, "Failed to reinit network"); + return; + } + if (!dce->start("test.mosquitto.org", 1883)) { + ESP_LOGI(TAG, "Network reinitialized, retrying"); + } + } + +} diff --git a/components/esp_modem/examples/modem_tcp_client/main/sock_commands.cpp b/components/esp_modem/examples/modem_tcp_client/main/sock_commands.cpp new file mode 100644 index 000000000..f091cac3b --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/sock_commands.cpp @@ -0,0 +1,186 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 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" + +namespace sock_commands { + +static const char *TAG = "sock_commands"; + +using namespace esp_modem; + +command_result net_open(CommandableIf *term) +{ + ESP_LOGV(TAG, "%s", __func__ ); + std::string response; + auto ret = dce_commands::generic_get_string(term, "AT+NETOPEN?\r", response, 1000); + if (ret != command_result::OK) { + return ret; + } + ESP_LOGV(TAG, "%s", response.data() ); + if (response.find("+NETOPEN: 1") != std::string::npos) { + ESP_LOGD(TAG, "Already there"); + return command_result::OK; + } else if (response.find("+NETOPEN: 0") != std::string::npos) { + ESP_LOGD(TAG, "Need to setup"); + return dce_commands::generic_command(term, "AT+NETOPEN\r", "+NETOPEN: 1", "+NETOPEN: 0", 10000); + } + return command_result::FAIL; +} + +command_result net_close(CommandableIf *term) +{ + ESP_LOGV(TAG, "%s", __func__ ); + return dce_commands::generic_command(term, "AT+NETCLOSE\r", "+NETCLOSE:", "ERROR", 30000); +} + +command_result tcp_open(CommandableIf *term, const std::string &host, int port, int timeout) +{ + ESP_LOGV(TAG, "%s", __func__ ); + auto ret = dce_commands::generic_command(term, "AT+CIPRXGET=1\r", "OK", "ERROR", 50000); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "Setting Rx mode failed!"); + return ret; + } + ESP_LOGV(TAG, "%s", __func__ ); + std::string ip_open = R"(AT+CIPOPEN=0,"TCP",")" + host + "\"," + std::to_string(port) + "\r"; + ret = dce_commands::generic_command(term, ip_open, "+CIPOPEN: 0,0", "ERROR", timeout); + if (ret != command_result::OK) { + ESP_LOGE(TAG, "%s Failed", __func__ ); + return ret; + } + return command_result::OK; +} + +command_result tcp_close(CommandableIf *term) +{ + ESP_LOGV(TAG, "%s", __func__ ); + return dce_commands::generic_command(term, "AT+CIPCLOSE=0\r", "+CIPCLOSE:", "ERROR", 10000); +} + +command_result tcp_send(CommandableIf *term, uint8_t *data, size_t len) +{ + ESP_LOGV(TAG, "%s", __func__ ); + std::string send = "AT+CIPSEND=0," + std::to_string(len) + "\r"; + auto ret = term->command(send, [&](uint8_t *data, size_t len) { + std::string_view response((char *)data, len); + ESP_LOGI(TAG, "CIPSEND response %.*s", static_cast(response.size()), response.data()); + if (response.find('>') != std::string::npos) { + return command_result::OK; + } + return command_result::TIMEOUT; + }, 50000, '>'); + if (ret != command_result::OK) { + return ret; + } + ret = command_result::TIMEOUT; + ESP_LOGW(TAG, "Before setting..."); + term->on_read([&ret](uint8_t *cmd_data, size_t cmd_len) { + std::string_view response((char *)cmd_data, cmd_len); + ESP_LOGW(TAG, "CIPSEND response %.*s", static_cast(response.size()), response.data()); + + if (response.find("+CIPSEND:") != std::string::npos) { + ret = command_result::OK; + } else if (response.find("ERROR") != std::string::npos) { + ret = command_result::FAIL; + } + return ret; + }); + ESP_LOGW(TAG, "Before writing..."); + auto written = term->write(data, len); + if (written != len) { + ESP_LOGE(TAG, "written %d (%d)...", written, len); + return command_result::FAIL; + } + uint8_t ctrl_z = '\x1A'; + term->write(&ctrl_z, 1); + int count = 0; + while (ret == command_result::TIMEOUT && count++ < 1000 ) { + vTaskDelay(pdMS_TO_TICKS(1000)); + } + term->on_read(nullptr); + return ret; +} + +command_result tcp_recv(CommandableIf *term, uint8_t *data, size_t len, size_t &out_len) +{ + ESP_LOGV(TAG, "%s", __func__ ); + std::string out; + auto ret = dce_commands::generic_get_string(term, "AT+CIPRXGET=4,0\r", out); + if (ret != command_result::OK) { + return ret; + } + constexpr std::string_view pattern = "+CIPRXGET: 4,0,"; + if (out.find(pattern) == std::string::npos) { + + return command_result::FAIL; + } + size_t data_len; + if (std::from_chars(out.data() + pattern.size(), out.data() + out.size(), data_len).ec == std::errc::invalid_argument) { + return command_result::FAIL; + } + ESP_LOGD(TAG, "size=%d", data_len); + if (data_len == 0) { + out_len = data_len; + return command_result::OK; + } + return term->command("AT+CIPRXGET=2,0,100\r", [&](uint8_t *cmd_data, size_t cmd_len) { + char pattern[] = "+CIPRXGET: 2,0,"; + ESP_LOG_BUFFER_HEXDUMP(TAG, cmd_data, cmd_len, ESP_LOG_DEBUG); + char *pos = strstr((char *)cmd_data, pattern); + if (pos == nullptr) { + return command_result::FAIL; + } + auto p1 = memchr(pos + sizeof(pattern) - 1, ',', 4); + if (p1 == nullptr) { + return command_result::FAIL; + } + *(char *)p1 = '\0'; + size_t actual_len = atoi(pos + sizeof(pattern) - 1); + ESP_LOGD(TAG, "actual len=%d", actual_len); + + pos = strchr((char *)p1 + 1, '\n'); + if (pos == nullptr) { + ESP_LOGE(TAG, "not found"); + return command_result::FAIL; + } + if (actual_len > len) { + ESP_LOGE(TAG, "TOO BIG"); + return command_result::FAIL; + } + out_len = actual_len; + memcpy(data, pos + 1, actual_len); + pos = strstr((char *)pos + 1 + actual_len, "OK"); + if (pos == nullptr) { + ESP_LOGE(TAG, "ok NOT FOUND"); + return command_result::FAIL; + } + return command_result::OK; + }, 50000); +} + +command_result get_ip(CommandableIf *term, std::string &ip) +{ + std::string resp; + auto ret = dce_commands::generic_get_string(term, "AT+IPADDR\r", resp, 5000); + if (ret != command_result::OK) { + return ret; + } + ip = resp; + return command_result::OK; +} + +command_result set_rx_mode(CommandableIf *term, int mode) +{ + return dce_commands::generic_command(term, "AT+CIPRXGET=" + std::to_string(mode) + "\r", "OK", "ERROR", 5000); +} + + + +} diff --git a/components/esp_modem/examples/modem_tcp_client/main/sock_commands.hpp b/components/esp_modem/examples/modem_tcp_client/main/sock_commands.hpp new file mode 100644 index 000000000..ed1f68242 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/sock_commands.hpp @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "cxx_include/esp_modem_dte.hpp" +#include "cxx_include/esp_modem_dce_module.hpp" +#include "cxx_include/esp_modem_types.hpp" +#include "socket_commands.inc" + +namespace sock_commands { + +//using namespace esp_modem; + +#define ESP_MODEM_DECLARE_DCE_COMMAND(name, return_type, num, ...) \ + esp_modem::return_type name(esp_modem::CommandableIf *t, ## __VA_ARGS__); + +DECLARE_SOCK_COMMANDS(declare name(Commandable *p, ...);) + +#undef ESP_MODEM_DECLARE_DCE_COMMAND + +} diff --git a/components/esp_modem/examples/modem_tcp_client/main/sock_dce.cpp b/components/esp_modem/examples/modem_tcp_client/main/sock_dce.cpp new file mode 100644 index 000000000..a7d23cdf0 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/sock_dce.cpp @@ -0,0 +1,483 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_vfs.h" +#include "esp_vfs_eventfd.h" + +#include "sock_dce.hpp" + +namespace sock_dce { + +constexpr auto const *TAG = "sock_dce"; + + +bool DCE::perform() +{ + if (listen_sock == -1) { + ESP_LOGE(TAG, "Listening socket not ready"); + close_sock(); + return false; + } + if (sock == -1) { // no active socket, need to accept one first + return accept_sock(); + } + + // we have a socket, let's check the status + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 500000, + }; + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(sock, &fdset); + FD_SET(data_ready_fd, &fdset); + int s = select(std::max(sock, data_ready_fd) + 1, &fdset, nullptr, nullptr, &tv); + if (s == 0) { + ESP_LOGD(TAG, "perform select timeout..."); + return true; + } else if (s < 0) { + ESP_LOGE(TAG, "select error %d", errno); + close_sock(); + return false; + } + if (FD_ISSET(sock, &fdset) && !sock_to_at()) { + return false; + } + if (FD_ISSET(data_ready_fd, &fdset) && !at_to_sock()) { + return false; + } + return true; +} + +void DCE::forwarding(uint8_t *data, size_t len) +{ + ESP_LOG_BUFFER_HEXDUMP(TAG, data, len, ESP_LOG_DEBUG); + if (state == status::SENDING) { + switch (at.send(data, len)) { + case Listener::state::OK: + state = status::IDLE; + signal.set(IDLE); + return; + case Listener::state::FAIL: + state = status::SENDING_FAILED; + signal.set(IDLE); + return; + case Listener::state::IN_PROGRESS: + return; + } + } else if (state == status::RECEIVING || state == status::RECEIVING_1 ) { + switch (at.recv(data, len)) { + case Listener::state::OK: + state = status::IDLE; + signal.set(IDLE); + return; + case Listener::state::FAIL: + state = status::RECEIVING_FAILED; + signal.set(IDLE); + return; + case Listener::state::IN_PROGRESS: + return; + } + } + std::string_view response((char *)data, len); + at.check_async_replies(response); + // Notification about Data Ready could come any time + if (state == status::SENDING) { + switch (at.send(response)) { + case Listener::state::OK: + state = status::IDLE; + signal.set(IDLE); + return; + case Listener::state::FAIL: + state = status::SENDING_FAILED; + signal.set(IDLE); + return; + case Listener::state::IN_PROGRESS: + break; + } + } + if (state == status::CONNECTING) { + switch (at.connect(response)) { + case Listener::state::OK: + state = status::IDLE; + signal.set(IDLE); + return; + case Listener::state::FAIL: + state = status::CONNECTION_FAILED; + signal.set(IDLE); + return; + case Listener::state::IN_PROGRESS: + break; + } + } +} + +void DCE::close_sock() +{ + if (sock > 0) { + close(sock); + sock = -1; + } +} + +bool DCE::at_to_sock() +{ + uint64_t data; + read(data_ready_fd, &data, sizeof(data)); + ESP_LOGD(TAG, "select read: modem data available %x", data); + if (!signal.wait(IDLE, 1000)) { + ESP_LOGE(TAG, "Failed to get idle"); + close_sock(); + return false; + } + if (state != status::IDLE) { + ESP_LOGE(TAG, "Unexpected state %d", state); + close_sock(); + return false; + } + state = status::RECEIVING; + send_cmd("AT+CIPRXGET=2,0," + std::to_string(size) + "\r"); + return true; +} + +bool DCE::sock_to_at() +{ + ESP_LOGD(TAG, "socket read: data available"); + if (!signal.wait(IDLE, 1000)) { + ESP_LOGE(TAG, "Failed to get idle"); + close_sock(); + return false; + } + if (state != status::IDLE) { + ESP_LOGE(TAG, "Unexpected state %d", state); + close_sock(); + return false; + } + state = status::SENDING; + int len = ::recv(sock, &buffer[0], size, 0); + if (len < 0) { + ESP_LOGE(TAG, "read error %d", errno); + close_sock(); + return false; + } else if (len == 0) { + ESP_LOGE(TAG, "EOF %d", errno); + close_sock(); + return false; + } + ESP_LOG_BUFFER_HEXDUMP(TAG, &buffer[0], len, ESP_LOG_VERBOSE); + data_to_send = len; + send_cmd("AT+CIPSEND=0," + std::to_string(len) + "\r"); + return true; +} + +bool DCE::accept_sock() +{ + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 500000, + }; + fd_set fdset; + FD_ZERO(&fdset); + FD_SET(listen_sock, &fdset); + int s = select(listen_sock + 1, &fdset, nullptr, nullptr, &tv); + if (s > 0 && FD_ISSET(listen_sock, &fdset)) { + struct sockaddr_in source_addr = {}; + socklen_t addr_len = sizeof(source_addr); + sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len); + if (sock < 0) { + ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno); + return false; + } + ESP_LOGD(TAG, "Socket accepted!"); + FD_ZERO(&fdset); + return true; + } else if (s == 0) { + return true; + } + return false; +} + +void DCE::init(int port) +{ + esp_vfs_eventfd_config_t config = ESP_VFS_EVENTD_CONFIG_DEFAULT(); + esp_vfs_eventfd_register(&config); + + data_ready_fd = eventfd(0, EFD_SUPPORT_ISR); + assert(data_ready_fd > 0); + + listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); + if (listen_sock < 0) { + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + return; + } + int opt = 1; + setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ESP_LOGI(TAG, "Socket created"); + struct sockaddr_in addr = { }; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); +// inet_aton("127.0.0.1", &addr.sin_addr); + + int err = bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr)); + if (err != 0) { + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + return; + } + ESP_LOGI(TAG, "Socket bound, port %d", 1883); + err = listen(listen_sock, 1); + if (err != 0) { + ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno); + return; + } + +} + +void Listener::check_async_replies(std::string_view &response) const +{ + ESP_LOGD(TAG, "response %.*s", static_cast(response.size()), response.data()); + if (response.find("+CIPRXGET: 1") != std::string::npos) { + uint64_t data_ready = 1; + write(data_ready_fd, &data_ready, sizeof(data_ready)); + ESP_LOGD(TAG, "Got data on modem!"); + } + +} + +bool DCE::start(std::string host, int port) +{ + dte->on_read(nullptr); + tcp_close(); + if (set_rx_mode(1) != esp_modem::command_result::OK) { + ESP_LOGE(TAG, "Unable to set Rx mode"); + return false; + } + dte->on_read([this](uint8_t *data, size_t len) { + this->forwarding(data, len); + return esp_modem::command_result::TIMEOUT; + }); + send_cmd(R"(AT+CIPOPEN=0,"TCP",")" + host + "\"," + std::to_string(port) + "\r"); + state = status::CONNECTING; + return true; +} + +bool DCE::init_network() +{ + const int retries = 5; + int i = 0; + while (sync() != esp_modem::command_result::OK) { + if (i++ > retries) { + ESP_LOGE(TAG, "Failed to sync up"); + return false; + } + esp_modem::Task::Delay(1000); + } + ESP_LOGD(TAG, "Modem in sync"); + i = 0; + while (setup_data_mode() != true) { + if (i++ > retries) { + ESP_LOGE(TAG, "Failed to setup pdp/data"); + return false; + } + esp_modem::Task::Delay(1000); + } + ESP_LOGD(TAG, "PDP configured"); + i = 0; + while (net_open() != esp_modem::command_result::OK) { + if (i++ > retries) { + ESP_LOGE(TAG, "Failed to open network"); + return false; + } + esp_modem::Task::Delay(1000); + } + ESP_LOGD(TAG, "Network opened"); + i = 0; + std::string ip_addr; + while (get_ip(ip_addr) != esp_modem::command_result::OK) { + if (i++ > retries) { + ESP_LOGE(TAG, "Failed obtain an IP address"); + return false; + } + esp_modem::Task::Delay(5000); + } + ESP_LOGI(TAG, "Got IP %s", ip_addr.c_str()); + return true; +} + + +class Factory: public ::esp_modem::dce_factory::Factory { +public: + static std::unique_ptr create(const esp_modem::dce_config *config, std::shared_ptr dte) + { + return esp_modem::dce_factory::Factory::build_module_T>(config, std::move(dte)); + } +}; + +std::unique_ptr create(const esp_modem::dce_config *config, std::shared_ptr dte) +{ + return Factory::create(config, std::move(dte)); +} + +// Helper macros to handle multiple arguments of declared API +#define ARGS0 +#define ARGS1 , p1 +#define ARGS2 , p1 , p2 +#define ARGS3 , p1 , p2 , p3 + +#define EXPAND_ARGS(x) ARGS ## x +#define ARGS(x) EXPAND_ARGS(x) + +// +// Repeat all declarations and forward to the AT commands defined in ::sock_commands namespace +// +#define ESP_MODEM_DECLARE_DCE_COMMAND(name, return_type, arg_nr, ...) \ + esp_modem::return_type DCE::name(__VA_ARGS__) { return sock_commands::name(dte.get() ARGS(arg_nr) ); } + +DECLARE_SOCK_COMMANDS(return_type name(...) ) + +#undef ESP_MODEM_DECLARE_DCE_COMMAND + + + + +Listener::state Listener::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) { + static constexpr std::string_view head = "+CIPRXGET: 2,0,"; + auto head_pos = std::search(recv_data, recv_data + len, head.begin(), head.end()); + if (head_pos == nullptr) { + return state::FAIL; + } +// state = status::RECEIVING_FAILED; +// signal.set(IDLE); +// return; +// } + if (head_pos - (char *)data > MIN_MESSAGE) { + // check for async replies before the Recv header + std::string_view response((char *)data, head_pos - (char *)data); + check_async_replies(response); + } + + auto next_comma = (char *)memchr(head_pos + head.size(), ',', MIN_MESSAGE); + if (next_comma == nullptr) { + return state::FAIL; + } + if (std::from_chars(head_pos + head.size(), next_comma, actual_len).ec == std::errc::invalid_argument) { + ESP_LOGE(TAG, "cannot convert"); + return state::FAIL; + } + + auto next_nl = (char *)memchr(next_comma, '\n', 8 /* total_len size (~4) + markers */); + if (next_nl == nullptr) { + ESP_LOGE(TAG, "not found"); + return state::FAIL; + } + if (actual_len > size) { + ESP_LOGE(TAG, "TOO BIG"); + return state::FAIL; + } + size_t total_len = 0; + if (std::from_chars(next_comma + 1, next_nl - 1, total_len).ec == std::errc::invalid_argument) { + ESP_LOGE(TAG, "cannot convert"); + return state::FAIL; + } + read_again = (total_len > 0); + recv_data = next_nl + 1; + auto first_data_len = len - (recv_data - (char *)data) /* minus size of the command marker */; + if (actual_len > first_data_len) { + ::send(sock, recv_data, first_data_len, 0); + data_to_recv = actual_len - first_data_len; + return state::IN_PROGRESS; + } + ::send(sock, recv_data, actual_len, 0); + } else if (data_to_recv > len) { // continue sending + ::send(sock, recv_data, len, 0); + data_to_recv -= len; + return state::IN_PROGRESS; + } else if (data_to_recv <= len) { // last read -> looking for "OK" marker + ::send(sock, recv_data, data_to_recv, 0); + actual_len = data_to_recv; + } + + // "OK" after the data + char *last_pos = nullptr; + if (actual_len + 1 + 2 /* OK */ > len) { + last_pos = (char *)memchr(recv_data + 1 + actual_len, 'O', MIN_MESSAGE); + if (last_pos == nullptr || last_pos[1] != 'K') { + data_to_recv = 0; + return state::FAIL; + } + } + if (last_pos != nullptr && (char *)data + len - last_pos > MIN_MESSAGE) { + // check for async replies after the Recv header + std::string_view response((char *)last_pos + 2 /* OK */, (char *)data + len - last_pos - 2); + check_async_replies(response); + } + data_to_recv = 0; + if (read_again) { + uint64_t data_ready = 1; + write(data_ready_fd, &data_ready, sizeof(data_ready)); + } + return state::OK; +} + +Listener::state Listener::send(uint8_t *data, size_t len) +{ + if (send_stat == 0) { + if (memchr(data, '>', len) == NULL) { + ESP_LOGE(TAG, "Missed >"); + return state::FAIL; + } + auto written = dte->write(&buffer[0], data_to_send); + if (written != data_to_send) { + ESP_LOGE(TAG, "written %d (%d)...", written, len); + return state::FAIL; + } + data_to_send = 0; + uint8_t ctrl_z = '\x1A'; + dte->write(&ctrl_z, 1); + send_stat++; + return state::IN_PROGRESS; + } + return Listener::state::IN_PROGRESS; +} + +Listener::state Listener::send(std::string_view response) +{ + if (send_stat == 1) { + if (response.find("+CIPSEND:") != std::string::npos) { + send_stat = 0; + return state::OK; + } + if (response.find("ERROR") != std::string::npos) { + ESP_LOGE(TAG, "Failed to sent"); + send_stat = 0; + return state::FAIL; + } + } + return Listener::state::IN_PROGRESS; +} + +Listener::state Listener::connect(std::string_view response) +{ + if (response.find("+CIPOPEN: 0,0") != std::string::npos) { + ESP_LOGI(TAG, "Connected!"); + return state::OK; + } + if (response.find("ERROR") != std::string::npos) { + ESP_LOGE(TAG, "Failed to open"); + return state::FAIL; + } + return Listener::state::IN_PROGRESS; +} + + +} // namespace sock_dce diff --git a/components/esp_modem/examples/modem_tcp_client/main/sock_dce.hpp b/components/esp_modem/examples/modem_tcp_client/main/sock_dce.hpp new file mode 100644 index 000000000..643713ce5 --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/sock_dce.hpp @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "esp_modem_config.h" +#include "cxx_include/esp_modem_api.hpp" +#include +#include "socket_commands.inc" +#include "sock_commands.hpp" + +#pragma once + +namespace sock_dce { + +static constexpr size_t size = 512; + +class Listener { +public: + enum class state { + OK, FAIL, IN_PROGRESS + }; + Listener(std::array &b, int &s, int &ready_fd, std::shared_ptr &dte_arg): + buffer(b), sock(s), data_ready_fd(ready_fd), dte(dte_arg) {} + state recv(uint8_t *data, size_t len); + state send(uint8_t *data, size_t len); + state send(std::string_view response); + state connect(std::string_view response); + void check_async_replies(std::string_view &response) const; +private: + std::array &buffer; + size_t data_to_recv = 0; + bool read_again = false; + int &sock; + int &data_ready_fd; + int send_stat = 0; + size_t data_to_send = 0; + std::shared_ptr &dte; +}; + +class DCE : public ::esp_modem::GenericModule { + using esp_modem::GenericModule::GenericModule; +public: + +#define ESP_MODEM_DECLARE_DCE_COMMAND(name, return_type, num, ...) \ +esp_modem::return_type name(__VA_ARGS__); + + DECLARE_SOCK_COMMANDS(declare name(Commandable *p, ...);) + +#undef ESP_MODEM_DECLARE_DCE_COMMAND + + bool init_network(); + bool start(std::string host, int port); + + void init(int port); + + bool perform(); + +private: + esp_modem::SignalGroup signal; + + void close_sock(); + bool accept_sock(); + bool sock_to_at(); + bool at_to_sock(); + + void forwarding(uint8_t *data, size_t len); + +// void check_async_replies(std::string_view &response) const; + + void send_cmd(std::string_view command) + { + dte->write((uint8_t *) command.begin(), command.size()); + } + + enum class status { + IDLE, + CONNECTING, + CONNECTION_FAILED, + SENDING, + SENDING_1, + SENDING_FAILED, + RECEIVING, + RECEIVING_1, + RECEIVING_FAILED + }; + status state{status::IDLE}; + static constexpr uint8_t IDLE = 1; + std::array buffer; + Listener at{buffer, sock, data_ready_fd, dte}; + size_t data_to_send = 0; +// size_t data_to_recv = 0; + bool read_again = false; + int sock {-1}; + int listen_sock {-1}; + int data_ready_fd {-1}; +}; + +std::unique_ptr create(const esp_modem::dce_config *config, std::shared_ptr dte); + +} diff --git a/components/esp_modem/examples/modem_tcp_client/main/socket_commands.inc b/components/esp_modem/examples/modem_tcp_client/main/socket_commands.inc new file mode 100644 index 000000000..1036df84c --- /dev/null +++ b/components/esp_modem/examples/modem_tcp_client/main/socket_commands.inc @@ -0,0 +1,58 @@ +// Copyright 2021-2022 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at + +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#pragma once + +#include "generate/esp_modem_command_declare_helper.inc" + +#define DECLARE_SOCK_COMMANDS(...) \ +/** + * @brief Opens network in AT command mode + * @return OK, FAIL or TIMEOUT + */ \ +ESP_MODEM_DECLARE_DCE_COMMAND(net_open, command_result, 0) \ + \ +/** + * @brief Closes network in AT command mode + * @return OK, FAIL or TIMEOUT + */ \ +ESP_MODEM_DECLARE_DCE_COMMAND(net_close, command_result, 0) \ + \ +/** + * @brief Opens a TCP connection + * @param[in] host Host name or IP address to connect to + * @param[in] port Port number + * @param[in] timeout Connection timeout + * @return OK, FAIL or TIMEOUT + */ \ +ESP_MODEM_DECLARE_DCE_COMMAND(tcp_open, command_result, 3, STRING_IN(p1, host), INT_IN(p2, port), INT_IN(p3, timeout)) \ + \ +/** + * @brief Closes opened TCP socket + * @return OK, FAIL or TIMEOUT + */ \ +ESP_MODEM_DECLARE_DCE_COMMAND(tcp_close, command_result, 0) \ + \ +/** + * @brief Gets modem IP address + * @param[out] addr String representation of modem's IP + * @return OK, FAIL or TIMEOUT + */ \ +ESP_MODEM_DECLARE_DCE_COMMAND(get_ip, command_result, 1, STRING_OUT(p1, addr)) \ + \ +/** + * @brief Sets Rx mode + * @param[in] mode 0=auto, 1=manual + * @return OK, FAIL or TIMEOUT + */ \ +ESP_MODEM_DECLARE_DCE_COMMAND(set_rx_mode, command_result, 1, INT_IN(p1, mode)) diff --git a/components/esp_modem/examples/simple_cmux_client/CMakeLists.txt b/components/esp_modem/examples/simple_cmux_client/CMakeLists.txt index 4f298cdb6..be1c2ac41 100644 --- a/components/esp_modem/examples/simple_cmux_client/CMakeLists.txt +++ b/components/esp_modem/examples/simple_cmux_client/CMakeLists.txt @@ -1,6 +1,7 @@ # The following lines of boilerplate have to be in your project's CMakeLists # in this exact order for cmake to work correctly -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.8) +set(CMAKE_CXX_STANDARD 17) set(EXTRA_COMPONENT_DIRS "../..") diff --git a/components/esp_modem/include/generate/esp_modem_command_declare_helper.inc b/components/esp_modem/include/generate/esp_modem_command_declare_helper.inc new file mode 100644 index 000000000..2d2cec90d --- /dev/null +++ b/components/esp_modem/include/generate/esp_modem_command_declare_helper.inc @@ -0,0 +1,26 @@ + + +// Parameters +// * handle different parameters for C++ and C API +// * make parameter unique names, so they could be easily referenced and forwarded +#define _ARG(param, name) param +#define INT_IN(param, name) int _ARG(param, name) +#ifdef __cplusplus +#include +#define STRING_IN(param, name) const std::string& _ARG(param, name) +#define STRING_OUT(param, name) std::string& _ARG(param, name) +#define BOOL_IN(param, name) const bool _ARG(param, name) +#define BOOL_OUT(param, name) bool& _ARG(param, name) +#define INT_OUT(param, name) int& _ARG(param, name) +#define INTEGER_LIST_IN(param, name) const int* _ARG(param, name) + +#define STRUCT_OUT(struct_name, p1) struct_name& p1 +#else +#define STRING_IN(param, name) const char* _ARG(param, name) +#define STRING_OUT(param, name) char* _ARG(param, name) +#define BOOL_IN(param, name) const bool _ARG(param, name) +#define BOOL_OUT(param, name) bool* _ARG(param, name) +#define INT_OUT(param, name) int* _ARG(param, name) +#define INTEGER_LIST_IN(param, name) const int* _ARG(param, name) +#define STRUCT_OUT(struct_name, p1) esp_modem_ ## struct_name ## _t* p1 +#endif