automation and better DCE recover

This commit is contained in:
oarcher 2024-07-17 00:08:29 +02:00
parent 321594ae3d
commit a6baff9085
4 changed files with 185 additions and 38 deletions

View file

@ -6,12 +6,13 @@ from esphome.const import (
CONF_USERNAME, CONF_USERNAME,
CONF_PASSWORD, CONF_PASSWORD,
CONF_MODEL, CONF_MODEL,
CONF_TRIGGER_ID,
) )
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.core import coroutine_with_priority from esphome.core import coroutine_with_priority
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option 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"] CODEOWNERS = ["@oarcher"]
DEPENDENCIES = ["esp32"] 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) # following should be removed if conflicts are resolved (so we can have a wifi ap using modem)
CONFLICTS_WITH = ["wifi", "captive_portal", "ethernet"] CONFLICTS_WITH = ["wifi", "captive_portal", "ethernet"]
CONF_POWER_PIN = "power_pin"
CONF_FLIGHT_PIN = "flight_pin"
CONF_PIN_CODE = "pin_code" CONF_PIN_CODE = "pin_code"
CONF_APN = "apn" CONF_APN = "apn"
CONF_STATUS_PIN = "status_pin"
CONF_DTR_PIN = "dtr_pin" CONF_DTR_PIN = "dtr_pin"
CONF_INIT_AT = "init_at" 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") modem_ns = cg.esphome_ns.namespace("modem")
ModemComponent = modem_ns.class_("ModemComponent", cg.Component) ModemComponent = modem_ns.class_("ModemComponent", cg.Component)
ModemNotRespondingTrigger = modem_ns.class_(
"ModemNotRespondingTrigger", automation.Trigger.template()
)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
@ -41,16 +43,21 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_RX_PIN): cv.positive_int, cv.Required(CONF_RX_PIN): cv.positive_int,
cv.Required(CONF_MODEL): cv.string, cv.Required(CONF_MODEL): cv.string,
cv.Required(CONF_APN): cv.string, cv.Required(CONF_APN): cv.string,
cv.Required(CONF_READY): cv.use_id(BinarySensor), # cv.Optional(CONF_ON_SCRIPT): cv.use_id(Script),
cv.Optional(CONF_FLIGHT_PIN): cv.positive_int, # cv.Optional(CONF_OFF_SCRIPT): cv.use_id(Script),
cv.Optional(CONF_POWER_PIN): cv.positive_int,
cv.Optional(CONF_STATUS_PIN): cv.positive_int,
cv.Optional(CONF_DTR_PIN): cv.positive_int, cv.Optional(CONF_DTR_PIN): cv.positive_int,
cv.Optional(CONF_PIN_CODE): cv.string_strict, cv.Optional(CONF_PIN_CODE): cv.string_strict,
cv.Optional(CONF_USERNAME): cv.string, cv.Optional(CONF_USERNAME): cv.string,
cv.Optional(CONF_PASSWORD): cv.string, cv.Optional(CONF_PASSWORD): cv.string,
cv.Optional(CONF_USE_ADDRESS): cv.string, cv.Optional(CONF_USE_ADDRESS): cv.string,
cv.Optional(CONF_INIT_AT): cv.All(cv.ensure_list(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), ).extend(cv.COMPONENT_SCHEMA),
cv.require_framework_version( cv.require_framework_version(
@ -102,9 +109,17 @@ async def to_code(config):
for cmd in init_at: for cmd in init_at:
cg.add(var.add_init_at_command(cmd)) cg.add(var.add_init_at_command(cmd))
if modem_ready := config.get(CONF_READY, None): # if modem_ready := config.get(CONF_READY, None):
modem_ready_sensor = await cg.get_variable(modem_ready) # modem_ready_sensor = await cg.get_variable(modem_ready)
cg.add(var.set_ready_bsensor(modem_ready_sensor)) # 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_model(config[CONF_MODEL]))
cg.add(var.set_apn(config[CONF_APN])) 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_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]}"))) 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) await cg.register_component(var, config)

View file

@ -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

View file

@ -35,7 +35,20 @@ ModemComponent *global_modem_component; // NOLINT(cppcoreguidelines-avoid-non-c
return; \ 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; } ModemComponent::ModemComponent() { global_modem_component = this; }
@ -158,18 +171,8 @@ void ModemComponent::start_connect_() {
global_modem_component->got_ipv4_address_ = false; global_modem_component->got_ipv4_address_ = false;
this->dce->set_mode(modem_mode::COMMAND_MODE);
vTaskDelay(pdMS_TO_TICKS(2000)); 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 (this->dte_config_.uart_config.flow_control == ESP_MODEM_FLOW_CONTROL_HW) {
if (command_result::OK != this->dce->set_flow_control(2, 2)) { if (command_result::OK != this->dce->set_flow_control(2, 2)) {
ESP_LOGE(TAG, "Failed to set the set_flow_control mode"); 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)) { if (this->dce->set_mode(modem_mode::CMUX_MODE)) {
ESP_LOGD(TAG, "Modem has correctly entered multiplexed command/data mode"); ESP_LOGD(TAG, "Modem has correctly entered multiplexed command/data mode");
} else { } else {
ESP_LOGE(TAG, "Failed to configure multiplexed command mode... exiting"); ESP_LOGE(TAG, "Failed to configure multiplexed command mode. Trying to continue...");
return;
} }
vTaskDelay(pdMS_TO_TICKS(2000)); vTaskDelay(pdMS_TO_TICKS(2000));
@ -215,7 +217,11 @@ void ModemComponent::start_connect_() {
std::string result; std::string result;
command_result err = this->dce->at(cmd.c_str(), result, 1000); command_result err = this->dce->at(cmd.c_str(), result, 1000);
delay(100); // NOLINT 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() { void ModemComponent::loop() {
const uint32_t now = millis(); 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_) { switch (this->state_) {
case ModemComponentState::STOPPED: 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->started_) {
if (!this->modem_ready()) { if (!this->modem_ready()) {
if (now - last_log_time > 20000) { // ESP_LOGW(TAG, "Trying to recover dce");
ESP_LOGD(TAG, "Waiting for the modem to be ready..."); // ESP_ERROR_CHECK_WITHOUT_ABORT(this->dce->recover());
last_log_time = now; // 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 { } else {
ESP_LOGI(TAG, "Starting modem connection"); ESP_LOGI(TAG, "Starting modem connection");
this->state_ = ModemComponentState::CONNECTING; this->state_ = ModemComponentState::CONNECTING;
@ -260,7 +311,8 @@ void ModemComponent::loop() {
this->status_clear_warning(); this->status_clear_warning();
} else if (now - this->connect_begin_ > 45000) { } else if (now - this->connect_begin_ > 45000) {
ESP_LOGW(TAG, "Connecting via Modem failed! Re-connecting..."); ESP_LOGW(TAG, "Connecting via Modem failed! Re-connecting...");
this->start_connect_(); this->state_ = ModemComponentState::STOPPED;
// this->start_connect_();
} }
break; break;
case ModemComponentState::CONNECTED: case ModemComponentState::CONNECTED:
@ -271,6 +323,15 @@ void ModemComponent::loop() {
ESP_LOGW(TAG, "Connection via Modem lost! Re-connecting..."); ESP_LOGW(TAG, "Connection via Modem lost! Re-connecting...");
this->state_ = ModemComponentState::CONNECTING; this->state_ = ModemComponentState::CONNECTING;
this->start_connect_(); 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; break;
} }
@ -293,6 +354,50 @@ void ModemComponent::dump_connect_params_() {
ESP_LOGCONFIG(TAG, " DNS fallback: %s", network::IPAddress(dns_fallback_ip).str().c_str()); 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<unsigned char>(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<void()> &&callback) {
this->on_not_responding_callback_.add(std::move(callback));
}
} // namespace modem } // namespace modem
} // namespace esphome } // namespace esphome

View file

@ -4,8 +4,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/script/script.h"
#include "esphome/components/template/binary_sensor/template_binary_sensor.h"
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
@ -58,15 +57,20 @@ class ModemComponent : public Component {
void set_model(const std::string &model) { void set_model(const std::string &model) {
this->model_ = this->modem_model_map_.count(model) ? modem_model_map_[model] : ModemModel::UNKNOWN; 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); } 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<void()> &&callback);
std::unique_ptr<DCE> dce; std::unique_ptr<DCE> dce;
protected: protected:
gpio_num_t rx_pin_ = gpio_num_t::GPIO_NUM_NC; gpio_num_t rx_pin_ = gpio_num_t::GPIO_NUM_NC;
gpio_num_t tx_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 pin_code_;
std::string username_; std::string username_;
std::string password_; 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); 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_(); void dump_connect_params_();
std::string use_address_; std::string use_address_;
CallbackManager<void()> on_not_responding_callback_;
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)