diff --git a/esphome/components/canbus/packet_transport/__init__.py b/esphome/components/canbus/packet_transport/__init__.py new file mode 100644 index 0000000000..d361bf2f8b --- /dev/null +++ b/esphome/components/canbus/packet_transport/__init__.py @@ -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])) diff --git a/esphome/components/canbus/packet_transport/canbus_transport.cpp b/esphome/components/canbus/packet_transport/canbus_transport.cpp new file mode 100644 index 0000000000..bada52357a --- /dev/null +++ b/esphome/components/canbus/packet_transport/canbus_transport.cpp @@ -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 &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 &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 &buf) const { + if (buf.empty()) + return; + + size_t offset = 0; + uint8_t sequence = 0; + + while (offset < buf.size()) { + std::vector 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(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 diff --git a/esphome/components/canbus/packet_transport/canbus_transport.h b/esphome/components/canbus/packet_transport/canbus_transport.h new file mode 100644 index 0000000000..5d4f454949 --- /dev/null +++ b/esphome/components/canbus/packet_transport/canbus_transport.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/packet_transport/packet_transport.h" +#include "../canbus.h" +#include + +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 { + 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 &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 &data); + + uint32_t can_id_{0x600}; // Default CAN ID for packet transport + bool use_extended_id_{false}; + std::vector receive_buffer_{}; + uint8_t expected_sequence_{0}; + bool receiving_{false}; +}; + +} // namespace canbus +} // namespace esphome diff --git a/tests/components/canbus/packet_transport/common.yaml b/tests/components/canbus/packet_transport/common.yaml new file mode 100644 index 0000000000..72504795e6 --- /dev/null +++ b/tests/components/canbus/packet_transport/common.yaml @@ -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" diff --git a/tests/components/canbus/packet_transport/test.esp32-c3-idf.yaml b/tests/components/canbus/packet_transport/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..54a57c1562 --- /dev/null +++ b/tests/components/canbus/packet_transport/test.esp32-c3-idf.yaml @@ -0,0 +1,4 @@ +packages: + base: !include ../../test_build_components/base.yaml + +<<: !include common.yaml diff --git a/tests/components/canbus/packet_transport/test.esp32-idf.yaml b/tests/components/canbus/packet_transport/test.esp32-idf.yaml new file mode 100644 index 0000000000..54a57c1562 --- /dev/null +++ b/tests/components/canbus/packet_transport/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + base: !include ../../test_build_components/base.yaml + +<<: !include common.yaml