diff --git a/components/esp_modem/include/cxx_include/esp_modem_dce.hpp b/components/esp_modem/include/cxx_include/esp_modem_dce.hpp index 3db3fd86dd..045783c2dd 100644 --- a/components/esp_modem/include/cxx_include/esp_modem_dce.hpp +++ b/components/esp_modem/include/cxx_include/esp_modem_dce.hpp @@ -30,9 +30,11 @@ class DCE_Mode { ~DCE_Mode() = default; bool set(DTE *dte, ModuleIf *module, Netif &netif, modem_mode m); modem_mode get(); + modem_mode guess(DTE *dte, bool with_cmux = false); private: bool set_unsafe(DTE *dte, ModuleIf *module, Netif &netif, modem_mode m); + modem_mode guess_unsafe(DTE *dte, bool with_cmux); modem_mode mode; }; @@ -79,6 +81,11 @@ class DCE_T { return dte->command(command, std::move(got_line), time_ms); } + modem_mode guess_mode(bool with_cmux = false) + { + return mode.guess(dte.get(), with_cmux); + } + bool set_mode(modem_mode m) { return mode.set(dte.get(), device.get(), netif, m); diff --git a/components/esp_modem/include/cxx_include/esp_modem_dte.hpp b/components/esp_modem/include/cxx_include/esp_modem_dte.hpp index 787e9810ff..f576e75f1e 100644 --- a/components/esp_modem/include/cxx_include/esp_modem_dte.hpp +++ b/components/esp_modem/include/cxx_include/esp_modem_dte.hpp @@ -65,6 +65,18 @@ class DTE : public CommandableIf { int write(DTE_Command command); + /** + * @brief send data to the selected terminal, by default (without term_id argument) + * this API works the same as write: sends data to the secondary terminal, which is + * typically used as data terminal (for networking). + * + * @param data Data pointer to write + * @param len Data len to write + * @param term_id Terminal id: Primary if id==0, Secondary if id==1 + * @return number of bytes written + */ + int send(uint8_t *data, size_t len, int term_id = 1); + /** * @brief Reading from the underlying terminal * @param d Returning the data pointer of the received payload diff --git a/components/esp_modem/include/cxx_include/esp_modem_types.hpp b/components/esp_modem/include/cxx_include/esp_modem_types.hpp index 944427156d..1387ee33ba 100644 --- a/components/esp_modem/include/cxx_include/esp_modem_types.hpp +++ b/components/esp_modem/include/cxx_include/esp_modem_types.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -37,6 +37,17 @@ enum class modem_mode { CMUX_MANUAL_DATA, /*!< Sets the primary terminal to DATA mode in manual CMUX */ CMUX_MANUAL_COMMAND, /*!< Sets the primary terminal to COMMAND mode in manual CMUX */ CMUX_MANUAL_SWAP, /*!< Swaps virtual terminals in manual CMUX mode (primary <-> secondary) */ + RESUME_DATA_MODE, /*!< This is used when the device is already in DATA mode and we need the modem lib to + * enter the mode without switching. On success, we would end up in DATA-mode, UNDEF otherwise */ + RESUME_COMMAND_MODE, /*!< This is used when the device is already in COMMAND mode and we want to resume it + * On success, we would end up in DATA-mode, UNDEF otherwise */ + RESUME_CMUX_MANUAL_MODE, /*!< This is used when the device is already in CMUX mode and we need the modem lib to + * enter it without switching. On success, we would end up in CMUX_MANUAL-mode, UNDEF otherwise */ + RESUME_CMUX_MANUAL_DATA, /*!< This is used when the device is already in CMUX-DATA mode and we need the modem lib to + * enter it without switching. On success, we would end up in CMUX_MANUAL-DATA mode, UNDEF otherwise */ + AUTODETECT, /*!< Auto-detection command: It tries to send a few packets in order to recognize which mode the + * the device currently is and update the modem library mode. On success the modem is updated, + * otherwise it's set to UNDEF */ }; /** diff --git a/components/esp_modem/src/esp_modem_dce.cpp b/components/esp_modem/src/esp_modem_dce.cpp index 55c51f0cdc..f178eea7d5 100644 --- a/components/esp_modem/src/esp_modem_dce.cpp +++ b/components/esp_modem/src/esp_modem_dce.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -103,6 +103,51 @@ bool DCE_Mode::set_unsafe(DTE *dte, ModuleIf *device, Netif &netif, modem_mode m return true; case modem_mode::DUAL_MODE: // Only DTE can be in Dual mode break; + case modem_mode::AUTODETECT: { + auto guessed = guess_unsafe(dte, true); + if (guessed == modem_mode::UNDEF) { + return false; + } + // prepare the undefined mode before to allow all possible transitions + if (!dte->set_mode(modem_mode::UNDEF)) { + return false; + } + mode = modem_mode::UNDEF; + ESP_LOGD("DCE mode", "Detected mode: %d", static_cast(guessed)); + if (guessed == modem_mode::DATA_MODE) { + return set_unsafe(dte, device, netif, esp_modem::modem_mode::RESUME_DATA_MODE); + } else if (guessed == esp_modem::modem_mode::COMMAND_MODE) { + return set_unsafe(dte, device, netif, esp_modem::modem_mode::RESUME_COMMAND_MODE); + } else if (guessed == esp_modem::modem_mode::CMUX_MODE) { + if (!set_unsafe(dte, device, netif, esp_modem::modem_mode::RESUME_CMUX_MANUAL_MODE)) { + return false; + } + // now we guess the mode for each terminal + guessed = guess_unsafe(dte, false); + ESP_LOGD("DCE mode", "Detected mode on primary term: %d", static_cast(guessed)); + // now we need to access the second terminal, so we could simply send a SWAP command + // (switching to data mode does the swapping internally, so we only swap if we're in CMD mode) + if (guessed == modem_mode::DATA_MODE) { + // switch to DATA on the primary terminal and swap terminals + if (!set_unsafe(dte, device, netif, esp_modem::modem_mode::RESUME_CMUX_MANUAL_DATA)) { + return false; + } + } else { + // swap terminals + if (!set_unsafe(dte, device, netif, esp_modem::modem_mode::CMUX_MANUAL_SWAP)) { + return false; + } + } + guessed = guess_unsafe(dte, false); + ESP_LOGD("DCE mode", "Detected mode on secondary term: %d", static_cast(guessed)); + if (guessed == modem_mode::DATA_MODE) { + if (!set_unsafe(dte, device, netif, esp_modem::modem_mode::RESUME_CMUX_MANUAL_DATA)) { + return false; + } + } + } + return true; + } case modem_mode::COMMAND_MODE: if (mode == modem_mode::COMMAND_MODE || mode >= modem_mode::CMUX_MANUAL_MODE) { return false; @@ -122,6 +167,32 @@ bool DCE_Mode::set_unsafe(DTE *dte, ModuleIf *device, Netif &netif, modem_mode m } mode = m; return true; + case modem_mode::RESUME_DATA_MODE: + if (!dte->set_mode(modem_mode::DATA_MODE)) { + return false; + } + netif.start(); + mode = modem_mode::DATA_MODE; + return true; + case modem_mode::RESUME_COMMAND_MODE: + if (!dte->set_mode(modem_mode::COMMAND_MODE)) { + return false; + } + mode = modem_mode::COMMAND_MODE; + return true; + case modem_mode::RESUME_CMUX_MANUAL_MODE: + if (!dte->set_mode(modem_mode::CMUX_MANUAL_MODE)) { + return false; + } + mode = modem_mode::CMUX_MANUAL_MODE; + return true; + case modem_mode::RESUME_CMUX_MANUAL_DATA: + if (!dte->set_mode(modem_mode::CMUX_MANUAL_SWAP)) { + return false; + } + netif.start(); + mode = modem_mode::CMUX_MANUAL_MODE; + return true; case modem_mode::DATA_MODE: if (mode == modem_mode::DATA_MODE || mode == modem_mode::CMUX_MODE || mode >= modem_mode::CMUX_MANUAL_MODE) { return false; @@ -191,4 +262,114 @@ modem_mode DCE_Mode::get() return mode; } +modem_mode DCE_Mode::guess(DTE *dte, bool with_cmux) +{ + Scoped lock(*dte); + return guess_unsafe(dte, with_cmux); +} + +/** + * This namespace contains probe packets and expected replies on 3 different protocols, + * the modem device could use (as well as timeouts and mode ids for synchronisation) + */ +namespace probe { + +namespace ppp { +// Test that we're in the PPP mode by sending an LCP protocol echo request and expecting LCP echo reply +constexpr std::array lcp_echo_request = {0x7e, 0xff, 0x03, 0xc0, 0x21, 0x09, 0x01, 0x00, 0x08, 0x99, 0xd1, 0x35, 0xc1, 0x8e, 0x2c, 0x7e }; +constexpr std::array lcp_echo_reply_head = {0x7e, 0xff, 0x7d, 0x23, 0xc0}; +const size_t mode = 1 << 0; +const int timeout = 200; +} + +namespace cmd { +// For command mode, we just send a simple AT command +const char at[] = "\r\nAT\r\n"; +const size_t max_at_reply = 16; // account for some whitespaces and/or CMUX encapsulation +const char reply[] = { 'O', 'K' }; +const int mode = 1 << 1; +const int timeout = 500; +} + +namespace cmux { +// For CMUX mode, we send an SABM on control terminal (0) +const uint8_t sabm0_reqest[] = {0xf9, 0x03, 0x3f, 0x01, 0x1c, 0xf9}; +const uint8_t sabm0_reply[] = {0xf9, 0x03, 0x73, 0x01}; +const int mode = 1 << 0; +const int timeout = 200; +} +}; + +modem_mode DCE_Mode::guess_unsafe(DTE *dte, bool with_cmux) +{ + // placeholder for reply and its size, since it could come in pieces, and we have to cache + // this is captured by the lambda by reference. + // must make sure the lambda is cleared before exiting this function (done by dte->on_read(nullptr)) + uint8_t reply[std::max(probe::cmd::max_at_reply, std::max(sizeof(probe::ppp::lcp_echo_request), sizeof(probe::cmux::sabm0_reply)))]; + size_t reply_pos = 0; + auto signal = std::make_shared(); + std::weak_ptr weak_signal = signal; + dte->on_read([weak_signal, with_cmux, &reply, &reply_pos](uint8_t *data, size_t len) { + // storing the response in the `reply` array and de-fragmenting + if (reply_pos >= sizeof(reply)) { + return command_result::TIMEOUT; + } + auto reply_size = std::min((size_t)sizeof(reply) - reply_pos, len); + ::memcpy(reply + reply_pos, data, reply_size); + reply_pos += reply_size; + ESP_LOG_BUFFER_HEXDUMP("esp-modem: guess mode data:", reply, reply_pos, ESP_LOG_DEBUG); + + // Check whether the response resembles the "golden" reply (for these 3 protocols) + if (reply_pos >= sizeof(probe::ppp::lcp_echo_reply_head)) { + // check for initial 2 bytes + auto *ptr = static_cast(memmem(reply, reply_pos, probe::ppp::lcp_echo_reply_head.data(), 2)); + // and check the other two bytes for protocol ID: LCP + if (ptr && ptr[3] == probe::ppp::lcp_echo_reply_head[3] && ptr[4] == probe::ppp::lcp_echo_reply_head[4]) { + if (auto signal = weak_signal.lock()) { + signal->set(probe::ppp::mode); + } + } + } + if (reply_pos >= 4 && memmem(reply, reply_pos, probe::cmd::reply, sizeof(probe::cmd::reply))) { + if (reply[0] != 0xf9) { // double check that the reply is not wrapped in CMUX headers + if (auto signal = weak_signal.lock()) { + signal->set(probe::cmd::mode); + } + } + } + if (with_cmux && reply_pos >= sizeof(probe::cmux::sabm0_reply)) { + // checking the initial 3 bytes + auto *ptr = static_cast(memmem(reply, reply_pos, probe::cmux::sabm0_reply, 3)); + // and checking that DLCI is 0 (control frame) + if (ptr && (ptr[3] >> 2) == 0) { + if (auto signal = weak_signal.lock()) { + signal->set(probe::cmux::mode); + } + } + } + return command_result::TIMEOUT; + }); + auto guessed = modem_mode::UNDEF; + // Check the PPP mode fist by sending LCP echo request + dte->send((uint8_t *)probe::ppp::lcp_echo_request.data(), sizeof(probe::ppp::lcp_echo_request), 0); + if (signal->wait(probe::ppp::mode, probe::ppp::timeout)) { + guessed = modem_mode::DATA_MODE; + } else { // LCP echo timeout + // now check for AT mode + reply_pos = 0; + dte->send((uint8_t *)probe::cmd::at, sizeof(probe::cmd::at), 0); + if (signal->wait(probe::cmd::mode, probe::cmd::timeout)) { + guessed = modem_mode::COMMAND_MODE; + } else if (with_cmux) { // no AT reply, check for CMUX mode (if requested) + reply_pos = 0; + dte->send((uint8_t *) probe::cmux::sabm0_reqest, sizeof(probe::cmux::sabm0_reqest), 0); + if (signal->wait(probe::cmux::mode, probe::cmux::timeout)) { + guessed = modem_mode::CMUX_MODE; + } + } + } + dte->on_read(nullptr); + return guessed; +} + } // esp_modem diff --git a/components/esp_modem/src/esp_modem_dte.cpp b/components/esp_modem/src/esp_modem_dte.cpp index 677708fa4e..8345c6fe8b 100644 --- a/components/esp_modem/src/esp_modem_dte.cpp +++ b/components/esp_modem/src/esp_modem_dte.cpp @@ -223,13 +223,13 @@ bool DTE::set_mode(modem_mode m) } } // transitions (COMMAND|DUAL|CMUX|UNDEF) -> DATA - if (m == modem_mode::DATA_MODE) { + if (m == modem_mode::DATA_MODE || m == modem_mode::RESUME_DATA_MODE) { if (mode == modem_mode::CMUX_MODE || mode == modem_mode::CMUX_MANUAL_MODE || mode == modem_mode::DUAL_MODE) { // mode stays the same, but need to swap terminals (as command has been switched) secondary_term.swap(primary_term); set_command_callbacks(); } else { - mode = m; + mode = modem_mode::DATA_MODE; } return true; } @@ -316,6 +316,12 @@ int DTE::write(uint8_t *data, size_t len) return secondary_term->write(data, len); } +int DTE::send(uint8_t *data, size_t len, int term_id) +{ + Terminal *term = term_id == 0 ? primary_term.get() : secondary_term.get(); + return term->write(data, len); +} + int DTE::write(DTE_Command command) { return primary_term->write(command.data, command.len); diff --git a/components/esp_modem/src/esp_modem_netif.cpp b/components/esp_modem/src/esp_modem_netif.cpp index d808db0748..fbb75c37c5 100644 --- a/components/esp_modem/src/esp_modem_netif.cpp +++ b/components/esp_modem/src/esp_modem_netif.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -87,8 +87,10 @@ void Netif::start() receive(data, len); return true; }); - signal.set(PPP_STARTED); - esp_netif_action_start(driver.base.netif, nullptr, 0, nullptr); + if (!signal.is_any(PPP_STARTED)) { + signal.set(PPP_STARTED); + esp_netif_action_start(driver.base.netif, nullptr, 0, nullptr); + } } void Netif::stop()