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:
33
esphome/components/canbus/packet_transport/__init__.py
Normal file
33
esphome/components/canbus/packet_transport/__init__.py
Normal 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]))
|
||||
134
esphome/components/canbus/packet_transport/canbus_transport.cpp
Normal file
134
esphome/components/canbus/packet_transport/canbus_transport.cpp
Normal 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
|
||||
@@ -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
|
||||
53
tests/components/canbus/packet_transport/common.yaml
Normal file
53
tests/components/canbus/packet_transport/common.yaml
Normal 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"
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
base: !include ../../test_build_components/base.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
base: !include ../../test_build_components/base.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user