diff --git a/esphome/components/modem/__init__.py b/esphome/components/modem/__init__.py index 70b66a6f11..d9d47a3890 100644 --- a/esphome/components/modem/__init__.py +++ b/esphome/components/modem/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_ON_CONNECT, CONF_ON_DISCONNECT, + CONF_ENABLE_ON_BOOT, ) import esphome.codegen as cg import esphome.config_validation as cv @@ -30,7 +31,7 @@ CONF_ON_NOT_RESPONDING = "on_not_responding" modem_ns = cg.esphome_ns.namespace("modem") ModemComponent = modem_ns.class_("ModemComponent", cg.Component) -ModemState = modem_ns.enum("ModemState") +ModemComponentState = modem_ns.enum("ModemComponentState") ModemOnNotRespondingTrigger = modem_ns.class_( "ModemOnNotRespondingTrigger", automation.Trigger.template() ) @@ -38,7 +39,7 @@ ModemOnConnectTrigger = modem_ns.class_( "ModemOnConnectTrigger", automation.Trigger.template() ) ModemOnDisconnectTrigger = modem_ns.class_( - "ModemOndisconnectTrigger", automation.Trigger.template() + "ModemOnDisconnectTrigger", automation.Trigger.template() ) @@ -56,6 +57,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PASSWORD): cv.string, cv.Optional(CONF_USE_ADDRESS): cv.string, cv.Optional(CONF_INIT_AT): cv.All(cv.ensure_list(cv.string)), + cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ON_NOT_RESPONDING): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( @@ -121,6 +123,10 @@ async def to_code(config): if pin_code := config.get(CONF_PIN_CODE, None): cg.add(var.set_pin_code(pin_code)) + if enable_on_boot := config.get(CONF_ENABLE_ON_BOOT, None): + if enable_on_boot: + cg.add(var.enable()) + if init_at := config.get(CONF_INIT_AT, None): for cmd in init_at: cg.add(var.add_init_at_command(cmd)) diff --git a/esphome/components/modem/automation.h b/esphome/components/modem/automation.h index 3fa18dd339..151a47f55a 100644 --- a/esphome/components/modem/automation.h +++ b/esphome/components/modem/automation.h @@ -12,8 +12,8 @@ namespace modem { class ModemOnNotRespondingTrigger : public Trigger<> { public: explicit ModemOnNotRespondingTrigger(ModemComponent *parent) { - parent->add_on_state_callback([this, parent](ModemState state) { - if (!parent->is_failed() && state == ModemState::NOT_RESPONDING) { + parent->add_on_state_callback([this, parent](ModemComponentState state) { + if (!parent->is_failed() && state == ModemComponentState::NOT_RESPONDING) { this->trigger(); } }); @@ -23,8 +23,8 @@ class ModemOnNotRespondingTrigger : public Trigger<> { class ModemOnConnectTrigger : public Trigger<> { public: explicit ModemOnConnectTrigger(ModemComponent *parent) { - parent->add_on_state_callback([this, parent](ModemState state) { - if (!parent->is_failed() && state == ModemState::CONNECTED) { + parent->add_on_state_callback([this, parent](ModemComponentState state) { + if (!parent->is_failed() && state == ModemComponentState::CONNECTED) { this->trigger(); } }); @@ -34,8 +34,8 @@ class ModemOnConnectTrigger : public Trigger<> { class ModemOnDisconnectTrigger : public Trigger<> { public: explicit ModemOnDisconnectTrigger(ModemComponent *parent) { - parent->add_on_state_callback([this, parent](ModemState state) { - if (!parent->is_failed() && state == ModemState::DISCONNECTED) { + parent->add_on_state_callback([this, parent](ModemComponentState state) { + if (!parent->is_failed() && state == ModemComponentState::DISCONNECTED) { this->trigger(); } }); diff --git a/esphome/components/modem/modem_component.cpp b/esphome/components/modem/modem_component.cpp index 085296110a..0b2afc6675 100644 --- a/esphome/components/modem/modem_component.cpp +++ b/esphome/components/modem/modem_component.cpp @@ -52,6 +52,31 @@ std::string command_result_to_string(command_result err) { return res; } +std::string state_to_string(ModemComponentState state) { + std::string str; + switch (state) { + case ModemComponentState::NOT_RESPONDING: + str = "NOT_RESPONDING"; + break; + case ModemComponentState::DISCONNECTED: + str = "DISCONNECTED"; + break; + case ModemComponentState::CONNECTING: + str = "CONNECTING"; + break; + case ModemComponentState::CONNECTED: + str = "CONNECTED"; + break; + case ModemComponentState::DISCONNECTING: + str = "DISCONNECTING"; + break; + case ModemComponentState::DISABLED: + str = "DISABLED"; + break; + } + return str; +} + void set_wdt(uint32_t timeout_s) { #if ESP_IDF_VERSION_MAJOR >= 5 esp_task_wdt_config_t wdt_config = { @@ -104,8 +129,34 @@ bool ModemComponent::is_connected() { return this->state_ == ModemComponentState void ModemComponent::setup() { ESP_LOGI(TAG, "Setting up Modem..."); - // increase WDT, because setup and start_connect take some time. - set_wdt(60); + + esp_err_t err; + err = esp_netif_init(); + ESPHL_ERROR_CHECK(err, "PPP netif init error"); + err = esp_event_loop_create_default(); + ESPHL_ERROR_CHECK(err, "PPP event loop init error"); + + esp_netif_config_t netif_ppp_config = ESP_NETIF_DEFAULT_PPP(); + + this->ppp_netif_ = esp_netif_new(&netif_ppp_config); + assert(this->ppp_netif_); + + // err = esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &ModemComponent::ip_event_handler, nullptr, + // nullptr); + err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &ModemComponent::ip_event_handler, nullptr); + ESPHL_ERROR_CHECK(err, "IP event handler register error"); + + this->reset_(); + + ESP_LOGV(TAG, "Setup finished"); +} + +void ModemComponent::reset_() { + // destroy previous dte/dce, and recreate them. + // destroying them seems to be the only way to have a clear state after hang up, and be able to reconnect. + + this->dte_.reset(); + this->dce.reset(); ESP_LOGV(TAG, "DTE setup"); esp_modem_dte_config_t dte_config = ESP_MODEM_DTE_DEFAULT_CONFIG(); @@ -124,37 +175,19 @@ void ModemComponent::setup() { this->dte_ = create_uart_dte(&this->dte_config_); + ESP_LOGV(TAG, "PPP netif setup"); + assert(this->dte_); ESP_LOGV(TAG, "Set APN: %s", this->apn_.c_str()); esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG(this->apn_.c_str()); - ESP_LOGV(TAG, "PPP netif setup"); - esp_err_t err; - err = esp_netif_init(); - ESPHL_ERROR_CHECK(err, "PPP netif init error"); - err = esp_event_loop_create_default(); - ESPHL_ERROR_CHECK(err, "PPP event loop init error"); - esp_netif_config_t netif_ppp_config = ESP_NETIF_DEFAULT_PPP(); - - this->ppp_netif_ = esp_netif_new(&netif_ppp_config); - assert(this->ppp_netif_); if (!this->username_.empty()) { ESP_LOGV(TAG, "Set auth: username: %s password: %s", this->username_.c_str(), this->password_.c_str()); ESPHL_ERROR_CHECK(esp_netif_ppp_set_auth(this->ppp_netif_, NETIF_PPP_AUTHTYPE_PAP, this->username_.c_str(), this->password_.c_str()), "ppp set auth"); } - // dns setup not needed (perhaps fallback ?) - // esp_netif_dns_info_t dns_main = {}; - // dns_main.ip.u_addr.ip4.addr = esp_ip4addr_aton("8.8.8.8"); - // dns_main.ip.type = ESP_IPADDR_TYPE_V4; - // ESPHL_ERROR_CHECK(esp_netif_set_dns_info(this->ppp_netif_, ESP_NETIF_DNS_MAIN, &dns_main), "dns_main"); - - // Register user defined event handers - err = esp_event_handler_register(IP_EVENT, IP_EVENT_PPP_GOT_IP, &ModemComponent::got_ip_event_handler, nullptr); - ESPHL_ERROR_CHECK(err, "GOT IP event handler register error"); - ESP_LOGV(TAG, "DCE setup"); // NOLINTBEGIN(bugprone-branch-clone) @@ -181,19 +214,6 @@ void ModemComponent::setup() { assert(this->dce); - this->started_ = true; - - set_wdt(CONFIG_ESP_TASK_WDT_TIMEOUT_S); - ESP_LOGV(TAG, "Setup finished"); -} - -void ModemComponent::start_connect_() { - set_wdt(60); - this->connect_begin_ = millis(); - this->status_set_warning("Starting connection"); - - global_modem_component->got_ipv4_address_ = false; - if (this->dte_config_.uart_config.flow_control == ESP_MODEM_FLOW_CONTROL_HW) { if (command_result::OK != this->dce->set_flow_control(2, 2)) { ESP_LOGE(TAG, "Failed to set the set_flow_control mode"); @@ -204,6 +224,25 @@ void ModemComponent::start_connect_() { ESP_LOGI(TAG, "not set_flow_control, because 2-wire mode active."); } + set_wdt(60); + // be sure that the modem is not in CMUX mode + this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_MODE); + this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_COMMAND); + this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_EXIT); + + if (!this->modem_ready()) { + ESP_LOGW(TAG, "Modem still not ready after reset"); + } + set_wdt(CONFIG_ESP_TASK_WDT_TIMEOUT_S); +} + +void ModemComponent::start_connect_() { + set_wdt(60); + this->connect_begin_ = millis(); + this->status_set_warning("Starting connection"); + + global_modem_component->got_ipv4_address_ = false; + bool pin_ok = true; if (this->dce->read_pin(pin_ok) == command_result::OK && !pin_ok) { if (!this->pin_code_.empty()) { @@ -244,24 +283,36 @@ void ModemComponent::start_connect_() { set_wdt(CONFIG_ESP_TASK_WDT_TIMEOUT_S); } -void ModemComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { - ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; - const esp_netif_ip_info_t *ip_info = &event->ip_info; - ESP_LOGW(TAG, "[IP event] Got IP " IPSTR, IP2STR(&ip_info->ip)); - global_modem_component->got_ipv4_address_ = true; - global_modem_component->connected_ = true; +void ModemComponent::ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { + ip_event_got_ip_t *event; + const esp_netif_ip_info_t *ip_info; + switch (event_id) { + case IP_EVENT_PPP_GOT_IP: + event = (ip_event_got_ip_t *) event_data; + ip_info = &event->ip_info; + ESP_LOGW(TAG, "[IP event] Got IP " IPSTR, IP2STR(&ip_info->ip)); + global_modem_component->got_ipv4_address_ = true; + global_modem_component->connected_ = true; + break; + case IP_EVENT_PPP_LOST_IP: + ESP_LOGI(TAG, "got ip lost event"); + global_modem_component->got_ipv4_address_ = false; + global_modem_component->connected_ = false; + break; + } } void ModemComponent::loop() { + static ModemComponentState last_state = this->state_; const uint32_t now = millis(); static uint32_t last_health_check = now; const uint32_t healh_check_interval = 30000; switch (this->state_) { - case ModemComponentState::STOPPED: - set_wdt(60); + case ModemComponentState::NOT_RESPONDING: if (this->started_) { if (!this->modem_ready()) { + set_wdt(60); ESP_LOGD(TAG, "Modem not responding. Trying to recover..."); // Errors are not checked, because some commands return FAIL, but make the modem to answer again... @@ -269,45 +320,63 @@ void ModemComponent::loop() { this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_MODE); ESP_LOGV(TAG, "Forcing cmux manual command mode"); this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_COMMAND); - ESP_LOGW(TAG, "Forcing cmux manual exit mode"); + ESP_LOGV(TAG, "Forcing cmux manual exit mode"); this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_EXIT); - if (!this->modem_ready()) { - this->on_state_callback_.call(ModemState::NOT_RESPONDING); - } if (this->modem_ready()) { ESP_LOGI(TAG, "Modem is ready"); + this->state_ = ModemComponentState::DISCONNECTED; + } else { + // try to run `on_not_responding` user callback + this->on_state_callback_.call(this->state_); + if (!this->modem_ready()) { + ESP_LOGW(TAG, "Unable to recover modem. Reseting dte/dce"); + this->reset_(); + } } - } else { - ESP_LOGI(TAG, "Starting modem connection"); - this->state_ = ModemComponentState::CONNECTING; - this->start_connect_(); + set_wdt(CONFIG_TASK_WDT_TIMEOUT_S); } } - set_wdt(CONFIG_TASK_WDT_TIMEOUT_S); break; + + case ModemComponentState::DISCONNECTED: + if (this->enabled_) { + if (this->started_) { + if (this->modem_ready()) { + ESP_LOGI(TAG, "Starting modem connection"); + this->state_ = ModemComponentState::CONNECTING; + this->start_connect_(); + } else { + this->state_ = ModemComponentState::NOT_RESPONDING; + } + } else { + // when disconnected, we have to reset the dte and the dce + this->reset_(); + this->started_ = true; + } + } else { + this->state_ = ModemComponentState::DISABLED; + } + break; + case ModemComponentState::CONNECTING: if (!this->started_) { ESP_LOGI(TAG, "Stopped modem connection"); - this->state_ = ModemComponentState::STOPPED; + this->state_ = ModemComponentState::DISCONNECTED; } else if (this->connected_) { - // connection established ESP_LOGI(TAG, "Connected via Modem"); this->state_ = ModemComponentState::CONNECTED; this->dump_connect_params_(); this->status_clear_warning(); - this->on_state_callback_.call(ModemState::CONNECTED); } else if (now - this->connect_begin_ > 45000) { ESP_LOGW(TAG, "Connecting via Modem failed! Re-connecting..."); - this->state_ = ModemComponentState::STOPPED; - // this->start_connect_(); + this->state_ = ModemComponentState::DISCONNECTED; } break; case ModemComponentState::CONNECTED: if (!this->started_) { - ESP_LOGI(TAG, "Stopped Modem connection"); - this->state_ = ModemComponentState::STOPPED; + this->state_ = ModemComponentState::DISCONNECTED; } else if (!this->connected_) { ESP_LOGW(TAG, "Connection via Modem lost! Re-connecting..."); this->state_ = ModemComponentState::CONNECTING; @@ -316,14 +385,61 @@ void ModemComponent::loop() { if ((now - last_health_check) >= healh_check_interval) { ESP_LOGV(TAG, "Health check"); last_health_check = now; - if (this->send_at("AT+CGREG?") == "ERROR") { - ESP_LOGW(TAG, "Modem not responding. Re-connecting..."); - this->state_ = ModemComponentState::STOPPED; + if (!this->modem_ready()) { + ESP_LOGW(TAG, "Modem not responding while connected"); + this->state_ = ModemComponentState::NOT_RESPONDING; } } } break; + + case ModemComponentState::DISCONNECTING: + if (this->started_) { + if (this->connected_) { + ESP_LOGD(TAG, "Hanging up..."); + this->dce->hang_up(); + } + this->started_ = false; + } else if (!this->connected_) { + // ip lost as expected + this->state_ = ModemComponentState::DISCONNECTED; + this->reset_(); // reset dce/dte + } else { + // waiting for ip to be lost (TODO: possible infinite loop ?) + } + + break; + + case ModemComponentState::DISABLED: + if (this->enabled_) { + this->state_ = ModemComponentState::DISCONNECTED; + } + break; } + + if (this->state_ != last_state) { + ESP_LOGV(TAG, "State changed: %s -> %s", state_to_string(last_state).c_str(), + state_to_string(this->state_).c_str()); + if (this->state_ != ModemComponentState::NOT_RESPONDING) { + // NOT_RESPONDING cb is handled separately. + this->on_state_callback_.call(this->state_); + } + + last_state = this->state_; + } +} + +void ModemComponent::enable() { + if (this->state_ == ModemComponentState::DISABLED) { + this->state_ = ModemComponentState::DISCONNECTED; + } + this->started_ = true; + this->enabled_ = true; +} + +void ModemComponent::disable() { + this->enabled_ = false; + this->state_ = ModemComponentState::DISCONNECTING; } void ModemComponent::dump_connect_params_() { @@ -382,10 +498,12 @@ bool ModemComponent::get_imei(std::string &result) { bool ModemComponent::modem_ready() { // check if the modem is ready to answer AT commands std::string imei; + set_wdt(60); return this->get_imei(imei); + set_wdt(CONFIG_TASK_WDT_TIMEOUT_S); } -void ModemComponent::add_on_state_callback(std::function &&callback) { +void ModemComponent::add_on_state_callback(std::function &&callback) { this->on_state_callback_.add(std::move(callback)); } diff --git a/esphome/components/modem/modem_component.h b/esphome/components/modem/modem_component.h index b6a7433cab..04a3538523 100644 --- a/esphome/components/modem/modem_component.h +++ b/esphome/components/modem/modem_component.h @@ -27,18 +27,13 @@ using namespace esp_modem; static const char *const TAG = "modem"; -// used internally for loop management enum class ModemComponentState { - STOPPED, + NOT_RESPONDING, + DISCONNECTED, CONNECTING, CONNECTED, -}; - -// Automation states -enum class ModemState { - NOT_RESPONDING, - CONNECTED, - DISCONNECTED, + DISCONNECTING, + DISABLED, }; enum class ModemModel { BG96, SIM800, SIM7000, SIM7070, SIM7600, UNKNOWN }; @@ -68,10 +63,15 @@ class ModemComponent : public Component { std::string send_at(const std::string &cmd); bool get_imei(std::string &result); bool modem_ready(); - void add_on_state_callback(std::function &&callback); - std::unique_ptr dce; + void enable(); + void disable(); + void add_on_state_callback(std::function &&callback); + std::unique_ptr dce{nullptr}; protected: + void reset_(); + // void close_(); + // bool linked_(); gpio_num_t rx_pin_ = gpio_num_t::GPIO_NUM_NC; gpio_num_t tx_pin_ = gpio_num_t::GPIO_NUM_NC; std::string pin_code_; @@ -85,20 +85,21 @@ class ModemComponent : public Component { {"SIM7000", ModemModel::SIM7000}, {"SIM7070", ModemModel::SIM7070}, {"SIM7600", ModemModel::SIM7600}}; - std::shared_ptr dte_; + std::shared_ptr dte_{nullptr}; esp_netif_t *ppp_netif_{nullptr}; esp_modem_dte_config_t dte_config_; - ModemComponentState state_{ModemComponentState::STOPPED}; + ModemComponentState state_{ModemComponentState::DISABLED}; void start_connect_(); bool started_{false}; + bool enabled_{false}; bool connected_{false}; bool got_ipv4_address_{false}; uint32_t connect_begin_; - static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); + static void ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); void dump_connect_params_(); std::string use_address_; uint32_t command_delay_ = 500; - CallbackManager on_state_callback_; + CallbackManager on_state_callback_; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/tests/components/modem/test.esp32-idf.yaml b/tests/components/modem/test.esp32-idf.yaml index c41d623eeb..446a612296 100644 --- a/tests/components/modem/test.esp32-idf.yaml +++ b/tests/components/modem/test.esp32-idf.yaml @@ -7,7 +7,13 @@ modem: model: SIM7600 apn: orange pin_code: "0000" + enabled_on_boot: True init_at: - AT on_not_responding: logger.log: "modem not responding" + on_connect: + logger.log: "got IP" + on_disconnect: + logger.log: "lost IP" +