[canbus] Bus error logging, Auto-recover bus-off on ESP32 (#10953)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Patrick
2026-10-11 09:12:28 -10:00
committed by GitHub
co-authored by J. Nick Koston J. Nick Koston
parent eb1aa2bfd6
commit 9355bdc3c5
11 changed files with 273 additions and 15 deletions
+15 -2
View File
@@ -1,14 +1,17 @@
import logging
import re
from typing import Any
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM, CONF_TRIGGER_ID
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@mvturnho", "@danielschramm"]
DOMAIN = "canbus"
IS_PLATFORM_COMPONENT = True
@@ -150,9 +153,19 @@ async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
)
async def register_canbus(var: MockObj, config: ConfigType) -> None:
async def register_canbus(
var: MockObj, config: ConfigType, *, reports_status: bool | None = None
) -> None:
if not CORE.has_id(config[CONF_ID]):
var = cg.new_Pvariable(config[CONF_ID], var)
if reports_status is None:
_LOGGER.warning(
"CAN bus platform '%s' does not report bus state, so bus-off recovery and "
"error-state logging are disabled. Platform authors should either override "
"get_status() and get_events() and call register_canbus(..., reports_status=True) "
"or pass reports_status=False if the hardware cannot report it",
config[CONF_PLATFORM],
)
await setup_canbus_core_(var, config)
+58
View File
@@ -1,5 +1,6 @@
#include "canbus.h"
#include <algorithm>
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::canbus {
@@ -65,6 +66,12 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
};
void Canbus::loop() {
uint32_t now = App.get_loop_component_start_time();
if ((now - this->last_event_check_time_) >= EVENT_CHECK_INTERVAL_MS) {
this->log_events_(this->get_events(), now);
this->last_event_check_time_ = now;
}
struct CanFrame can_message;
uint32_t message_counter = 0;
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
@@ -109,4 +116,55 @@ void Canbus::loop() {
}
}
void Canbus::log_events_(CanEventFlags events, uint32_t now) {
// Bus-off gets its own handling because automatic recovery can make it flap
if (events & CanEventFlags::CAN_EVENT_BUS_OFF) {
this->last_bus_off_time_ = now;
if (!this->bus_off_) {
this->bus_off_ = true;
this->status_set_warning(LOG_STR("Bus off"));
}
} else if (this->bus_off_) {
if ((now - this->last_bus_off_time_) >= EVENT_LOG_BUS_OFF_HOLDOFF_MS) {
// No bus-off event for the hold-off time, so ask whether it is really over
if (this->get_status().bus_off) {
this->last_bus_off_time_ = now;
} else {
this->bus_off_ = false;
this->status_clear_warning();
// Events gathered while bus-off say nothing about the recovered bus
this->events_to_log_ = 0;
}
}
} else {
this->events_to_log_ |= events;
if (this->events_to_log_ != 0 && (now - this->last_event_log_time_) >= EVENT_LOG_THROTTLE_MS) {
if (this->events_to_log_ & (CanEventFlags::CAN_EVENT_PASSIVE | CanEventFlags::CAN_EVENT_ACTIVE)) {
bool error_passive = this->events_to_log_ & CanEventFlags::CAN_EVENT_PASSIVE;
if (error_passive && (this->events_to_log_ & CanEventFlags::CAN_EVENT_ACTIVE)) {
// Both edges within one throttle window, so the counters decide the current state
auto status = this->get_status();
error_passive = status.rx_error_counter >= CAN_ERROR_PASSIVE_THRESHOLD ||
status.tx_error_counter >= CAN_ERROR_PASSIVE_THRESHOLD;
}
ESP_LOGW(TAG, "entered %s", error_passive ? LOG_STR_LITERAL("error-passive") : LOG_STR_LITERAL("error-active"));
}
if (this->events_to_log_ & CanEventFlags::CAN_EVENT_RX_QUEUE_FULL) {
ESP_LOGD(TAG, "receive buffer overrun");
}
this->events_to_log_ = 0;
this->last_event_log_time_ = now;
}
}
#ifdef ESPHOME_LOG_HAS_VERBOSE
if ((now - this->last_state_log_time_) >= STATE_LOG_INTERVAL_MS) {
auto status = this->get_status();
ESP_LOGV(TAG, "Status: bus_off %d, tx_err %d, rx_err %d", status.bus_off, status.tx_error_counter,
status.rx_error_counter);
this->last_state_log_time_ = now;
}
#endif
}
} // namespace esphome::canbus
+44
View File
@@ -48,6 +48,8 @@ template<typename... Ts> class CanbusSendAction;
/* CAN payload length definitions according to ISO 11898-1 */
static const uint8_t CAN_MAX_DATA_LENGTH = 8;
static const uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
/*
Can Frame describes a normative CAN Frame
The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used
@@ -61,6 +63,26 @@ struct CanFrame {
uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8)));
};
enum CanEventFlags : uint8_t {
CAN_EVENT_PASSIVE = 1 << 0,
CAN_EVENT_ACTIVE = 1 << 1,
CAN_EVENT_BUS_OFF = 1 << 2,
CAN_EVENT_RX_QUEUE_FULL = 1 << 3,
};
static constexpr uint32_t CAN_STATUS_UNSET = UINT32_MAX;
struct CanStatus {
bool bus_off{false};
uint8_t rx_error_counter{0};
uint8_t tx_error_counter{0};
uint32_t tx_failed_count{CAN_STATUS_UNSET};
uint32_t rx_missed_count{CAN_STATUS_UNSET};
uint32_t rx_overrun_count{CAN_STATUS_UNSET};
uint32_t arb_lost_count{CAN_STATUS_UNSET};
uint32_t bus_error_count{CAN_STATUS_UNSET};
};
class Canbus : public Component {
public:
Canbus(){};
@@ -83,6 +105,7 @@ class Canbus : public Component {
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
virtual CanStatus get_status() { return {}; }
void add_trigger(CanbusTrigger *trigger);
/**
@@ -104,12 +127,33 @@ class Canbus : public Component {
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
bool use_extended_id_{false};
uint8_t events_to_log_{0};
bool bus_off_{false};
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
// interval in which to check for new can bus events (bus-off, passive, active)
static constexpr uint32_t EVENT_CHECK_INTERVAL_MS = 100;
uint32_t last_event_check_time_{0};
// time to wait after logging an event before logging the next event(s)
static constexpr uint32_t EVENT_LOG_THROTTLE_MS = 1000;
uint32_t last_event_log_time_{0};
// time span without bus-off events before the bus is declared recovered
static constexpr uint32_t EVENT_LOG_BUS_OFF_HOLDOFF_MS = 1000;
uint32_t last_bus_off_time_{0};
#ifdef ESPHOME_LOG_HAS_VERBOSE
static constexpr uint32_t STATE_LOG_INTERVAL_MS = 1000;
uint32_t last_state_log_time_{0};
#endif
void log_events_(CanEventFlags events, uint32_t now);
virtual bool setup_internal() = 0;
virtual Error send_message(struct CanFrame *frame) = 0;
virtual Error read_message(struct CanFrame *frame) = 0;
virtual CanEventFlags get_events() { return CanEventFlags{}; }
};
template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, public Parented<Canbus> {
+1 -1
View File
@@ -129,7 +129,7 @@ async def to_code(config: ConfigType) -> None:
# Also enable esp_driver_twai for future migration to new API
include_builtin_idf_component("esp_driver_twai")
var = cg.new_Pvariable(config[CONF_ID])
await canbus.register_canbus(var, config)
await canbus.register_canbus(var, config, reports_status=True)
cg.add(var.set_rx(config[CONF_RX_PIN]))
cg.add(var.set_tx(config[CONF_TX_PIN]))
@@ -84,6 +84,8 @@ bool ESP32Can::setup_internal() {
twai_general_config_t g_config =
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, twai_mode);
g_config.controller_id = next_twai_ctrl_num++;
g_config.alerts_enabled = TWAI_ALERT_ERR_PASS | TWAI_ALERT_ERR_ACTIVE | TWAI_ALERT_BUS_OFF |
TWAI_ALERT_BUS_RECOVERED | TWAI_ALERT_RX_QUEUE_FULL;
if (this->tx_queue_len_.has_value()) {
g_config.tx_queue_len = this->tx_queue_len_.value();
}
@@ -116,6 +118,86 @@ bool ESP32Can::setup_internal() {
return true;
}
canbus::CanEventFlags ESP32Can::get_events() {
if (this->twai_handle_ == nullptr) {
return {}; // not set up yet, or setup failed
}
uint32_t alerts;
esp_err_t err = twai_read_alerts_v2(this->twai_handle_, &alerts, 0);
if (err != ESP_OK) {
if (err != ESP_ERR_TIMEOUT) {
ESP_LOGD(TAG, "failed to get CAN events");
}
alerts = 0;
}
uint8_t events = 0;
if (alerts & TWAI_ALERT_ERR_PASS) {
events |= canbus::CAN_EVENT_PASSIVE;
}
if (alerts & TWAI_ALERT_ERR_ACTIVE) {
events |= canbus::CAN_EVENT_ACTIVE;
}
if (alerts & TWAI_ALERT_BUS_OFF) {
events |= canbus::CAN_EVENT_BUS_OFF;
// Start recovery at once, as the MCP2515 does in hardware
this->recovery_pending_ = true;
}
if (alerts & TWAI_ALERT_BUS_RECOVERED) {
// Recovery leaves the controller stopped
this->restart_pending_ = true;
}
if (this->recovery_pending_ || this->restart_pending_) {
this->handle_bus_off_recovery_();
}
if (alerts & TWAI_ALERT_RX_QUEUE_FULL) {
events |= canbus::CAN_EVENT_RX_QUEUE_FULL;
}
return static_cast<canbus::CanEventFlags>(events);
}
void ESP32Can::handle_bus_off_recovery_() {
// These only fail with an invalid handle or outside the expected state; retry on the next pass
if (this->recovery_pending_) {
if (twai_initiate_recovery_v2(this->twai_handle_) != ESP_OK) {
this->status_set_error(LOG_STR("Recovery after bus off failed"));
return;
}
this->recovery_pending_ = false;
}
if (this->restart_pending_) {
if (twai_start_v2(this->twai_handle_) != ESP_OK) {
this->status_set_error(LOG_STR("Restart after bus off failed"));
return;
}
this->restart_pending_ = false;
}
this->status_clear_error();
}
canbus::CanStatus ESP32Can::get_status() {
canbus::CanStatus status = {};
twai_status_info_t twai_status;
if (this->twai_handle_ != nullptr && twai_get_status_info_v2(this->twai_handle_, &twai_status) == ESP_OK) {
status.bus_off = (twai_status.state == TWAI_STATE_BUS_OFF) || (twai_status.state == TWAI_STATE_RECOVERING);
status.rx_error_counter = twai_status.rx_error_counter;
status.tx_error_counter = twai_status.tx_error_counter;
status.tx_failed_count = twai_status.tx_failed_count;
status.rx_missed_count = twai_status.rx_missed_count;
status.rx_overrun_count = twai_status.rx_overrun_count;
status.arb_lost_count = twai_status.arb_lost_count;
status.bus_error_count = twai_status.bus_error_count;
} else {
ESP_LOGD(TAG, "failed to get CAN status");
}
return status;
}
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
// In listen-only mode, we cannot transmit
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
+5
View File
@@ -24,16 +24,21 @@ class ESP32Can final : public canbus::Canbus {
void set_tx_enqueue_timeout_ms(uint32_t tx_enqueue_timeout_ms) {
this->tx_enqueue_timeout_ticks_ = pdMS_TO_TICKS(tx_enqueue_timeout_ms);
}
canbus::CanStatus get_status() override;
ESP32Can(){};
protected:
bool setup_internal() override;
canbus::CanEventFlags get_events() override;
canbus::Error send_message(struct canbus::CanFrame *frame) override;
canbus::Error read_message(struct canbus::CanFrame *frame) override;
void handle_bus_off_recovery_();
int rx_{-1};
int tx_{-1};
CanMode mode_{CAN_MODE_NORMAL};
bool recovery_pending_{false};
bool restart_pending_{false};
TickType_t tx_enqueue_timeout_ticks_{};
optional<uint32_t> tx_queue_len_{};
optional<uint32_t> rx_queue_len_{};
+1 -1
View File
@@ -40,7 +40,7 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
async def to_code(config: ConfigType) -> None:
rhs = mcp2515.new()
var = cg.Pvariable(config[CONF_ID], rhs)
await canbus.register_canbus(var, config)
await canbus.register_canbus(var, config, reports_status=True)
if CONF_CLOCK in config:
canclock = CAN_CLOCK[config[CONF_CLOCK]]
cg.add(var.set_mcp_clock(canclock))
+42 -11
View File
@@ -43,6 +43,48 @@ bool MCP2515::setup_internal() {
return true;
}
canbus::CanEventFlags MCP2515::get_events() {
uint8_t events = 0;
uint8_t error_flags = this->get_error_flags_();
// Error-passive is one state for RX and TX together, so only a change of the combined flag is an event
bool was_passive = this->last_error_flags_ & (EFLG_RXEP | EFLG_TXEP);
bool is_passive = error_flags & (EFLG_RXEP | EFLG_TXEP);
if (was_passive != is_passive) {
events |= is_passive ? canbus::CAN_EVENT_PASSIVE : canbus::CAN_EVENT_ACTIVE;
}
if ((this->last_error_flags_ ^ error_flags) & EFLG_TXBO) {
events |= canbus::CAN_EVENT_BUS_OFF;
}
// The receive flowchart in the datasheet says that if rollover is set (BUKT), RX1OVR flag will be set
// once both buffers are full. However, the RX0OVR flag is actually set instead.
// We can just check for both though because it doesn't break anything.
if (error_flags & (EFLG_RX0OVR | EFLG_RX1OVR)) {
events |= canbus::CAN_EVENT_RX_QUEUE_FULL;
this->clear_rx_n_ovr_flags_();
}
this->last_error_flags_ = error_flags;
return static_cast<canbus::CanEventFlags>(events);
}
canbus::CanStatus MCP2515::get_status() {
canbus::CanStatus status = {};
uint8_t error_flags = this->get_error_flags_();
status.bus_off = error_flags & EFLG_TXBO;
// Transmit Error Counter is followed by Receive Error Counter so we're reading both at once
uint8_t err_counters[2];
this->read_registers_(MCP_TEC, err_counters, 2);
status.rx_error_counter = err_counters[1];
status.tx_error_counter = err_counters[0];
return status;
}
canbus::Error MCP2515::reset_() {
this->enable();
this->transfer_byte(INSTRUCTION_RESET);
@@ -331,17 +373,6 @@ canbus::Error MCP2515::read_message(struct canbus::CanFrame *frame) {
rc = canbus::ERROR_NOMSG;
}
#ifdef ESPHOME_LOG_HAS_DEBUG
uint8_t err = get_error_flags_();
// The receive flowchart in the datasheet says that if rollover is set (BUKT), RX1OVR flag will be set
// once both buffers are full. However, the RX0OVR flag is actually set instead.
// We can just check for both though because it doesn't break anything.
if (err & (EFLG_RX0OVR | EFLG_RX1OVR)) {
ESP_LOGD(TAG, "receive buffer overrun");
clear_rx_n_ovr_flags_();
}
#endif
return rc;
}
+5
View File
@@ -58,6 +58,7 @@ class MCP2515 final : public canbus::Canbus,
MCP2515(){};
void set_mcp_clock(CanClock clock) { this->mcp_clock_ = clock; };
void set_mcp_mode(const CanctrlReqopMode mode) { this->mcp_mode_ = mode; }
canbus::CanStatus get_status() override;
static const struct TxBnRegs {
REGISTER CTRL;
REGISTER SIDH;
@@ -74,7 +75,11 @@ class MCP2515 final : public canbus::Canbus,
protected:
CanClock mcp_clock_{MCP_8MHZ};
CanctrlReqopMode mcp_mode_ = CANCTRL_REQOP_NORMAL;
uint8_t last_error_flags_{0};
bool setup_internal() override;
canbus::CanEventFlags get_events() override;
canbus::Error set_mode_(CanctrlReqopMode mode);
uint8_t read_register_(REGISTER reg);
+9
View File
@@ -44,3 +44,12 @@ canbus:
break;
// to be continued...
}
interval:
- interval: 1s
then:
- lambda: |-
auto status = id(esp32_can_esp32_internal_can).get_status();
ESP_LOGD("canbus_status", "bus_off: %d, tx_err: %d, rx_err: %d, bus_err: %" PRIu32 ", arb_lost: %" PRIu32,
status.bus_off, status.tx_error_counter, status.rx_error_counter,
status.bus_error_count, status.arb_lost_count);
+11
View File
@@ -42,6 +42,17 @@ canbus:
// to be continued...
}
interval:
- interval: 1s
then:
- lambda: |-
auto status = id(mcp2515_can).get_status();
ESP_LOGD("canbus_status", "bus_off: %d, tx_err: %d, rx_err: %d",
status.bus_off,
status.tx_error_counter,
status.rx_error_counter
);
button:
- platform: template
name: MCP2515 Send