mirror of
https://github.com/esphome/esphome.git
synced 2026-10-08 20:06:03 +00:00
[canbus] Keep constant send payloads in shared flash tables (#20309)
This commit is contained in:
@@ -144,20 +144,26 @@ async def register_canbus(var: MockObj, config: ConfigType) -> None:
|
||||
|
||||
|
||||
# Actions
|
||||
CANBUS_SEND_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
|
||||
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
|
||||
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
|
||||
# One classic CAN frame
|
||||
cv.Required(CONF_DATA): cv.templatable(
|
||||
cv.All(validate_raw_data, cv.Length(max=8))
|
||||
),
|
||||
},
|
||||
validate_id,
|
||||
key=CONF_DATA,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"canbus.send",
|
||||
canbus_ns.class_("CanbusSendAction", automation.Action),
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
|
||||
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
|
||||
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
|
||||
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
validate_id,
|
||||
key=CONF_DATA,
|
||||
),
|
||||
CANBUS_SEND_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def canbus_action_to_code(
|
||||
@@ -177,15 +183,11 @@ async def canbus_action_to_code(
|
||||
var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST])
|
||||
)
|
||||
|
||||
data = config[CONF_DATA]
|
||||
if cg.is_template(data):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
if isinstance(data, bytes):
|
||||
data = [int(x) for x in data]
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
await automation.templatable_bytes(
|
||||
config[CONF_DATA],
|
||||
args,
|
||||
var.set_data_template,
|
||||
var.set_data_static,
|
||||
"canbus_data",
|
||||
)
|
||||
return var
|
||||
|
||||
@@ -22,10 +22,11 @@ void Canbus::dump_config() {
|
||||
}
|
||||
|
||||
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
|
||||
const std::vector<uint8_t> &data) {
|
||||
const uint8_t *data, size_t len) {
|
||||
struct CanFrame can_message;
|
||||
|
||||
uint8_t size = static_cast<uint8_t>(data.size());
|
||||
// Clamp before narrowing, so a lambda payload of 256 bytes or more cannot wrap to a short length
|
||||
const uint8_t size = static_cast<uint8_t>(std::min<size_t>(len, CAN_MAX_DATA_LENGTH));
|
||||
if (use_extended_id) {
|
||||
ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
|
||||
size);
|
||||
@@ -33,8 +34,6 @@ canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remo
|
||||
ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
|
||||
size);
|
||||
}
|
||||
if (size > CAN_MAX_DATA_LENGTH)
|
||||
size = CAN_MAX_DATA_LENGTH;
|
||||
can_message.can_data_length_code = size;
|
||||
can_message.can_id = can_id;
|
||||
can_message.use_extended_id = use_extended_id;
|
||||
|
||||
@@ -69,8 +69,12 @@ class Canbus : public Component {
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
void loop() override;
|
||||
|
||||
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, const uint8_t *data,
|
||||
size_t len);
|
||||
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
|
||||
const std::vector<uint8_t> &data);
|
||||
const std::vector<uint8_t> &data) {
|
||||
return this->send_data(can_id, use_extended_id, remote_transmission_request, data.data(), data.size());
|
||||
}
|
||||
canbus::Error send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
|
||||
// for backwards compatibility only
|
||||
return this->send_data(can_id, use_extended_id, false, data);
|
||||
@@ -107,18 +111,7 @@ class Canbus : public Component {
|
||||
};
|
||||
|
||||
template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, public Parented<Canbus> {
|
||||
public:
|
||||
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
|
||||
this->data_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
TEMPLATABLE_BYTES(data)
|
||||
|
||||
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
|
||||
|
||||
@@ -132,26 +125,17 @@ template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, pu
|
||||
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
|
||||
auto use_extended_id =
|
||||
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
|
||||
std::vector<uint8_t> data;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
data.assign(this->data_.data, this->data_.data + this->len_);
|
||||
} else {
|
||||
// Template mode: call function
|
||||
data = this->data_.func(x...);
|
||||
}
|
||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data);
|
||||
this->data_.template visit<CAN_MAX_DATA_LENGTH>(
|
||||
[&](const uint8_t *data, size_t len) {
|
||||
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len);
|
||||
},
|
||||
x...);
|
||||
}
|
||||
|
||||
protected:
|
||||
optional<uint32_t> can_id_{};
|
||||
optional<bool> use_extended_id_{};
|
||||
bool remote_transmission_request_{false};
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} data_;
|
||||
};
|
||||
|
||||
class CanbusTrigger final : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
- canbus.send: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
|
||||
- canbus.send:
|
||||
can_id: 0x100
|
||||
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
|
||||
- canbus.send: !lambda return {0x09};
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
canbus:
|
||||
- platform: esp32_can
|
||||
id: can_bus
|
||||
rx_pin: 4
|
||||
tx_pin: 5
|
||||
can_id: 4
|
||||
bit_rate: 50kbps
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Tests for canbus constant payloads in shared PROGMEM tables."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.canbus import CANBUS_SEND_ACTION_SCHEMA
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
def test_constant_payloads_share_progmem_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Equal send payloads share one table; lambdas stay templates."""
|
||||
main_cpp = generate_main(component_config_path("payload_tables.yaml"))
|
||||
|
||||
tables = re.findall(
|
||||
r"static constexpr uint8_t (\w+)\[\] PROGMEM = (\{[^}]*\});", main_cpp
|
||||
)
|
||||
assert [v for _, v in tables] == [
|
||||
"{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}"
|
||||
]
|
||||
assert main_cpp.count(f"set_data_static({tables[0][0]}, 8);") == 2
|
||||
assert "set_data_template(" in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [8, 9])
|
||||
@pytest.mark.parametrize("make", [lambda n: [0x01] * n, lambda n: "a" * n])
|
||||
def test_send_one_frame_limit(
|
||||
size: int, make: Callable[[int], list[int] | str]
|
||||
) -> None:
|
||||
"""Payloads up to one 8 byte CAN frame are accepted, longer ones rejected."""
|
||||
config = {"canbus_id": "can_bus", "data": make(size)}
|
||||
if size <= 8:
|
||||
assert len(CANBUS_SEND_ACTION_SCHEMA(config)["data"]) == size
|
||||
else:
|
||||
with pytest.raises(cv.Invalid, match="at most 8"):
|
||||
CANBUS_SEND_ACTION_SCHEMA(config)
|
||||
Reference in New Issue
Block a user