Build static value PDUs in shared functions so each action sends once

This commit is contained in:
J. Nick Koston
2026-10-08 08:20:27 -10:00
parent 23c77f71b7
commit 120b9de474
2 changed files with 65 additions and 45 deletions
@@ -0,0 +1,46 @@
#include "modbus_client.h"
namespace esphome::modbus_client {
using modbus::helpers::PduBuffer;
#ifdef USE_ESP8266
PduBuffer static_write_registers_pdu(uint16_t start, const uint16_t *values, size_t len) {
std::array<uint16_t, modbus::MAX_NUM_OF_REGISTERS_TO_WRITE> staging;
progmem_memcpy(staging.data(), values, len * sizeof(uint16_t));
return modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(staging.data(), len));
}
PduBuffer static_write_coils_pdu(uint16_t start, const uint8_t *packed, uint16_t count) {
std::array<uint8_t, modbus::packed_bit_bytes(modbus::MAX_NUM_OF_COILS_TO_WRITE)> staging;
const size_t len = modbus::packed_bit_bytes(count);
progmem_memcpy(staging.data(), packed, len);
return modbus::helpers::create_write_coils_pdu(
start, modbus::PackedBits(std::span<const uint8_t>(staging.data(), len), count));
}
PduBuffer static_read_write_registers_pdu(uint16_t read_start, uint16_t read_count, uint16_t write_start,
const uint16_t *values, size_t len) {
std::array<uint16_t, modbus::MAX_NUM_OF_REGISTERS_TO_WRITE_RW> staging;
progmem_memcpy(staging.data(), values, len * sizeof(uint16_t));
return modbus::helpers::create_read_write_multiple_registers_pdu(read_start, read_count, write_start,
std::span<const uint16_t>(staging.data(), len));
}
#else
PduBuffer static_write_registers_pdu(uint16_t start, const uint16_t *values, size_t len) {
return modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(values, len));
}
PduBuffer static_write_coils_pdu(uint16_t start, const uint8_t *packed, uint16_t count) {
return modbus::helpers::create_write_coils_pdu(
start, modbus::PackedBits(std::span<const uint8_t>(packed, modbus::packed_bit_bytes(count)), count));
}
PduBuffer static_read_write_registers_pdu(uint16_t read_start, uint16_t read_count, uint16_t write_start,
const uint16_t *values, size_t len) {
return modbus::helpers::create_read_write_multiple_registers_pdu(read_start, read_count, write_start,
std::span<const uint16_t>(values, len));
}
#endif
} // namespace esphome::modbus_client
@@ -153,17 +153,12 @@ class ModbusClientSendAction : public ClientActionBase<Ts...>,
Trigger<std::span<const uint8_t>, std::span<const uint8_t>> response_trigger_;
};
/// Calls fn with a RAM-readable span of a static values table. On ESP8266 the table is in PROGMEM, which the
/// PDU builders cannot read directly, so it is copied to the stack first; the schema caps len at MAX.
template<size_t MAX, typename T, typename F> void with_static_values(const T *data, size_t len, F &&fn) {
#ifdef USE_ESP8266
std::array<T, MAX> buf;
progmem_memcpy(buf.data(), data, len * sizeof(T));
fn(std::span<const T>(buf.data(), len));
#else
fn(std::span<const T>(data, len));
#endif
}
// PDU builders for the static values tables, shared by every action instantiation. On ESP8266 the tables
// are in PROGMEM, which the builders cannot read directly, so these copy them to the stack first.
modbus::helpers::PduBuffer static_write_registers_pdu(uint16_t start, const uint16_t *values, size_t len);
modbus::helpers::PduBuffer static_write_coils_pdu(uint16_t start, const uint8_t *packed, uint16_t count);
modbus::helpers::PduBuffer static_read_write_registers_pdu(uint16_t read_start, uint16_t read_count,
uint16_t write_start, const uint16_t *values, size_t len);
/// Typed actions: these do NOT override the raw on_response, so the base ModbusClientDevice default runs
/// the shared dispatch (validation gate + decode) and the typed callbacks below fire directly on the
@@ -330,16 +325,9 @@ class WriteMultipleRegistersAction : public TypedClientActionBase<Ts...>, public
const uint16_t start = this->start_address_.value(x...);
// An empty or over-long set rejects into an empty PDU inside the builder, which logs the reason;
// the empty PDU then resolves via on_not_sent like any refused send.
if (this->len_ >= 0) {
with_static_values<modbus::MAX_NUM_OF_REGISTERS_TO_WRITE>(
this->values_.data, static_cast<size_t>(this->len_), [&](std::span<const uint16_t> values) {
this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, values),
this->write_command_options_(x...));
});
return;
}
const std::vector<uint16_t> values = this->values_.func(x...);
this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span<const uint16_t>(values)),
this->send_or_resolve_(this->len_ >= 0
? static_write_registers_pdu(start, this->values_.data, static_cast<size_t>(this->len_))
: modbus::helpers::create_write_registers_pdu(start, this->values_.func(x...)),
this->write_command_options_(x...));
}
void on_write_multiple_registers(uint16_t start_address, std::span<const uint16_t> registers,
@@ -381,18 +369,11 @@ class WriteMultipleCoilsAction : public TypedClientActionBase<Ts...>, public Wri
void play(const Ts &...x) override {
const uint16_t start = this->start_address_.value(x...);
if (this->count_ >= 0) {
const auto count = static_cast<uint16_t>(this->count_);
with_static_values<modbus::packed_bit_bytes(modbus::MAX_NUM_OF_COILS_TO_WRITE)>(
this->values_.packed, modbus::packed_bit_bytes(count), [&](std::span<const uint8_t> packed) {
this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, modbus::PackedBits(packed, count)),
this->write_command_options_(x...));
});
return;
}
// The builder packs and bound-checks; an over-long set is rejected and logged there.
this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...)),
this->write_command_options_(x...));
// For a lambda the builder packs and bound-checks; an over-long set is rejected and logged there.
this->send_or_resolve_(
this->count_ >= 0 ? static_write_coils_pdu(start, this->values_.packed, static_cast<uint16_t>(this->count_))
: modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...)),
this->write_command_options_(x...));
}
void on_write_multiple_coils(uint16_t start_address, modbus::PackedBits bits,
modbus::ResponseStatus status) override {
@@ -436,18 +417,11 @@ class ReadWriteMultipleRegistersAction : public TypedClientActionBase<Ts...>, pu
const uint16_t read_count = this->read_count_.value(x...);
const uint16_t write_start = this->write_address_.value(x...);
// An out-of-range read/write count builds an empty PDU (the builder logs why), resolving via on_not_sent.
if (this->len_ >= 0) {
with_static_values<modbus::MAX_NUM_OF_REGISTERS_TO_WRITE_RW>(
this->values_.data, static_cast<size_t>(this->len_), [&](std::span<const uint16_t> values) {
this->send_or_resolve_(
modbus::helpers::create_read_write_multiple_registers_pdu(read_start, read_count, write_start, values),
this->command_options_(x...));
});
return;
}
const std::vector<uint16_t> values = this->values_.func(x...);
this->send_or_resolve_(modbus::helpers::create_read_write_multiple_registers_pdu(
read_start, read_count, write_start, std::span<const uint16_t>(values)),
this->send_or_resolve_(this->len_ >= 0
? static_read_write_registers_pdu(read_start, read_count, write_start,
this->values_.data, static_cast<size_t>(this->len_))
: modbus::helpers::create_read_write_multiple_registers_pdu(
read_start, read_count, write_start, this->values_.func(x...)),
this->command_options_(x...));
}
// The 0x17 response carries only the read block, so the hub dispatch delivers it as a holding-register read.