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