From a6baff9085dbd8a68030a0e45cb56029fa7e7c65 Mon Sep 17 00:00:00 2001 From: oarcher Date: Wed, 17 Jul 2024 00:08:29 +0200 Subject: [PATCH] automation and better DCE recover --- esphome/components/modem/__init__.py | 45 ++++-- esphome/components/modem/automation.h | 17 +++ esphome/components/modem/modem_component.cpp | 145 ++++++++++++++++--- esphome/components/modem/modem_component.h | 16 +- 4 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 esphome/components/modem/automation.h diff --git a/esphome/components/modem/__init__.py b/esphome/components/modem/__init__.py index 06665f91d9..cc16ead6e5 100644 --- a/esphome/components/modem/__init__.py +++ b/esphome/components/modem/__init__.py @@ -6,12 +6,13 @@ from esphome.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_MODEL, + CONF_TRIGGER_ID, ) import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import coroutine_with_priority from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option -from esphome.components.binary_sensor import BinarySensor +from esphome import automation CODEOWNERS = ["@oarcher"] DEPENDENCIES = ["esp32"] @@ -19,18 +20,19 @@ AUTO_LOAD = ["network"] # following should be removed if conflicts are resolved (so we can have a wifi ap using modem) CONFLICTS_WITH = ["wifi", "captive_portal", "ethernet"] -CONF_POWER_PIN = "power_pin" -CONF_FLIGHT_PIN = "flight_pin" CONF_PIN_CODE = "pin_code" CONF_APN = "apn" -CONF_STATUS_PIN = "status_pin" CONF_DTR_PIN = "dtr_pin" CONF_INIT_AT = "init_at" -CONF_READY = "ready" - +# CONF_ON_SCRIPT = "on_script" +# CONF_OFF_SCRIPT = "off_script" +CONF_ON_NOT_RESPONDING = "on_not_responding" modem_ns = cg.esphome_ns.namespace("modem") ModemComponent = modem_ns.class_("ModemComponent", cg.Component) +ModemNotRespondingTrigger = modem_ns.class_( + "ModemNotRespondingTrigger", automation.Trigger.template() +) CONFIG_SCHEMA = cv.All( @@ -41,16 +43,21 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_RX_PIN): cv.positive_int, cv.Required(CONF_MODEL): cv.string, cv.Required(CONF_APN): cv.string, - cv.Required(CONF_READY): cv.use_id(BinarySensor), - cv.Optional(CONF_FLIGHT_PIN): cv.positive_int, - cv.Optional(CONF_POWER_PIN): cv.positive_int, - cv.Optional(CONF_STATUS_PIN): cv.positive_int, + # cv.Optional(CONF_ON_SCRIPT): cv.use_id(Script), + # cv.Optional(CONF_OFF_SCRIPT): cv.use_id(Script), cv.Optional(CONF_DTR_PIN): cv.positive_int, cv.Optional(CONF_PIN_CODE): cv.string_strict, cv.Optional(CONF_USERNAME): cv.string, 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_ON_NOT_RESPONDING): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ModemNotRespondingTrigger + ) + } + ), } ).extend(cv.COMPONENT_SCHEMA), cv.require_framework_version( @@ -102,9 +109,17 @@ async def to_code(config): for cmd in init_at: cg.add(var.add_init_at_command(cmd)) - if modem_ready := config.get(CONF_READY, None): - modem_ready_sensor = await cg.get_variable(modem_ready) - cg.add(var.set_ready_bsensor(modem_ready_sensor)) + # if modem_ready := config.get(CONF_READY, None): + # modem_ready_sensor = await cg.get_variable(modem_ready) + # cg.add(var.set_ready_bsensor(modem_ready_sensor)) + + # if conf_on_script := config.get(CONF_ON_SCRIPT, None): + # on_script = await cg.get_variable(conf_on_script) + # cg.add(var.set_on_script(on_script)) + # + # if conf_off_script := config.get(CONF_OFF_SCRIPT, None): + # off_script = await cg.get_variable(conf_off_script) + # cg.add(var.set_off_script(off_script)) cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_apn(config[CONF_APN])) @@ -114,4 +129,8 @@ async def to_code(config): cg.add(var.set_rx_pin(getattr(gpio_num_t, f"GPIO_NUM_{config[CONF_RX_PIN]}"))) cg.add(var.set_tx_pin(getattr(gpio_num_t, f"GPIO_NUM_{config[CONF_TX_PIN]}"))) + for conf in config.get(CONF_ON_NOT_RESPONDING, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + await cg.register_component(var, config) diff --git a/esphome/components/modem/automation.h b/esphome/components/modem/automation.h new file mode 100644 index 0000000000..52a9e9390c --- /dev/null +++ b/esphome/components/modem/automation.h @@ -0,0 +1,17 @@ +#pragma once +#include "modem_component.h" + +#include "esphome/core/automation.h" + +namespace esphome { +namespace modem { + +class ModemNotRespondingTrigger : public Trigger<> { + public: + explicit ModemNotRespondingTrigger(ModemComponent *parent) { + parent->add_on_not_responding_callback([this, parent]() { this->trigger(); }); + } +}; + +} // namespace modem +} // namespace esphome diff --git a/esphome/components/modem/modem_component.cpp b/esphome/components/modem/modem_component.cpp index 10dcbe1610..3108bee905 100644 --- a/esphome/components/modem/modem_component.cpp +++ b/esphome/components/modem/modem_component.cpp @@ -35,7 +35,20 @@ ModemComponent *global_modem_component; // NOLINT(cppcoreguidelines-avoid-non-c return; \ } -using namespace esp_modem; +std::string command_result_to_string(command_result err) { + std::string res = "UNKNOWN"; + switch (err) { + case command_result::FAIL: + res = "FAIL"; + break; + case command_result::OK: + res = "OK"; + break; + case command_result::TIMEOUT: + res = "TIMEOUT"; + } + return res; +} ModemComponent::ModemComponent() { global_modem_component = this; } @@ -158,18 +171,8 @@ void ModemComponent::start_connect_() { global_modem_component->got_ipv4_address_ = false; - this->dce->set_mode(modem_mode::COMMAND_MODE); vTaskDelay(pdMS_TO_TICKS(2000)); - command_result res = command_result::TIMEOUT; - - res = this->dce->sync(); - - if (res != command_result::OK) { - ESP_LOGW(TAG, "Unable to sync modem. Will retry later"); - return; - } - 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"); @@ -205,8 +208,7 @@ void ModemComponent::start_connect_() { if (this->dce->set_mode(modem_mode::CMUX_MODE)) { ESP_LOGD(TAG, "Modem has correctly entered multiplexed command/data mode"); } else { - ESP_LOGE(TAG, "Failed to configure multiplexed command mode... exiting"); - return; + ESP_LOGE(TAG, "Failed to configure multiplexed command mode. Trying to continue..."); } vTaskDelay(pdMS_TO_TICKS(2000)); @@ -215,7 +217,11 @@ void ModemComponent::start_connect_() { std::string result; command_result err = this->dce->at(cmd.c_str(), result, 1000); delay(100); // NOLINT - ESP_LOGI(TAG, "Init AT command: %s (status %d) -> %s", cmd.c_str(), (int) err, result.c_str()); + if (err != command_result::OK) { + ESP_LOGE(TAG, "Error while executing '%s' command (status %s)", cmd.c_str(), + command_result_to_string(err).c_str()); + } + ESP_LOGI(TAG, "Init AT command: %s -> %s", cmd.c_str(), result.c_str()); } } @@ -229,17 +235,62 @@ void ModemComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base void ModemComponent::loop() { const uint32_t now = millis(); - static uint32_t last_log_time = now; + static uint32_t last_health_check = now; + const uint32_t healh_check_interval = 30000; switch (this->state_) { case ModemComponentState::STOPPED: + if ((this->on_script_ != nullptr && this->on_script_->is_running()) || + (this->off_script_ != nullptr && this->off_script_->is_running())) { + break; + } if (this->started_) { if (!this->modem_ready()) { - if (now - last_log_time > 20000) { - ESP_LOGD(TAG, "Waiting for the modem to be ready..."); - last_log_time = now; + // ESP_LOGW(TAG, "Trying to recover dce"); + // ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->recover()); + // delay(1000); + // ESP_LOGW(TAG, "Forcing undef mode"); + // ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->set_mode(esp_modem::modem_mode::UNDEF)); + // delay(1000); + ESP_LOGW(TAG, "Forcing cmux manual mode mode"); + ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_MODE)); + delay(1000); + // // ESP_LOGW(TAG, "Trying to recover dce"); + // // ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->recover()); + // // delay(1000); + ESP_LOGW(TAG, "Forcing cmux manual command mode"); + ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_COMMAND)); + delay(1000); + ESP_LOGW(TAG, "Forcing cmux manual exit mode"); + ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->set_mode(esp_modem::modem_mode::CMUX_MANUAL_EXIT)); + delay(1000); + // ESP_LOGW(TAG, "Forcing command mode"); + // ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->set_mode(esp_modem::modem_mode::COMMAND_MODE)); + // delay(1000); + // ESP_LOGW(TAG, "Forcing reset"); + // this->dce->reset(); + // ESP_LOGW(TAG, "Forcing hangup"); + // this->dce->hang_up(); + // this->send_at("AT+CGATT=0"); // disconnect network + // delay(1000); + // this->send_at("ATH"); // hangup + // delay(1000); + // delay(1000); + // ESP_LOGW(TAG, "Forcing disconnect"); + // this->send_at("AT+CGATT=0"); // disconnect network + // delay(1000); + // ESP_LOGW(TAG, "Unable to sync modem"); + if (!this->modem_ready()) { + this->on_not_responding_callback_.call(); + // if (this->on_script_ != nullptr) { + // ESP_LOGD(TAG, "Executing recover_script"); + // this->on_script_->execute(); + // } else { + // ESP_LOGE(TAG, "Modem not responding, and no recover_script"); + // } + } else { + ESP_LOGD(TAG, "Modem is ready"); } - break; } else { ESP_LOGI(TAG, "Starting modem connection"); this->state_ = ModemComponentState::CONNECTING; @@ -260,7 +311,8 @@ void ModemComponent::loop() { this->status_clear_warning(); } else if (now - this->connect_begin_ > 45000) { ESP_LOGW(TAG, "Connecting via Modem failed! Re-connecting..."); - this->start_connect_(); + this->state_ = ModemComponentState::STOPPED; + // this->start_connect_(); } break; case ModemComponentState::CONNECTED: @@ -271,6 +323,15 @@ void ModemComponent::loop() { ESP_LOGW(TAG, "Connection via Modem lost! Re-connecting..."); this->state_ = ModemComponentState::CONNECTING; this->start_connect_(); + } else { + if ((now - last_health_check) >= healh_check_interval) { + ESP_LOGV(TAG, "Health check"); + last_health_check = now; + if (!this->send_at("AT+CGREG?")) { + ESP_LOGW(TAG, "Modem not responding. Re-connecting..."); + this->state_ = ModemComponentState::STOPPED; + } + } } break; } @@ -293,6 +354,50 @@ void ModemComponent::dump_connect_params_() { ESP_LOGCONFIG(TAG, " DNS fallback: %s", network::IPAddress(dns_fallback_ip).str().c_str()); } +bool ModemComponent::send_at(const std::string &cmd) { + std::string result; + // esp_modem::command_result err; + bool status; + ESP_LOGV(TAG, "Sending command: %s", cmd.c_str()); + status = this->dce->at(cmd, result, 3000) == esp_modem::command_result::OK; + ESP_LOGV(TAG, "Result for command %s: %s (status %d)", cmd.c_str(), result.c_str(), status); + return status; +} + +bool ModemComponent::get_imei(std::string &result) { + // wrapper around this->dce->get_imei() that check that the result is valid + // (so it can be used to check if the modem is responding correctly (a simple 'AT' cmd is sometime not enough)) + command_result status; + status = this->dce->get_imei(result); + bool success = true; + + if (status == command_result::OK && result.length() == 15) { + for (char c : result) { + if (!isdigit(static_cast(c))) { + success = false; + break; + } + } + } else { + success = false; + } + + if (!success) { + result = "UNAVAILABLE"; + } + return success; +} + +bool ModemComponent::modem_ready() { + // check if the modem is ready to answer AT commands + std::string imei; + return this->get_imei(imei); +} + +void ModemComponent::add_on_not_responding_callback(std::function &&callback) { + this->on_not_responding_callback_.add(std::move(callback)); +} + } // namespace modem } // namespace esphome diff --git a/esphome/components/modem/modem_component.h b/esphome/components/modem/modem_component.h index e8a2fe877f..dce65535eb 100644 --- a/esphome/components/modem/modem_component.h +++ b/esphome/components/modem/modem_component.h @@ -4,8 +4,7 @@ #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/components/network/util.h" -#include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/template/binary_sensor/template_binary_sensor.h" +#include "esphome/components/script/script.h" #ifdef USE_ESP_IDF @@ -58,15 +57,20 @@ class ModemComponent : public Component { void set_model(const std::string &model) { this->model_ = this->modem_model_map_.count(model) ? modem_model_map_[model] : ModemModel::UNKNOWN; } - void set_ready_bsensor(binary_sensor::BinarySensor *modem_ready) { this->modem_ready_ = modem_ready; } + void set_on_script(script::Script<> *on_script) { this->on_script_ = on_script; } + void set_off_script(script::Script<> *off_script) { this->off_script_ = off_script; } void add_init_at_command(const std::string &cmd) { this->init_at_commands_.push_back(cmd); } - bool modem_ready() { return this->modem_ready_->state; } + bool send_at(const std::string &cmd); + bool get_imei(std::string &result); + bool modem_ready(); + void add_on_not_responding_callback(std::function &&callback); std::unique_ptr dce; protected: gpio_num_t rx_pin_ = gpio_num_t::GPIO_NUM_NC; gpio_num_t tx_pin_ = gpio_num_t::GPIO_NUM_NC; - binary_sensor::BinarySensor *modem_ready_; + script::Script<> *on_script_ = nullptr; + script::Script<> *off_script_ = nullptr; std::string pin_code_; std::string username_; std::string password_; @@ -90,6 +94,8 @@ class ModemComponent : public Component { static void got_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_; + + CallbackManager on_not_responding_callback_; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)