Skip to content

Commit

Permalink
Merge pull request #612 from david-cermak/feat/modem_mode_detect
Browse files Browse the repository at this point in the history
[modem]: Add support for guessing mode
  • Loading branch information
david-cermak authored Nov 6, 2024
2 parents 0b5e362 + 1a9eaf3 commit 2442f6b
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
Expand Down Expand Up @@ -89,7 +89,7 @@ void wakeup_modem(void)
vTaskDelay(pdMS_TO_TICKS(2000));
}

#ifdef CONFIG_EXAMPLE_MODEM_DEVICE_SHINY
#ifdef CONFIG_ESP_MODEM_URC_HANDLER
command_result handle_urc(uint8_t *data, size_t len)
{
ESP_LOG_BUFFER_HEXDUMP("on_read", data, len, ESP_LOG_INFO);
Expand Down Expand Up @@ -238,7 +238,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 == "UNDEF") {
if (mode == "AUTO") {
dev_mode = esp_modem::modem_mode::AUTODETECT;
} else if (mode == "UNDEF") {
dev_mode = esp_modem::modem_mode::UNDEF;
} else if (mode == "CMUX1") {
dev_mode = esp_modem::modem_mode::CMUX_MANUAL_MODE;
Expand Down Expand Up @@ -370,15 +372,15 @@ 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
#ifdef CONFIG_ESP_MODEM_URC_HANDLER
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);
dce->set_urc(handle_urc);
} else {
ESP_LOGI(TAG, "URC removed");
dce->set_on_read(nullptr);
dce->set_urc(nullptr);
}
return 0;
});
Expand Down
7 changes: 7 additions & 0 deletions components/esp_modem/include/cxx_include/esp_modem_dce.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

};
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions components/esp_modem/include/cxx_include/esp_modem_dte.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion components/esp_modem/include/cxx_include/esp_modem_types.hpp
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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 */
};

/**
Expand Down
183 changes: 182 additions & 1 deletion components/esp_modem/src/esp_modem_dce.cpp
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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<int>(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<int>(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<int>(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;
Expand All @@ -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;
Expand Down Expand Up @@ -191,4 +262,114 @@ modem_mode DCE_Mode::get()
return mode;
}

modem_mode DCE_Mode::guess(DTE *dte, bool with_cmux)
{
Scoped<DTE> 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<uint8_t, 16> lcp_echo_request = {0x7e, 0xff, 0x03, 0xc0, 0x21, 0x09, 0x01, 0x00, 0x08, 0x99, 0xd1, 0x35, 0xc1, 0x8e, 0x2c, 0x7e };
constexpr std::array<uint8_t, 5> 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<SignalGroup>();
std::weak_ptr<SignalGroup> 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<uint8_t *>(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<uint8_t *>(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
10 changes: 8 additions & 2 deletions components/esp_modem/src/esp_modem_dte.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 5 additions & 3 deletions components/esp_modem/src/esp_modem_netif.cpp
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 2442f6b

Please sign in to comment.