mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 17:30:22 +00:00
[modbus] Heap-free response path (#17377)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Claude Fable 5
J. Nick Koston
J. Nick Koston
parent
d12300679e
commit
5e2d428e69
@@ -35,7 +35,8 @@ void GrowattSolar::update() {
|
||||
this->last_send_ = millis();
|
||||
}
|
||||
|
||||
void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
// Other components might be sending commands to our device. But we don't get called with enough
|
||||
// context to know what is what. So if we didn't do a send, we ignore the data.
|
||||
if (!this->last_send_)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace esphome::growatt_solar {
|
||||
|
||||
@@ -69,7 +69,7 @@ class GrowattSolar final : public PollingComponent, public modbus::ModbusClientD
|
||||
public:
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; }
|
||||
|
||||
@@ -10,7 +10,8 @@ static const char *const TAG = "havells_solar";
|
||||
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x03;
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers
|
||||
|
||||
void HavellsSolar::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void HavellsSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
|
||||
ESP_LOGW(TAG, "Invalid size for HavellsSolar!");
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace esphome::havells_solar {
|
||||
|
||||
@@ -77,7 +77,7 @@ class HavellsSolar final : public PollingComponent, public modbus::ModbusClientD
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
|
||||
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void Kuntze::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
|
||||
|
||||
this->waiting_ = false;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
namespace esphome::kuntze {
|
||||
|
||||
class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice {
|
||||
@@ -19,7 +21,7 @@ class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice
|
||||
void loop() override;
|
||||
void update() override;
|
||||
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -214,11 +214,10 @@ bool Modbus::parse_modbus_server_frame_() {
|
||||
|
||||
// Process before clearing: process_modbus_server_frame (receiving a response or peer message) never sends a reply
|
||||
// synchronously. We can safely point directly into rx_buffer_ and avoid a copy.
|
||||
uint8_t data_offset = helpers::server_frame_data_offset(this->rx_buffer_.data(), this->rx_buffer_.size());
|
||||
const uint8_t *data = this->rx_buffer_.data() + data_offset;
|
||||
uint16_t data_len = frame_length - 2 - data_offset;
|
||||
// The PDU is the frame without the leading address and the trailing CRC.
|
||||
std::span<const uint8_t> pdu(this->rx_buffer_.data() + 1, frame_length - 3);
|
||||
|
||||
this->process_modbus_server_frame(address, function_code, data, data_len);
|
||||
this->process_modbus_server_frame(address, pdu);
|
||||
this->clear_rx_buffer_(LOG_STR("parse succeeded"), false, frame_length);
|
||||
|
||||
return true;
|
||||
@@ -258,8 +257,16 @@ bool ModbusServerHub::parse_modbus_client_frame_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data,
|
||||
uint16_t len) {
|
||||
// Bounds contract, enforced by the parser (parse_modbus_server_frame_) rather than locally:
|
||||
// - pdu is never empty: helpers::server_frame_length() returns at least MIN_FRAME_SIZE (4) on every
|
||||
// branch, and find_custom_frame_end_() only ever lengthens that, so the PDU (frame minus address
|
||||
// and CRC) always holds at least the function code.
|
||||
// - When the exception bit is set, pdu has at least 2 bytes: server_frame_length() checks the
|
||||
// exception bit before anything else and pins those frames to 5 bytes, so the exception code
|
||||
// read below is always present.
|
||||
// Keep those guarantees in mind when changing server_frame_length() or adding callers.
|
||||
void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) {
|
||||
const uint8_t function_code = pdu[0];
|
||||
if (!this->waiting_for_response_.has_value()) {
|
||||
ESP_LOGW(TAG,
|
||||
"Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "ms after last send",
|
||||
@@ -292,20 +299,23 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t funct
|
||||
return;
|
||||
} else { // We have a valid device waiting for this response
|
||||
|
||||
ModbusClientDevice *device = wfr.device;
|
||||
// Move the command out of the waiting slot so the request PDU stays alive for the callback.
|
||||
ModbusDeviceCommand command = std::move(this->waiting_for_response_.value());
|
||||
this->waiting_for_response_.reset();
|
||||
ModbusClientDevice *device = command.device;
|
||||
// The request PDU is the sent frame without the leading address and the trailing CRC.
|
||||
std::span<const uint8_t> request_pdu(command.frame.data.data() + 1, command.frame.size() - 3);
|
||||
// Is it an error response?
|
||||
if (helpers::is_function_code_exception(function_code)) {
|
||||
uint8_t exception = len > 0 ? data[0] : 0;
|
||||
uint8_t exception = pdu[1]; // exception frames are fixed-length, so the code is always present
|
||||
ESP_LOGW(TAG,
|
||||
"Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send",
|
||||
function_code, exception, address, this->last_modbus_byte_ - this->last_send_);
|
||||
if (device)
|
||||
device->on_modbus_error(function_code & FUNCTION_CODE_MASK, exception);
|
||||
device->on_error(request_pdu, static_cast<ModbusExceptionCode>(exception));
|
||||
|
||||
} else if (device) { // Not an error response
|
||||
// on_modbus_data is existing public API taking const std::vector<uint8_t>&
|
||||
device->on_modbus_data(std::vector<uint8_t>(data, data + len));
|
||||
device->on_response(request_pdu, pdu);
|
||||
} else { // Not an error response, but no device to respond to
|
||||
ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send",
|
||||
address, this->last_modbus_byte_ - this->last_send_);
|
||||
@@ -314,7 +324,7 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, uint8_t funct
|
||||
}
|
||||
}
|
||||
|
||||
void ModbusServerHub::process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *, uint16_t) {
|
||||
void ModbusServerHub::process_modbus_server_frame(uint8_t address, std::span<const uint8_t>) {
|
||||
if (this->find_device_(address) != nullptr) {
|
||||
ESP_LOGE(TAG, "Unexpected response from address %" PRIu8 ", which is mapped to this device.", address);
|
||||
}
|
||||
@@ -503,7 +513,7 @@ void ModbusClientHub::send_next_frame_() {
|
||||
this->waiting_for_response_ = std::move(command);
|
||||
} else {
|
||||
if (command.device)
|
||||
command.device->on_modbus_not_sent();
|
||||
command.device->on_not_sent();
|
||||
}
|
||||
|
||||
this->tx_buffer_.pop_front();
|
||||
@@ -561,11 +571,10 @@ void ModbusServerHub::send_exception_(uint8_t address, uint8_t function_code, Mo
|
||||
this->send_raw_(raw_frame, 3);
|
||||
}
|
||||
|
||||
// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload.
|
||||
void ModbusClientHub::notify_no_response_(ModbusDeviceCommand &wfr) {
|
||||
if (wfr.device == nullptr)
|
||||
return;
|
||||
const bool retry = wfr.device->on_modbus_no_response();
|
||||
const bool retry = wfr.device->on_no_response();
|
||||
// The callback may have detached the device (e.g. clear_tx_queue_for_device()); honor the detach
|
||||
// over the retry request rather than re-queueing a frame that can no longer be routed.
|
||||
if (retry && wfr.device != nullptr)
|
||||
@@ -579,17 +588,18 @@ void ModbusClientHub::requeue_waiting_frame_(ModbusDeviceCommand &wfr) {
|
||||
if (this->tx_buffer_.size() >= MODBUS_TX_BUFFER_SIZE) {
|
||||
ESP_LOGE(TAG, "Write buffer full, dropped retry for address %" PRIu8, frame.data.data()[0]);
|
||||
if (wfr.device != nullptr)
|
||||
wfr.device->on_modbus_not_sent();
|
||||
wfr.device->on_not_sent();
|
||||
return;
|
||||
}
|
||||
// Re-queue a copy (not a move): the waiting entry may have to survive as an interrupted shell.
|
||||
this->tx_buffer_.emplace_back(wfr.device, frame.data.data()[0], frame.data.data() + 1, frame.size() - 3);
|
||||
}
|
||||
|
||||
// Raw send for client: pushes to tx queue. Everything except the CRC must be contained in payload.
|
||||
void ModbusClientHub::queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device) {
|
||||
if (pdu_len == 0) {
|
||||
if (device)
|
||||
device->on_modbus_not_sent();
|
||||
device->on_not_sent();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -605,7 +615,7 @@ void ModbusClientHub::queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t p
|
||||
#endif
|
||||
ESP_LOGE(TAG, "Write buffer full, dropped: %" PRIu8 ":%s", address, format_hex_pretty_to(hex_buf, pdu, pdu_len));
|
||||
if (device)
|
||||
device->on_modbus_not_sent();
|
||||
device->on_not_sent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,7 +654,7 @@ void ModbusClientHub::clear_tx_queue_for_device(ModbusClientDevice *device) {
|
||||
void ModbusClientHub::send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device) {
|
||||
if (payload.size() < 2) {
|
||||
if (device)
|
||||
device->on_modbus_not_sent();
|
||||
device->on_not_sent();
|
||||
return;
|
||||
}
|
||||
this->queue_raw_(payload[0], payload.data() + 1, static_cast<uint16_t>(payload.size() - 1), device);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
@@ -59,8 +60,8 @@ class Modbus : public uart::UARTDevice, public Component {
|
||||
virtual int32_t tx_delay_remaining();
|
||||
virtual void parse_modbus_frames() = 0;
|
||||
bool parse_modbus_server_frame_();
|
||||
virtual void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data,
|
||||
uint16_t len) = 0;
|
||||
// pdu is the whole PDU (function code + payload, no address/CRC); pdu[0] is the (standard or custom) function code.
|
||||
virtual void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) = 0;
|
||||
void clear_rx_buffer_(const LogString *reason, bool warn = false, size_t bytes_to_clear = 0);
|
||||
bool send_frame_(const ModbusFrame &frame);
|
||||
// Scans forward from min_length to find a frame boundary by CRC match for custom function codes.
|
||||
@@ -118,10 +119,9 @@ class ModbusClientHub : public Modbus {
|
||||
protected:
|
||||
int32_t tx_delay_remaining() override;
|
||||
void parse_modbus_frames() override;
|
||||
// Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here.
|
||||
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override;
|
||||
void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
|
||||
void send_next_frame_();
|
||||
// Notify the waiting device of no response; re-queues the frame if on_modbus_no_response() returns true.
|
||||
// Notify the waiting device of no response; re-queues the frame if on_no_response() returns true.
|
||||
// wfr is the caller's checked reference to waiting_for_response_.
|
||||
void notify_no_response_(ModbusDeviceCommand &wfr);
|
||||
void requeue_waiting_frame_(ModbusDeviceCommand &wfr);
|
||||
@@ -146,8 +146,7 @@ class ModbusServerHub : public Modbus {
|
||||
protected:
|
||||
void parse_modbus_frames() override;
|
||||
bool parse_modbus_client_frame_();
|
||||
// Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here.
|
||||
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override;
|
||||
void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
|
||||
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data);
|
||||
ModbusServerDevice *find_device_(uint8_t address);
|
||||
// Returns true if [start_address, start_address + number_of_registers) fits in the 16-bit address space.
|
||||
@@ -180,12 +179,35 @@ class ModbusClientDevice {
|
||||
ModbusClientDevice &operator=(ModbusClientDevice &&) = delete;
|
||||
void set_parent(ModbusClientHub *parent) { this->parent_ = parent; }
|
||||
void set_address(uint8_t address) { this->address_ = address; }
|
||||
virtual void on_modbus_data(const std::vector<uint8_t> &data) {}
|
||||
virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
|
||||
virtual void on_modbus_not_sent() {}
|
||||
/// Called with the request PDU this device sent and the response PDU received (both: function code +
|
||||
/// data, no address, no CRC). The spans are only valid for the duration of the call - copy the bytes
|
||||
/// if they must outlive it. Slice the payload out of the response with helpers::server_pdu_payload().
|
||||
virtual void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {}
|
||||
/// Called with the request PDU and the modbus exception code decoded from the error response.
|
||||
virtual void on_error(std::span<const uint8_t> request_pdu, ModbusExceptionCode exception_code) {}
|
||||
// The on_modbus_* names are signature-identical renames, so the new defaults forward to the old
|
||||
// virtuals: external devices overriding the old names keep working through the deprecation window.
|
||||
// Remove the forwards together with the deprecated names.
|
||||
virtual void on_not_sent() {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
this->on_modbus_not_sent();
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
/// Called when no (valid) response arrived; return true to have the hub re-queue the frame for a retry.
|
||||
/// The hub does not bound retries: the device is responsible for limiting them (e.g. track a counter and
|
||||
/// return false when exhausted), or an unresponsive peer will starve other traffic on the bus.
|
||||
virtual bool on_no_response() {
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return this->on_modbus_no_response();
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
// Remove before 2027.2.0
|
||||
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
|
||||
virtual void on_modbus_not_sent() {}
|
||||
// Remove before 2027.2.0
|
||||
ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0")
|
||||
virtual bool on_modbus_no_response() { return false; }
|
||||
void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
|
||||
const uint8_t *payload = nullptr) {
|
||||
|
||||
@@ -47,6 +47,8 @@ uint16_t server_frame_length(const uint8_t *frame, size_t size);
|
||||
// If the frame is too short to determine the length, returns the minimum length
|
||||
uint16_t client_frame_length(const uint8_t *frame, size_t size);
|
||||
|
||||
// Remove before 2027.2.0
|
||||
ESPDEPRECATED("Use server_pdu_payload() on the response PDU instead. Removed in 2027.2.0", "2026.8.0")
|
||||
inline uint8_t server_frame_data_offset(const uint8_t *frame, size_t size) {
|
||||
if (size < 2)
|
||||
return 0;
|
||||
@@ -61,6 +63,20 @@ inline uint8_t server_frame_data_offset(const uint8_t *frame, size_t size) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the payload portion of a server response PDU: the bytes after the function code, and for the
|
||||
* standard read responses (0x01-0x04) also after the byte-count byte. Responses to 0x14/0x17 also carry a
|
||||
* byte-count byte, but those codes are not implemented and their count byte is left in the payload. For
|
||||
* an exception PDU the payload is the exception code byte (the read check must not see the masked
|
||||
* function code, or an exception-of-read would classify as a read and return an empty span). Returns an
|
||||
* empty span if the PDU is too short.
|
||||
*/
|
||||
inline std::span<const uint8_t> server_pdu_payload(std::span<const uint8_t> pdu) {
|
||||
if (pdu.empty())
|
||||
return {};
|
||||
const size_t offset = (!is_function_code_exception(pdu[0]) && is_function_code_read(pdu[0])) ? 2 : 1;
|
||||
return pdu.size() > offset ? pdu.subspan(offset) : std::span<const uint8_t>();
|
||||
}
|
||||
|
||||
inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; }
|
||||
|
||||
enum class SensorValueType : uint8_t {
|
||||
|
||||
@@ -57,7 +57,7 @@ bool ModbusController::send_next_command_() {
|
||||
}
|
||||
|
||||
// Queue incoming response
|
||||
void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void ModbusController::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
if (this->command_queue_.empty()) {
|
||||
ESP_LOGW(TAG, "Received modbus data but command queue is empty");
|
||||
return;
|
||||
@@ -78,8 +78,10 @@ void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
this->online_callback_.call((int) current_command->function_code, current_command->register_address);
|
||||
}
|
||||
|
||||
// Move the commandItem to the response queue
|
||||
current_command->payload = data;
|
||||
// Move the commandItem to the response queue. The span points into the hub's receive buffer, so
|
||||
// copy the payload into the command for deferred processing in loop().
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
current_command->payload.assign(data.begin(), data.end());
|
||||
this->incoming_queue_.push(std::move(current_command));
|
||||
ESP_LOGV(TAG, "Modbus response queued");
|
||||
this->command_queue_.pop_front();
|
||||
@@ -93,8 +95,11 @@ void ModbusController::process_modbus_data_(const ModbusCommandItem *response) {
|
||||
response->on_data_func(response->register_type, response->register_address, response->payload);
|
||||
}
|
||||
|
||||
void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) {
|
||||
ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code);
|
||||
void ModbusController::on_error(std::span<const uint8_t> request_pdu, modbus::ModbusExceptionCode exception_code) {
|
||||
// The request function code (request_pdu[0]) already carries what the log needs; the exception bit only
|
||||
// ever appears on the response, so no masking is needed here.
|
||||
const uint8_t function_code = request_pdu.empty() ? 0 : request_pdu[0];
|
||||
ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, static_cast<uint8_t>(exception_code));
|
||||
if (this->command_queue_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <list>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -293,9 +294,9 @@ class ModbusController final : public PollingComponent, public modbus::ModbusCli
|
||||
/// Registers a sensor with the controller. Called by esphomes code generator
|
||||
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
|
||||
/// called when a modbus response was parsed without errors
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
/// called when a modbus error response was received
|
||||
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
|
||||
void on_error(std::span<const uint8_t> request_pdu, modbus::ModbusExceptionCode exception_code) override;
|
||||
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
|
||||
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
|
||||
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
|
||||
|
||||
@@ -9,7 +9,8 @@ static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04;
|
||||
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
|
||||
static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers
|
||||
|
||||
void PZEMAC::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void PZEMAC::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < 20) {
|
||||
ESP_LOGW(TAG, "Invalid size for PZEM AC!");
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace esphome::pzemac {
|
||||
|
||||
@@ -22,7 +22,7 @@ class PZEMAC final : public PollingComponent, public modbus::ModbusClientDevice
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04;
|
||||
static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42;
|
||||
static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers
|
||||
|
||||
void PZEMDC::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void PZEMDC::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < 16) {
|
||||
ESP_LOGW(TAG, "Invalid size for PZEM DC!");
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace esphome::pzemdc {
|
||||
|
||||
@@ -18,7 +18,7 @@ class PZEMDC final : public PollingComponent, public modbus::ModbusClientDevice
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ static const char *const TAG = "sdm_meter";
|
||||
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 80; // 74 x 16-bit registers
|
||||
|
||||
void SDMMeter::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void SDMMeter::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
|
||||
ESP_LOGW(TAG, "Invalid size for SDMMeter!");
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace esphome::sdm_meter {
|
||||
|
||||
@@ -55,7 +55,7 @@ class SDMMeter final : public PollingComponent, public modbus::ModbusClientDevic
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ static const char *const TAG = "selec_meter";
|
||||
static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04;
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers
|
||||
|
||||
void SelecMeter::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
void SelecMeter::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
|
||||
ESP_LOGW(TAG, "Invalid size for SelecMeter!");
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
|
||||
#include <vector>
|
||||
#include <span>
|
||||
|
||||
namespace esphome::selec_meter {
|
||||
|
||||
@@ -37,7 +37,7 @@ class SelecMeter final : public PollingComponent, public modbus::ModbusClientDev
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_modbus_data(const std::vector<uint8_t> &data) override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
@@ -39,6 +39,50 @@ namespace esphome::modbus::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
// A UART the test can inject received bytes into; sent bytes are discarded.
|
||||
class InjectableUART : public uart::UARTComponent {
|
||||
public:
|
||||
void write_array(const uint8_t *data, size_t len) override {}
|
||||
bool peek_byte(uint8_t *data) override {
|
||||
if (this->rx_.empty())
|
||||
return false;
|
||||
*data = this->rx_.front();
|
||||
return true;
|
||||
}
|
||||
bool read_array(uint8_t *data, size_t len) override {
|
||||
if (len > this->rx_.size())
|
||||
return false;
|
||||
memcpy(data, this->rx_.data(), len);
|
||||
this->rx_.erase(this->rx_.begin(), this->rx_.begin() + len);
|
||||
return true;
|
||||
}
|
||||
size_t available() override { return this->rx_.size(); }
|
||||
uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; }
|
||||
void check_logger_conflict() override {}
|
||||
|
||||
void inject_frame(uint8_t address, std::span<const uint8_t> pdu) {
|
||||
// Wire frame: address + PDU + CRC16(low, high)
|
||||
size_t start = this->rx_.size();
|
||||
this->rx_.push_back(address);
|
||||
this->rx_.insert(this->rx_.end(), pdu.begin(), pdu.end());
|
||||
uint16_t crc = crc16(this->rx_.data() + start, this->rx_.size() - start);
|
||||
this->rx_.push_back(crc & 0xFF);
|
||||
this->rx_.push_back(crc >> 8);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> rx_;
|
||||
};
|
||||
|
||||
class NullDevice : public ModbusClientDevice {
|
||||
public:
|
||||
using ModbusClientDevice::ModbusClientDevice;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override {
|
||||
this->responses++;
|
||||
}
|
||||
int responses{0};
|
||||
};
|
||||
|
||||
struct Sample {
|
||||
size_t count;
|
||||
size_t bytes;
|
||||
@@ -93,14 +137,61 @@ TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) {
|
||||
EXPECT_EQ(total, 0u);
|
||||
}
|
||||
|
||||
// End to end: bytes injected at the UART travel through receive, frame parsing, response matching and
|
||||
// device dispatch. The first response may grow the hub's rx buffer once; after that warm-up, handling a
|
||||
// response performs zero heap allocations all the way to the device callback.
|
||||
TEST(HeapProbe, ResponseHandlingIsAllocationFreeAfterWarmup) {
|
||||
InjectableUART uart;
|
||||
uart.set_baud_rate(115200); // tx timing math divides by the baud rate
|
||||
ModbusClientHub hub;
|
||||
hub.set_uart_parent(&uart);
|
||||
hub.setup(); // computes frame timing from the baud rate
|
||||
NullDevice device(&hub, 0x02);
|
||||
|
||||
StaticVector<uint8_t, MAX_PDU_SIZE> req;
|
||||
const uint8_t read_pdu[] = {0x03, 0x01, 0x00, 0x00, 0x02};
|
||||
req.assign(read_pdu, read_pdu + sizeof(read_pdu));
|
||||
|
||||
// Largest possible read response first, so the rx buffer warm-up covers every later size.
|
||||
uint8_t large_resp[252] = {0x03, 250};
|
||||
const uint8_t small_resp[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
|
||||
|
||||
auto round_trip = [&](std::span<const uint8_t> response_pdu) {
|
||||
device.send_pdu(req);
|
||||
hub.loop(); // transmit; the tx queue is empty during the measured receive below
|
||||
uart.inject_frame(0x02, response_pdu);
|
||||
return sample([&] { hub.loop(); }); // receive + parse + match + dispatch
|
||||
};
|
||||
|
||||
Sample warmup = round_trip(std::span<const uint8_t>(large_resp, sizeof(large_resp)));
|
||||
Sample steady_large = round_trip(std::span<const uint8_t>(large_resp, sizeof(large_resp)));
|
||||
Sample steady_small = round_trip(small_resp);
|
||||
|
||||
printf("HEAPPROBE warmup count=%zu bytes=%zu\n", warmup.count, warmup.bytes);
|
||||
printf("HEAPPROBE steady_large count=%zu bytes=%zu\n", steady_large.count, steady_large.bytes);
|
||||
printf("HEAPPROBE steady_small count=%zu bytes=%zu\n", steady_small.count, steady_small.bytes);
|
||||
|
||||
EXPECT_EQ(device.responses, 3);
|
||||
EXPECT_LE(warmup.count, 1u); // at most the one-time rx buffer growth
|
||||
EXPECT_EQ(steady_large.count, 0u);
|
||||
EXPECT_EQ(steady_small.count, 0u);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus::testing
|
||||
|
||||
#else // !HEAP_PROBE_HAS_ASAN
|
||||
|
||||
// Stub every ASan-gated test name, so the suite's test list is identical in every build configuration.
|
||||
namespace esphome::modbus::testing {
|
||||
TEST(HeapProbe, TypicalFrameConstructionIsAllocationFree) {
|
||||
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
|
||||
}
|
||||
TEST(HeapProbe, QueueingTypicalCommandsIsAllocationFree) {
|
||||
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
|
||||
}
|
||||
TEST(HeapProbe, ResponseHandlingIsAllocationFreeAfterWarmup) {
|
||||
GTEST_SKIP() << "allocation counting requires an AddressSanitizer build";
|
||||
}
|
||||
} // namespace esphome::modbus::testing
|
||||
|
||||
#endif // HEAP_PROBE_HAS_ASAN
|
||||
|
||||
@@ -27,8 +27,8 @@ class NoResponseProbeHub : public ModbusClientHub {
|
||||
this->tx_buffer_.pop_front();
|
||||
}
|
||||
// Drives the real unexpected-frame branch in process_modbus_server_frame().
|
||||
void receive_frame_for_test(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) {
|
||||
this->process_modbus_server_frame(address, function_code, data, len);
|
||||
void receive_frame_for_test(uint8_t address, std::span<const uint8_t> pdu) {
|
||||
this->process_modbus_server_frame(address, pdu);
|
||||
}
|
||||
void timeout_waiting() {
|
||||
if (this->waiting_for_response_.has_value())
|
||||
@@ -37,11 +37,11 @@ class NoResponseProbeHub : public ModbusClientHub {
|
||||
}
|
||||
};
|
||||
|
||||
// A device with a scripted answer to on_modbus_no_response().
|
||||
// A device with a scripted answer to on_no_response().
|
||||
class RetryingDevice : public ModbusClientDevice {
|
||||
public:
|
||||
RetryingDevice(ModbusClientHub *hub, uint8_t address, bool retry) : ModbusClientDevice(hub, address), retry_(retry) {}
|
||||
bool on_modbus_no_response() override {
|
||||
bool on_no_response() override {
|
||||
this->no_response_count_++;
|
||||
return this->retry_;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ class RetryingDevice : public ModbusClientDevice {
|
||||
class ClearingRetryDevice : public ModbusClientDevice {
|
||||
public:
|
||||
ClearingRetryDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {}
|
||||
bool on_modbus_no_response() override {
|
||||
bool on_no_response() override {
|
||||
this->no_response_count_++;
|
||||
this->clear_tx_queue_for_device(); // detaches this device from the waiting slot mid-callback
|
||||
return true; // and still requests a retry
|
||||
@@ -143,8 +143,8 @@ TEST(ModbusClientHubNoResponse, RetryBehindInterruptedShell) {
|
||||
hub.force_send_front();
|
||||
|
||||
// A frame from the wrong address (0x07, expected 0x02) hits the unexpected-frame branch.
|
||||
const uint8_t stray_payload[] = {0x04, 0x00, 0x2A, 0x01, 0x00};
|
||||
hub.receive_frame_for_test(0x07, 0x03, stray_payload, sizeof(stray_payload));
|
||||
const uint8_t stray_pdu[] = {0x03, 0x04, 0x00, 0x2A, 0x01, 0x00};
|
||||
hub.receive_frame_for_test(0x07, stray_pdu);
|
||||
|
||||
EXPECT_EQ(device.no_response_count_, 1);
|
||||
ASSERT_EQ(hub.queued_frames(), 1u); // exactly one requeue...
|
||||
@@ -175,4 +175,37 @@ TEST(ModbusClientHubNoResponse, MidCallbackClearCancelsRetry) {
|
||||
EXPECT_FALSE(hub.waiting());
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Overrides only the DEPRECATED on_modbus_* names: the new-name default implementations must forward, so
|
||||
// external devices written against the old names keep working through the deprecation window.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
class LegacyNameDevice : public ModbusClientDevice {
|
||||
public:
|
||||
LegacyNameDevice(ModbusClientHub *hub, uint8_t address) : ModbusClientDevice(hub, address) {}
|
||||
void on_modbus_not_sent() override { this->legacy_not_sent_++; }
|
||||
bool on_modbus_no_response() override {
|
||||
this->legacy_no_response_++;
|
||||
return false;
|
||||
}
|
||||
int legacy_not_sent_{0};
|
||||
int legacy_no_response_{0};
|
||||
};
|
||||
#pragma GCC diagnostic pop
|
||||
} // namespace
|
||||
|
||||
TEST(ModbusClientHubCompat, LegacyCallbackNamesStillForward) {
|
||||
NoResponseProbeHub hub;
|
||||
LegacyNameDevice device(&hub, 0x02);
|
||||
|
||||
const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x01};
|
||||
device.send_pdu(read);
|
||||
hub.force_send_front();
|
||||
hub.timeout_waiting(); // no reply -> on_no_response -> forwards to on_modbus_no_response
|
||||
EXPECT_EQ(device.legacy_no_response_, 1);
|
||||
|
||||
device.send_pdu(std::span<const uint8_t>()); // empty PDU refused -> on_not_sent -> forwards
|
||||
EXPECT_EQ(device.legacy_not_sent_, 1);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus::testing
|
||||
|
||||
@@ -229,4 +229,21 @@ TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) {
|
||||
EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value());
|
||||
}
|
||||
|
||||
// server_pdu_payload() must never classify an exception PDU as a read: [fc|0x80, code] is 2 bytes, and a
|
||||
// read-offset of 2 would return an empty span, losing the exception code. The payload of an exception PDU
|
||||
// is the exception code byte, for reads and writes alike.
|
||||
TEST(ModbusServerPduPayload, ExceptionOfReadYieldsExceptionCode) {
|
||||
const uint8_t pdu[] = {0x83, 0x02}; // exception response to READ_HOLDING_REGISTERS
|
||||
auto payload = server_pdu_payload(pdu);
|
||||
ASSERT_EQ(payload.size(), 1u);
|
||||
EXPECT_EQ(payload[0], 0x02);
|
||||
}
|
||||
|
||||
TEST(ModbusServerPduPayload, ExceptionOfWriteYieldsExceptionCode) {
|
||||
const uint8_t pdu[] = {0x86, 0x03}; // exception response to WRITE_SINGLE_REGISTER
|
||||
auto payload = server_pdu_payload(pdu);
|
||||
ASSERT_EQ(payload.size(), 1u);
|
||||
EXPECT_EQ(payload[0], 0x03);
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus::helpers
|
||||
|
||||
Reference in New Issue
Block a user