1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-18 07:45:56 +00:00

[canbus] Add packet_transport support for CAN bus

Implements native CANBus transport for the packet_transport component,
enabling ESPHome nodes to share sensor and device data over physical
CAN bus networks.

Key features:
- Packet fragmentation to handle CAN's 8-byte frame limit
- Uses 7 bytes per frame for payload (1 byte for sequence/flags)
- Configurable CAN ID (default 0x600) and extended ID support
- Sequence tracking with error detection and recovery
- Compatible with all packet_transport features (encryption, rolling codes, ping-pong)

Use cases:
- Marine applications with existing CAN bus networks
- Automotive battery management and motor controller data sharing
- Industrial automation sensor networks

Configuration example:
```yaml
canbus:
  - platform: esp32_can
    id: my_can
    tx_pin: GPIO5
    rx_pin: GPIO4
    can_id: 4
    bit_rate: 125kbps

packet_transport:
  platform: canbus
  canbus_id: my_can
  can_id: 0x600
  sensors:
    - my_sensor
  providers:
    - name: remote-device
      encryption: "encryption key"
```

Implements: https://github.com/orgs/esphome/discussions/3255
This commit is contained in:
Claude
2025-11-17 21:41:57 +00:00
parent 3d6c361037
commit 77242c65ed
6 changed files with 279 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components.packet_transport import (
PacketTransport,
new_packet_transport,
transport_schema,
)
from esphome.const import CONF_ID
from esphome.cpp_types import PollingComponent
from .. import CONF_CANBUS_ID, CONF_CAN_ID, CONF_USE_EXTENDED_ID, canbus_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["canbus"]
Canbus = canbus_ns.class_("Canbus", cg.Component)
CanbusTransport = canbus_ns.class_("CanbusTransport", PacketTransport, PollingComponent)
CONFIG_SCHEMA = transport_schema(CanbusTransport).extend(
{
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(Canbus),
cv.Optional(CONF_CAN_ID, default=0x600): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
}
)
async def to_code(config):
var, _ = await new_packet_transport(config)
canbus_var = await cg.get_variable(config[CONF_CANBUS_ID])
cg.add(var.set_parent(canbus_var))
cg.add(var.set_can_id(config[CONF_CAN_ID]))
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))

View File

@@ -0,0 +1,134 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "canbus_transport.h"
namespace esphome {
namespace canbus {
static const char *const TAG = "canbus_transport";
void CanbusTransport::setup() {
PacketTransport::setup();
// Register callback to receive CAN frames
this->parent_->add_callback(
[this](uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data) {
this->handle_can_frame_(can_id, extended_id, rtr, data);
});
ESP_LOGCONFIG(TAG, "CAN packet transport using CAN ID 0x%03X%s", this->can_id_,
this->use_extended_id_ ? " (extended)" : "");
}
void CanbusTransport::loop() { PacketTransport::loop(); }
void CanbusTransport::update() {
this->updated_ = true;
this->resend_data_ = true;
PacketTransport::update();
}
void CanbusTransport::handle_can_frame_(uint32_t can_id, bool extended_id, bool rtr,
const std::vector<uint8_t> &data) {
// Ignore frames not for us
if (can_id != this->can_id_ || extended_id != this->use_extended_id_ || rtr)
return;
// Need at least 1 byte (header)
if (data.empty()) {
ESP_LOGW(TAG, "Received empty CAN frame");
return;
}
uint8_t header = data[0];
uint8_t sequence = header & SEQUENCE_MASK;
bool is_last = (header & LAST_FRAME_FLAG) != 0;
// Check for sequence errors
if (this->receiving_ && sequence != this->expected_sequence_) {
ESP_LOGD(TAG, "Sequence error: expected %d, got %d. Resetting.", this->expected_sequence_, sequence);
this->receive_buffer_.clear();
this->receiving_ = false;
this->expected_sequence_ = 0;
}
// Start of new packet
if (!this->receiving_ && sequence == 0) {
this->receiving_ = true;
this->receive_buffer_.clear();
}
if (!this->receiving_)
return;
// Append payload data (skip header byte)
for (size_t i = 1; i < data.size(); i++) {
if (this->receive_buffer_.size() >= MAX_PACKET_SIZE) {
ESP_LOGD(TAG, "Packet too large, discarding");
this->receive_buffer_.clear();
this->receiving_ = false;
this->expected_sequence_ = 0;
return;
}
this->receive_buffer_.push_back(data[i]);
}
if (is_last) {
// Complete packet received
ESP_LOGV(TAG, "Received complete packet: %zu bytes", this->receive_buffer_.size());
this->process_(this->receive_buffer_);
this->receive_buffer_.clear();
this->receiving_ = false;
this->expected_sequence_ = 0;
} else {
// Expect next sequence
this->expected_sequence_ = (sequence + 1) & SEQUENCE_MASK;
}
}
void CanbusTransport::send_packet(const std::vector<uint8_t> &buf) const {
if (buf.empty())
return;
size_t offset = 0;
uint8_t sequence = 0;
while (offset < buf.size()) {
std::vector<uint8_t> frame_data;
frame_data.reserve(CAN_MAX_DATA_LENGTH);
// Calculate how many bytes we can send in this frame
size_t remaining = buf.size() - offset;
size_t payload_size = std::min(remaining, static_cast<size_t>(PAYLOAD_BYTES_PER_FRAME));
bool is_last = (offset + payload_size >= buf.size());
// Build header byte
uint8_t header = sequence & SEQUENCE_MASK;
if (is_last)
header |= LAST_FRAME_FLAG;
frame_data.push_back(header);
// Add payload
for (size_t i = 0; i < payload_size; i++) {
frame_data.push_back(buf[offset + i]);
}
// Send CAN frame
auto result = this->parent_->send_data(this->can_id_, this->use_extended_id_, false, frame_data);
if (result != Error::ERROR_OK) {
ESP_LOGW(TAG, "Failed to send CAN frame (sequence %d)", sequence);
}
offset += payload_size;
sequence = (sequence + 1) & SEQUENCE_MASK;
// Small delay between frames to avoid overwhelming the bus
if (!is_last) {
delayMicroseconds(500); // 500us between frames
}
}
ESP_LOGV(TAG, "Sent packet: %zu bytes in %d frames", buf.size(), sequence);
}
} // namespace canbus
} // namespace esphome

View File

@@ -0,0 +1,51 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/packet_transport/packet_transport.h"
#include "../canbus.h"
#include <vector>
namespace esphome {
namespace canbus {
/**
* A transport protocol for sending and receiving packets over a CAN bus.
*
* Since CAN frames are limited to 8 bytes, packets are fragmented across multiple frames.
* Frame format:
* Byte 0: Sequence number and flags (bit 7 = last frame, bits 0-6 = sequence 0-127)
* Bytes 1-7: Payload data (7 bytes per frame)
*
* A dedicated CAN ID is used for packet transport frames.
*/
static const uint16_t MAX_PACKET_SIZE = 508; // Match UART transport limit
static const uint8_t PAYLOAD_BYTES_PER_FRAME = 7; // 1 byte for header, 7 for data
static const uint8_t LAST_FRAME_FLAG = 0x80;
static const uint8_t SEQUENCE_MASK = 0x7F;
class CanbusTransport : public packet_transport::PacketTransport, public Parented<Canbus> {
public:
void setup() override;
void loop() override;
void update() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; }
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
void handle_can_frame_(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data);
uint32_t can_id_{0x600}; // Default CAN ID for packet transport
bool use_extended_id_{false};
std::vector<uint8_t> receive_buffer_{};
uint8_t expected_sequence_{0};
bool receiving_{false};
};
} // namespace canbus
} // namespace esphome

View File

@@ -0,0 +1,53 @@
canbus:
- platform: esp32_can
id: esp32_internal_can
rx_pin: GPIO4
tx_pin: GPIO5
can_id: 4
bit_rate: 125kbps
packet_transport:
platform: canbus
canbus_id: esp32_internal_can
can_id: 0x600
use_extended_id: false
update_interval: 5s
encryption: "test encryption key"
rolling_code_enable: true
ping_pong_enable: true
binary_sensors:
- binary_sensor_id1
- id: binary_sensor_id1
broadcast_id: other_id
sensors:
- sensor_id1
- id: sensor_id1
broadcast_id: other_id
providers:
- name: remote-device
encryption: "remote device key"
sensor:
- platform: template
id: sensor_id1
name: "Local Sensor"
- platform: packet_transport
provider: remote-device
id: remote_sensor_id
remote_id: some_sensor_id
name: "Remote Sensor"
internal: true
binary_sensor:
- platform: packet_transport
provider: unencrypted-device
id: other_binary_sensor_id
name: "Other Binary Sensor"
internal: true
- platform: packet_transport
provider: remote-device
type: status
name: "Remote Device Status"
- platform: template
id: binary_sensor_id1
name: "Local Binary Sensor"

View File

@@ -0,0 +1,4 @@
packages:
base: !include ../../test_build_components/base.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
base: !include ../../test_build_components/base.yaml
<<: !include common.yaml