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 249d006145..b703d99137 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 @@ -235,7 +235,9 @@ extern "C" void app_main(void) if (c->get_count_of(&SetModeArgs::mode)) { auto mode = c->get_string_of(&SetModeArgs::mode); modem_mode dev_mode; - if (mode == "CMUX1") { + if (mode == "UNDEF") { + dev_mode = esp_modem::modem_mode::UNDEF; + } else if (mode == "CMUX1") { dev_mode = esp_modem::modem_mode::CMUX_MANUAL_MODE; } else if (mode == "CMUX2") { dev_mode = esp_modem::modem_mode::CMUX_MANUAL_EXIT; diff --git a/components/esp_modem/src/esp_modem_dce.cpp b/components/esp_modem/src/esp_modem_dce.cpp index 4397302c38..55c51f0cdc 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-2022 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -96,6 +96,11 @@ bool DCE_Mode::set_unsafe(DTE *dte, ModuleIf *device, Netif &netif, modem_mode m { switch (m) { case modem_mode::UNDEF: + if (!dte->set_mode(m)) { + return false; + } + mode = m; + return true; case modem_mode::DUAL_MODE: // Only DTE can be in Dual mode break; case modem_mode::COMMAND_MODE: @@ -151,7 +156,7 @@ bool DCE_Mode::set_unsafe(DTE *dte, ModuleIf *device, Netif &netif, modem_mode m mode = modem_mode::CMUX_MANUAL_MODE; return true; case modem_mode::CMUX_MANUAL_EXIT: - if (mode != modem_mode::CMUX_MANUAL_MODE) { + if (mode != modem_mode::CMUX_MANUAL_MODE && mode != modem_mode::UNDEF) { return false; } if (!dte->set_mode(m)) { @@ -160,7 +165,7 @@ bool DCE_Mode::set_unsafe(DTE *dte, ModuleIf *device, Netif &netif, modem_mode m mode = modem_mode::COMMAND_MODE; return true; case modem_mode::CMUX_MANUAL_SWAP: - if (mode != modem_mode::CMUX_MANUAL_MODE) { + if (mode != modem_mode::CMUX_MANUAL_MODE && mode != modem_mode::UNDEF) { return false; } if (!dte->set_mode(m)) { @@ -168,12 +173,12 @@ bool DCE_Mode::set_unsafe(DTE *dte, ModuleIf *device, Netif &netif, modem_mode m } return true; case modem_mode::CMUX_MANUAL_DATA: - if (mode != modem_mode::CMUX_MANUAL_MODE) { + if (mode != modem_mode::CMUX_MANUAL_MODE && mode != modem_mode::UNDEF) { return false; } return transitions::enter_data(*dte, *device, netif); case modem_mode::CMUX_MANUAL_COMMAND: - if (mode != modem_mode::CMUX_MANUAL_MODE) { + if (mode != modem_mode::CMUX_MANUAL_MODE && mode != modem_mode::UNDEF) { return false; } return transitions::exit_data(*dte, *device, netif); diff --git a/components/esp_modem/src/esp_modem_dte.cpp b/components/esp_modem/src/esp_modem_dte.cpp index 08b8af3a7e..d402166fb1 100644 --- a/components/esp_modem/src/esp_modem_dte.cpp +++ b/components/esp_modem/src/esp_modem_dte.cpp @@ -151,10 +151,14 @@ command_result DTE::command(const std::string &cmd, got_line_cb got_line, uint32 bool DTE::exit_cmux() { + if (!cmux_term) { + return false; + } if (!cmux_term->deinit()) { return false; } exit_cmux_internal(); + cmux_term.reset(); return true; } @@ -174,6 +178,10 @@ void DTE::exit_cmux_internal() bool DTE::setup_cmux() { + if (cmux_term) { + ESP_LOGE("esp_modem_dte", "Cannot setup_cmux(), cmux_term already exists"); + return false; + } cmux_term = std::make_shared(primary_term, std::move(buffer)); if (cmux_term == nullptr) { return false; @@ -198,6 +206,11 @@ bool DTE::setup_cmux() bool DTE::set_mode(modem_mode m) { + // transitions (any) -> UNDEF + if (m == modem_mode::UNDEF) { + mode = m; + return true; + } // transitions (COMMAND|UNDEF) -> CMUX if (m == modem_mode::CMUX_MODE) { if (mode == modem_mode::UNDEF || mode == modem_mode::COMMAND_MODE) { @@ -246,7 +259,7 @@ bool DTE::set_mode(modem_mode m) return false; } // manual CMUX transitions: Exit CMUX - if (m == modem_mode::CMUX_MANUAL_EXIT && mode == modem_mode::CMUX_MANUAL_MODE) { + if (m == modem_mode::CMUX_MANUAL_EXIT && (mode == modem_mode::CMUX_MANUAL_MODE || mode == modem_mode::UNDEF)) { if (exit_cmux()) { mode = modem_mode::COMMAND_MODE; return true; @@ -255,7 +268,7 @@ bool DTE::set_mode(modem_mode m) return false; } // manual CMUX transitions: Swap terminals - if (m == modem_mode::CMUX_MANUAL_SWAP && mode == modem_mode::CMUX_MANUAL_MODE) { + if (m == modem_mode::CMUX_MANUAL_SWAP && (mode == modem_mode::CMUX_MANUAL_MODE || mode == modem_mode::UNDEF)) { secondary_term.swap(primary_term); set_command_callbacks(); return true; diff --git a/components/esp_modem/test/host_test/main/test_modem.cpp b/components/esp_modem/test/host_test/main/test_modem.cpp index 4a238f5bc5..1a27cc59fb 100644 --- a/components/esp_modem/test/host_test/main/test_modem.cpp +++ b/components/esp_modem/test/host_test/main/test_modem.cpp @@ -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 */ @@ -247,3 +247,88 @@ TEST_CASE("Test CMUX protocol by injecting payloads", "[esp_modem]") CHECK(ret == command_result::OK); } } + +TEST_CASE("Command and Data mode transitions", "[esp_modem][transitions]") +{ + auto term = std::make_unique(); + auto loopback = term.get(); + auto dte = std::make_shared(std::move(term)); + CHECK(term == nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("APN"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + CHECK(dce != nullptr); + + // UNDEF -> CMD (OK) + uint8_t resp[] = "DISCONNECTED\n"; + loopback->inject(&resp[0], sizeof(resp), sizeof(resp), /* 10ms before injecting reply */100, 0); + loopback->write(nullptr, 0); /* this triggers sending the injected response */ + CHECK(dce->set_mode(esp_modem::modem_mode::COMMAND_MODE) == true); + loopback->inject(nullptr, 0, 0, 0, 0); /* reset injection, use synchronous replies now */ + // CMD -> CMD (Fail) + CHECK(dce->set_mode(esp_modem::modem_mode::COMMAND_MODE) == false); + + // Forcing transition to CMD (via UNDEF) + // CMD -> UNDEF (OK) + CHECK(dce->set_mode(esp_modem::modem_mode::UNDEF) == true); + // UNDEF -> CMD (OK) + CHECK(dce->set_mode(esp_modem::modem_mode::COMMAND_MODE) == true); + + // CMD -> DATA (OK) + CHECK(dce->set_mode(esp_modem::modem_mode::DATA_MODE) == true); + // DATA -> CMD (OK) + CHECK(dce->set_mode(esp_modem::modem_mode::COMMAND_MODE) == true); +} + +TEST_CASE("CMUX mode transitions", "[esp_modem][transitions]") +{ + auto term = std::make_unique(); + auto dte = std::make_shared(std::move(term)); + CHECK(term == nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("APN"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + CHECK(dce != nullptr); + // UNDEF -> CMUX (OK) + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MODE) == true); + // CMUX -> DATA (Fail) + CHECK(dce->set_mode(esp_modem::modem_mode::DATA_MODE) == false); + // CMUX back -> CMD (OK) + CHECK(dce->set_mode(esp_modem::modem_mode::COMMAND_MODE) == true); +} + +TEST_CASE("CMUX manual mode transitions", "[esp_modem][transitions]") +{ + auto term = std::make_unique(); + auto dte = std::make_shared(std::move(term)); + CHECK(term == nullptr); + + esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("APN"); + esp_netif_t netif{}; + auto dce = create_SIM7600_dce(&dce_config, dte, &netif); + CHECK(dce != nullptr); + + // Happy flow transitions of Manual CMUX transitions + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_MODE) == true); + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_EXIT) == true); + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_MODE) == true); + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_SWAP) == true); + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_DATA) == true); + // Cannot test CMUX_MANUAL_DATA -> CMUX_MANUAL_COMMAND with our mocked terminal for now + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_EXIT) == true); + + // Check some out of order manual transitions, most of them are allowed, + // but some fail as modem layers report issues with specific steps + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_SWAP) == false); // cannot go directly to SWAP + CHECK(dce->set_mode(esp_modem::modem_mode::UNDEF) == true); + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_SWAP) == true); // can go via UNDEF + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_EXIT) == false); // EXIT is allowed, but CMUX terms don't exist + CHECK(dce->set_mode(esp_modem::modem_mode::UNDEF) == true); + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_MODE) == true); // Enter CMUX (via UNDEF) + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_DATA) == true); // Go directly to DATA mode + CHECK(dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_EXIT) == true); // Exit CMUX + CHECK(dce->set_mode(esp_modem::modem_mode::UNDEF) == true); // Succeeds from any state + +} diff --git a/docs/esp_modem/en/README.rst b/docs/esp_modem/en/README.rst index cab8b87459..3d368c2bed 100644 --- a/docs/esp_modem/en/README.rst +++ b/docs/esp_modem/en/README.rst @@ -81,16 +81,68 @@ Common use cases of the esp-modem are also listed as the examples: - ``examples/modem_console`` is an example to exercise all possible module commands in a console application. - ``examples/ap_to_pppos`` this example focuses on the network connectivity of the esp-modem and provides a WiFi AP that forwards packets (and uses NAT) to and from the PPPoS connection. +Working modes +~~~~~~~~~~~~~ + +Modem devices could work in multiple different modes, esp-modem library +uses these states to describe them: +- Standard modes: + - Command mode -- This mode is used for sending AT commands + - Data or PPP mode -- This mode is used for data communication (to create PPPoS tunnel between the device and the library) +- Multiplexing modes: + - CMUX mode -- This mode creates two virtual channels and uses one for sending AT commands and the other one for data communication. + - DUAL mode -- This mode uses two physical channels the same way as CMUX. This mode is supported only by certain devices, usually with USB interface. +- Manual CMUX modes -- These modes are designed for applications to have better control over CMUX mode transitions. It allows setting up the virtual channels, + switching between channels, transitioning between data and command modes for each channel separately, and exiting the CMUX. + +Switching between common modes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The diagram below depicts allowed transitions between the most common modes + +:: + + +---------+ +---------+ + | COMMAND |<-->| DATA | + +---------+ +---------+ + ^ + | + v + +-------+ + | CMUX | + +-------+ + +Note that it is possible to switch from any mode to the "UNDEF" mode and vice-versa. + +Switching between manual modes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The diagram below depicts allowed transitions between manual CMUX modes + +:: + +------------------------------------ + | | + +----------+ +-------------+ +------------+ +----------+ + | |<-->| MANUAL_DATA |<-->| MANUAL_CMD |<-->| COMMAND | + | CMUX | +-------------+ +------------+ | (CMUX | + | MANUAL | | | MANUAL | + | | +-------------+ | EXIT) | + | |<-->| MANUAL_SWAP |<-------------------->| | + +----------+ +-------------+ +----------+ + | | + +-----------------------------------------------------+ + +Note that transitioning between "MANUAL_DATA" and "MANUAL_CMD" switches the secondary terminal (dedicated to PPP session) and could be used for recovering data communication if PPP session gets dropped. + Extensibility ------------- CMUX ~~~~ -Implementation of virtual terminals is an experimental feature, which -allows users to also issue commands in the data mode, after creating -multiple virtual terminals, designating some of them solely to data -mode, others solely to command mode. +Implements virtual terminals which allow users to also issue commands in the data mode; +after creating two virtual terminals, designating one of them solely to data mode, and +another one solely to command mode. DTE ~~~