mirror of
https://github.com/esphome/esphome.git
synced 2026-08-28 08:43:27 +00:00
Merge remote-tracking branch 'origin/dev' into esp8266-native-library-backend
This commit is contained in:
@@ -119,16 +119,22 @@ jobs:
|
||||
# pushed image) keeps it working for fork PRs, which never push to ghcr.io.
|
||||
- name: Export image for compile-test
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz
|
||||
# zstd over gzip: docker save is on the critical path for every
|
||||
# compile-test job, and zstd -T0 is multithreaded (export 50s -> 9s).
|
||||
# docker load auto-detects the format; its time is layer extraction,
|
||||
# not decompression, so it is unchanged. shell: bash adds pipefail so
|
||||
# a failed docker save cannot upload a truncated artifact.
|
||||
shell: bash
|
||||
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | zstd -T0 -3 > compile-test-image.tar.zst
|
||||
|
||||
- name: Upload compile-test image artifact
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
# The tar is already gzipped, so upload it as-is. archive: false skips
|
||||
# the redundant zip and makes the file name the artifact name (the
|
||||
# `name` input is ignored in that mode).
|
||||
path: compile-test-image.tar.gz
|
||||
# The tar is already compressed, so upload it as-is. archive: false
|
||||
# skips the redundant zip and makes the file name the artifact name
|
||||
# (the `name` input is ignored in that mode).
|
||||
path: compile-test-image.tar.zst
|
||||
retention-days: 1
|
||||
archive: false
|
||||
|
||||
@@ -206,9 +212,9 @@ jobs:
|
||||
- name: Download image artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: compile-test-image.tar.gz
|
||||
name: compile-test-image.tar.zst
|
||||
- name: Load image
|
||||
run: docker load --input compile-test-image.tar.gz
|
||||
run: docker load --input compile-test-image.tar.zst
|
||||
- name: Compile ${{ matrix.id }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
|
||||
@@ -536,7 +536,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
|
||||
uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
|
||||
@@ -20,7 +20,9 @@ const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E
|
||||
|
||||
enum class FunctionCode : uint8_t {
|
||||
INVALID = 0x00, // 0x00 is not a valid function code (even for custom functions).
|
||||
CUSTOM = 0x00, // The CUSTOM alias should be removed in future.
|
||||
// Remove before 2027.3.0
|
||||
CUSTOM ESPDEPRECATED("0x00 is not a function code; use FunctionCode::INVALID for the sentinel. Removed in 2027.3.0",
|
||||
"2026.9.0") = 0x00,
|
||||
READ_COILS = 0x01,
|
||||
READ_DISCRETE_INPUTS = 0x02,
|
||||
READ_HOLDING_REGISTERS = 0x03,
|
||||
|
||||
@@ -153,6 +153,50 @@ inline std::span<const uint8_t> server_pdu_payload(std::span<const uint8_t> pdu)
|
||||
|
||||
inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; }
|
||||
|
||||
/** Extract data from modbus response buffer
|
||||
* @param T one of supported integer data types int_8,int_16,int_32,int_64
|
||||
* @param data modbus response buffer (uint8_t)
|
||||
* @param buffer_offset offset in bytes.
|
||||
* @return value of type T extracted from buffer
|
||||
*/
|
||||
template<typename T> T get_data(const uint8_t *data, size_t buffer_offset) {
|
||||
if (sizeof(T) == sizeof(uint8_t)) {
|
||||
return T(data[buffer_offset]);
|
||||
}
|
||||
if (sizeof(T) == sizeof(uint16_t)) {
|
||||
return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
|
||||
}
|
||||
if (sizeof(T) == sizeof(uint32_t)) {
|
||||
return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
|
||||
static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
|
||||
}
|
||||
if (sizeof(T) == sizeof(uint64_t)) {
|
||||
return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
|
||||
(static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
|
||||
}
|
||||
static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
|
||||
sizeof(T) == sizeof(uint64_t),
|
||||
"Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
|
||||
return T{};
|
||||
}
|
||||
|
||||
/// Function code of a PDU, exception flag masked; 0 for an empty PDU.
|
||||
inline uint8_t pdu_function_code(std::span<const uint8_t> pdu) {
|
||||
return pdu.empty() ? 0 : (pdu[0] & FUNCTION_CODE_MASK);
|
||||
}
|
||||
|
||||
/// Start address of a standard client request PDU ([fc, addr_hi, addr_lo, ...]). Empty when the PDU is
|
||||
/// too short or its function code has no known layout - custom-frame bytes are not misread as an address.
|
||||
inline std::optional<uint16_t> client_pdu_start_address(std::span<const uint8_t> pdu) {
|
||||
if (pdu.size() < 3 || is_function_code_unknown_length(pdu[0]))
|
||||
return std::nullopt;
|
||||
const auto fc = static_cast<FunctionCode>(pdu[0]);
|
||||
// The file-record PDUs are known-length but carry a byte count, not a start address.
|
||||
if (fc == FunctionCode::READ_FILE_RECORD || fc == FunctionCode::WRITE_FILE_RECORD)
|
||||
return std::nullopt;
|
||||
return get_data<uint16_t>(pdu.data(), 1);
|
||||
}
|
||||
|
||||
enum class SensorValueType : uint8_t {
|
||||
RAW = 0x00, // variable length
|
||||
U_WORD = 0x1, // 1 Register unsigned
|
||||
@@ -256,34 +300,6 @@ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
|
||||
return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
|
||||
}
|
||||
|
||||
// Extract data from modbus response buffer
|
||||
/** Extract data from modbus response buffer
|
||||
* @param T one of supported integer data types int_8,int_16,int_32,int_64
|
||||
* @param data modbus response buffer (uint8_t)
|
||||
* @param buffer_offset offset in bytes.
|
||||
* @return value of type T extracted from buffer
|
||||
*/
|
||||
template<typename T> T get_data(const uint8_t *data, size_t buffer_offset) {
|
||||
if (sizeof(T) == sizeof(uint8_t)) {
|
||||
return T(data[buffer_offset]);
|
||||
}
|
||||
if (sizeof(T) == sizeof(uint16_t)) {
|
||||
return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
|
||||
}
|
||||
if (sizeof(T) == sizeof(uint32_t)) {
|
||||
return static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset)) << 16 |
|
||||
static_cast<uint32_t>(get_data<uint16_t>(data, buffer_offset + 2));
|
||||
}
|
||||
if (sizeof(T) == sizeof(uint64_t)) {
|
||||
return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
|
||||
(static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
|
||||
}
|
||||
static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) ||
|
||||
sizeof(T) == sizeof(uint64_t),
|
||||
"Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported.");
|
||||
return T{};
|
||||
}
|
||||
|
||||
template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
|
||||
return get_data<T>(data.data(), buffer_offset);
|
||||
}
|
||||
|
||||
@@ -23,60 +23,106 @@ void WriterDevice::warn_write_buffer_deprecated(const LogString *platform, uint1
|
||||
bool WriterDevice::send_raw_frame_deprecated(std::span<const uint8_t> frame) {
|
||||
if (frame.empty())
|
||||
return false;
|
||||
this->dispatched_ = true;
|
||||
return this->parent_->queue_pdu(frame[0], frame.subspan(1), this);
|
||||
}
|
||||
|
||||
void WriterDevice::set_controller(ModbusController *controller) {
|
||||
void ControllerDevice::set_controller(ModbusController *controller) {
|
||||
this->controller_ = controller;
|
||||
this->set_parent(controller->hub());
|
||||
this->set_address(controller->device_address());
|
||||
}
|
||||
|
||||
void WriterDevice::notify_online_(std::span<const uint8_t> request_pdu) {
|
||||
if (this->controller_ != nullptr)
|
||||
this->controller_->set_online(true, fc_of(request_pdu), addr_of(request_pdu));
|
||||
// A request whose layout carries no start address (a custom PDU) reports -1; 0 stays a real address.
|
||||
static int trigger_address(std::span<const uint8_t> request_pdu) {
|
||||
const auto addr = modbus::helpers::client_pdu_start_address(request_pdu);
|
||||
return addr.has_value() ? *addr : -1;
|
||||
}
|
||||
|
||||
void ControllerDevice::notify_online_(std::span<const uint8_t> request_pdu) {
|
||||
if (this->controller_ != nullptr) {
|
||||
this->controller_->set_online(true, modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu));
|
||||
}
|
||||
}
|
||||
|
||||
void ControllerDevice::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
this->notify_online_(request_pdu);
|
||||
}
|
||||
|
||||
void ControllerDevice::on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) {
|
||||
ESP_LOGW(TAG, "Modbus error function code: 0x%X register %d exception: %d",
|
||||
modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu),
|
||||
static_cast<uint8_t>(exception_code));
|
||||
this->notify_online_(request_pdu); // an exception is still a legitimate reply -> device is online
|
||||
}
|
||||
|
||||
void WriterDevice::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
this->notify_online_(request_pdu);
|
||||
ControllerDevice::on_response(request_pdu, response_pdu);
|
||||
this->dispatch_response_(request_pdu, response_pdu, std::nullopt);
|
||||
}
|
||||
|
||||
void WriterDevice::on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) {
|
||||
ESP_LOGW(TAG, "Modbus error function code: 0x%X register 0x%X exception: %d", fc_of(request_pdu),
|
||||
addr_of(request_pdu), static_cast<uint8_t>(exception_code));
|
||||
this->notify_online_(request_pdu); // an exception is still a legitimate reply -> device is online
|
||||
ControllerDevice::on_error(request_pdu, exception_code);
|
||||
this->dispatch_response_(request_pdu, {}, exception_code);
|
||||
}
|
||||
|
||||
// Fired once per wire transmission (including hub re-queues from a retry), so the on_command_sent trigger
|
||||
// reflects when the frame actually went out, not when it was queued.
|
||||
void WriterDevice::on_sent(std::span<const uint8_t> request_pdu) {
|
||||
if (this->controller_ != nullptr)
|
||||
this->controller_->command_sent(fc_of(request_pdu), addr_of(request_pdu));
|
||||
}
|
||||
|
||||
void WriterDevice::on_not_sent(std::span<const uint8_t> request_pdu) {
|
||||
// Only the offline teardown reaches this (a supersede retires silently), so the frame is genuinely
|
||||
// lost; a dropped write was already published optimistically, so surface it.
|
||||
if (modbus::helpers::is_function_code_write(fc_of(request_pdu))) {
|
||||
ESP_LOGW(TAG, "Write not sent: function 0x%X register 0x%X", fc_of(request_pdu), addr_of(request_pdu));
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Request not sent: function 0x%X register 0x%X", fc_of(request_pdu), addr_of(request_pdu));
|
||||
void ControllerDevice::on_sent(std::span<const uint8_t> request_pdu) {
|
||||
if (this->controller_ != nullptr) {
|
||||
this->controller_->command_sent(modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu));
|
||||
}
|
||||
}
|
||||
|
||||
bool WriterDevice::on_no_response(std::span<const uint8_t> request_pdu) {
|
||||
void ControllerDevice::on_not_sent(std::span<const uint8_t> request_pdu) {
|
||||
const uint8_t fc = modbus::helpers::pdu_function_code(request_pdu);
|
||||
const int addr = trigger_address(request_pdu);
|
||||
// Only the offline teardown reaches this (a supersede retires silently), so the frame is genuinely
|
||||
// lost; a dropped write was already published optimistically, so surface it.
|
||||
if (modbus::helpers::is_function_code_write(fc)) {
|
||||
ESP_LOGW(TAG, "Write not sent: function 0x%X register %d", fc, addr);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Request not sent: function 0x%X register %d", fc, addr);
|
||||
}
|
||||
}
|
||||
|
||||
bool ControllerDevice::on_no_response(std::span<const uint8_t> request_pdu) {
|
||||
if (this->controller_ == nullptr)
|
||||
return false;
|
||||
this->controller_->increment_non_response_count();
|
||||
if (this->controller_->can_send())
|
||||
return true; // the hub re-queues the frame it is holding; on_sent fires again on the retry
|
||||
this->controller_->set_online(false, fc_of(request_pdu), addr_of(request_pdu));
|
||||
this->controller_->set_online(false, modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu));
|
||||
return false;
|
||||
}
|
||||
|
||||
PollingDevice::PollingDevice(ModbusController &controller, RegisterRange &&range)
|
||||
: ControllerDevice(&controller), range_(std::move(range)) {}
|
||||
|
||||
bool PollingDevice::queue(modbus::CommandOptions options) {
|
||||
bool accepted;
|
||||
if (this->range_.custom_pdu != nullptr) {
|
||||
accepted = this->queue_pdu(std::span<const uint8_t>(*this->range_.custom_pdu), options);
|
||||
} else {
|
||||
accepted = this->read_entities(this->range_.register_type, this->range_.start_address, this->range_.register_count,
|
||||
options);
|
||||
}
|
||||
if (accepted) {
|
||||
ESP_LOGV(TAG, "Poll queued type=%u 0x%X %d", static_cast<uint8_t>(this->range_.register_type),
|
||||
this->range_.start_address, this->range_.register_count);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
|
||||
void PollingDevice::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
this->notify_online_(request_pdu);
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
for (auto *sensor : this->range_.sensors)
|
||||
sensor->parse_and_publish(data);
|
||||
}
|
||||
|
||||
// ModbusCommandItem's machinery stays as-is until its removal in 2027.3.0; silence its self-references.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
ModbusCommandItem::ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address,
|
||||
RegisterRange &&range)
|
||||
: modbus::ModbusClientDevice(parent, address),
|
||||
@@ -207,6 +253,8 @@ bool ModbusCommandItem::on_no_response(std::span<const uint8_t> request_pdu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void ModbusController::set_online(bool online, int function_code, int register_address) {
|
||||
if (online) {
|
||||
this->cmd_non_responses_ = 0;
|
||||
@@ -228,6 +276,8 @@ void ModbusController::set_online(bool online, int function_code, int register_a
|
||||
}
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
void ModbusController::queue_command(ModbusCommandItem command) {
|
||||
this->sweep_completed_one_shots_(); // reclaim finished one-shots before adding a new one
|
||||
// Duplicates are the caller's to manage; the controller only holds the item until its terminal callback.
|
||||
@@ -262,6 +312,8 @@ void ModbusController::sweep_completed_one_shots_() {
|
||||
[](const std::unique_ptr<ModbusCommandItem> &item) { return item->pending_removal; });
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
void ModbusController::update() {
|
||||
this->sweep_completed_one_shots_(); // reclaim one-shots deferred out of their own callbacks
|
||||
if (this->module_offline_) {
|
||||
@@ -270,11 +322,11 @@ void ModbusController::update() {
|
||||
if (offline_retry_due(this->update_counter_, this->module_offline_at_, this->offline_skip_updates_)) {
|
||||
ESP_LOGV(TAG, "Module offline - retrying");
|
||||
this->cmd_non_responses_ = 0; // allow the probe through can_send()
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
for (auto &poll : this->polling_devices_) {
|
||||
// Probes carry the read-side options too, so a recovering device resumes streaming on the
|
||||
// probe itself rather than waiting for the next update_interval.
|
||||
if (!cmd.send(this->read_options_)) {
|
||||
ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", cmd.register_address());
|
||||
if (!poll.queue(this->read_options_)) {
|
||||
ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", poll.register_address());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -285,12 +337,12 @@ void ModbusController::update() {
|
||||
}
|
||||
|
||||
if (this->can_send()) {
|
||||
for (auto &cmd : this->polling_command_items_) {
|
||||
ESP_LOGVV(TAG, "Updating range 0x%X", cmd.register_address());
|
||||
for (auto &poll : this->polling_devices_) {
|
||||
ESP_LOGVV(TAG, "Updating range 0x%X", poll.register_address());
|
||||
// read_options_ carries the controller's continuous flag (the offline probe above sends it too).
|
||||
// A refusal is already logged by the hub; note the affected range for controller-level diagnostics.
|
||||
if (!cmd.send(this->read_options_)) {
|
||||
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", cmd.register_address());
|
||||
if (!poll.queue(this->read_options_)) {
|
||||
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", poll.register_address());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +360,10 @@ void ModbusController::create_polling_commands_() {
|
||||
// force_new_range ahead of the rest, then address - so the walk is not purely address-ordered.
|
||||
// Each keeps the address it was configured with; what is resolved here is its `offset`, the position
|
||||
// of its data within the response of whichever range it ends up in.
|
||||
// One range per sensor is a strict upper bound: each walk step closes at most one range, plus one
|
||||
// closed after the walk. Sized to that bound so no push is ever silently dropped, then handed on by move.
|
||||
FixedVector<RegisterRange> ranges;
|
||||
ranges.init(this->sensorset_.size());
|
||||
RegisterRange r = {};
|
||||
bool have_range = false;
|
||||
// Set while the open range belongs to a force_new_range sensor: a range the user asked to keep
|
||||
@@ -390,7 +446,7 @@ void ModbusController::create_polling_commands_() {
|
||||
if (!join) {
|
||||
if (have_range) {
|
||||
ESP_LOGV(TAG, "Add range 0x%X %d", r.start_address, r.register_count);
|
||||
this->create_polling_command_(std::move(r));
|
||||
ranges.push_back(std::move(r));
|
||||
}
|
||||
r = {};
|
||||
range_bytes = curr->get_register_size();
|
||||
@@ -401,6 +457,8 @@ void ModbusController::create_polling_commands_() {
|
||||
r.start_address = curr->start_address;
|
||||
r.register_count = curr->register_count;
|
||||
r.register_type = curr->register_type;
|
||||
if (curr->register_type == modbus::EntityType::CUSTOM)
|
||||
r.custom_pdu = &curr->custom_pdu;
|
||||
have_range = true;
|
||||
}
|
||||
|
||||
@@ -412,11 +470,13 @@ void ModbusController::create_polling_commands_() {
|
||||
}
|
||||
if (have_range) {
|
||||
ESP_LOGV(TAG, "Add last range 0x%X %d", r.start_address, r.register_count);
|
||||
this->create_polling_command_(std::move(r));
|
||||
ranges.push_back(std::move(r));
|
||||
}
|
||||
// Staged in a setup-time vector so the device storage can be sized exactly (see polling_devices_).
|
||||
this->polling_devices_.init(ranges.size());
|
||||
for (auto &range : ranges) {
|
||||
this->polling_devices_.emplace_back(*this, std::move(range));
|
||||
}
|
||||
// Reclaim growth slack; safe here because nothing has registered with the hub yet (see the
|
||||
// lifetime note on polling_command_items_).
|
||||
this->polling_command_items_.shrink_to_fit();
|
||||
}
|
||||
|
||||
void ModbusController::dump_config() {
|
||||
@@ -435,13 +495,15 @@ void ModbusController::dump_config() {
|
||||
it->get_register_size());
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, "ranges");
|
||||
for (auto &it : this->polling_command_items_) {
|
||||
for (auto &it : this->polling_devices_) {
|
||||
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d", static_cast<uint8_t>(it.register_type()),
|
||||
it.register_address(), it.register_count());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
void ModbusController::on_write_register_response(EntityType register_type, uint16_t start_address,
|
||||
std::span<const uint8_t> data) {
|
||||
// A well-formed write ACK echoes address and value, but a truncated PDU yields a short/empty span.
|
||||
@@ -598,5 +660,6 @@ bool ModbusCommandItem::send(modbus::CommandOptions options) {
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
} // namespace esphome::modbus_controller
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "esphome/components/modbus/modbus.h"
|
||||
#include "esphome/components/modbus/modbus_helpers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <list>
|
||||
@@ -230,15 +231,22 @@ struct RegisterRange {
|
||||
modbus::EntityType register_type;
|
||||
uint8_t register_count;
|
||||
SensorSet sensors; // all sensors of this range
|
||||
/// A custom range polls this PDU, referenced from the sensor that opened the range.
|
||||
const SmallInlineBuffer<8> *custom_pdu{nullptr};
|
||||
};
|
||||
|
||||
/// A hub device owned by a writer entity (switch/number/select/output) through WriterEntity.
|
||||
/// Centralises the feedback to the controller - online/offline tracking, retry counting and the
|
||||
/// on_command_sent trigger - and records every dispatch, so a write lambda can tell "I sent it myself"
|
||||
/// from "use the default write". The hub base is inherited protected, so the public members below are
|
||||
/// the entity's whole request API and nothing can bypass the recording or re-target the device.
|
||||
class WriterDevice final : protected modbus::ModbusClientDevice {
|
||||
/// The shared feedback half of a controller-owned hub device: online/offline tracking, retry counting
|
||||
/// and the on_command_sent trigger all route to the controller from here. The hub base is inherited
|
||||
/// protected, so a subclass chooses exactly what request API it exposes.
|
||||
class ControllerDevice : protected modbus::ModbusClientDevice {
|
||||
public:
|
||||
// Public: only the owner can reach this instance, so reachability is the access gate.
|
||||
void set_controller(ModbusController *controller);
|
||||
|
||||
protected:
|
||||
ControllerDevice() = default; // WriterEntity's member is wired later via set_controller()
|
||||
explicit ControllerDevice(ModbusController *controller) { this->set_controller(controller); }
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override;
|
||||
void on_sent(std::span<const uint8_t> request_pdu) override;
|
||||
@@ -246,90 +254,82 @@ class WriterDevice final : protected modbus::ModbusClientDevice {
|
||||
bool on_no_response(std::span<const uint8_t> request_pdu) override;
|
||||
|
||||
void notify_online_(std::span<const uint8_t> request_pdu);
|
||||
/// Function code / register address decoded from a request PDU ([fc, addr_hi, addr_lo, ...]).
|
||||
static int fc_of(std::span<const uint8_t> pdu) { return pdu.empty() ? 0 : (pdu[0] & modbus::FUNCTION_CODE_MASK); }
|
||||
static int addr_of(std::span<const uint8_t> pdu) {
|
||||
return pdu.size() >= 3 ? modbus::helpers::get_data<uint16_t>(pdu.data(), 1) : 0;
|
||||
}
|
||||
|
||||
/// Declared before controller_ so they land in the padding after ModbusClientDevice::custom_response_warned_
|
||||
/// instead of adding a word to every entity that owns a device.
|
||||
/// dispatched_: a frame was queued since the last clear_dispatched_().
|
||||
/// write_buffer_deprecated_warned_: warn-once for the legacy write_lambda buffer parameter.
|
||||
/// Write-path state owned by WriterEntity's forwarders, stored here so both bools land in the base's
|
||||
/// tail padding instead of adding a word to every writer entity. The warn flag leaves in 2027.3.0.
|
||||
bool dispatched_{false};
|
||||
bool write_buffer_deprecated_warned_{false};
|
||||
ModbusController *controller_{nullptr};
|
||||
};
|
||||
|
||||
/// The write side of a ControllerDevice, owned by the writer entities through WriterEntity, whose
|
||||
/// forwarders re-expose exactly the request API a write lambda may use and record every dispatch.
|
||||
class WriterDevice final : public ControllerDevice {
|
||||
public:
|
||||
/// Whether a frame was queued to the hub since the last clear_dispatched_().
|
||||
bool dispatched() const { return this->dispatched_; }
|
||||
using modbus::ModbusClientDevice::clear_tx_queue_for_device;
|
||||
using modbus::ModbusClientDevice::queue_pdu;
|
||||
using modbus::ModbusClientDevice::write_multiple_coils;
|
||||
using modbus::ModbusClientDevice::write_multiple_registers;
|
||||
using modbus::ModbusClientDevice::write_single_coil;
|
||||
using modbus::ModbusClientDevice::write_single_register;
|
||||
|
||||
bool write_single_register(uint16_t address, uint16_t value) {
|
||||
this->dispatched_ = true;
|
||||
return modbus::ModbusClientDevice::write_single_register(address, value);
|
||||
}
|
||||
bool write_single_coil(uint16_t address, bool value) {
|
||||
this->dispatched_ = true;
|
||||
return modbus::ModbusClientDevice::write_single_coil(address, value);
|
||||
}
|
||||
bool write_multiple_registers(uint16_t address, std::span<const uint16_t> values) {
|
||||
this->dispatched_ = true;
|
||||
return modbus::ModbusClientDevice::write_multiple_registers(address, values);
|
||||
}
|
||||
bool write_multiple_coils(uint16_t address, std::span<const bool> values) {
|
||||
this->dispatched_ = true;
|
||||
return modbus::ModbusClientDevice::write_multiple_coils(address, values);
|
||||
}
|
||||
bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) {
|
||||
this->dispatched_ = true;
|
||||
return modbus::ModbusClientDevice::write_multiple_coils(address, bits);
|
||||
}
|
||||
bool queue_pdu(std::span<const uint8_t> pdu, modbus::CommandOptions options = {}) {
|
||||
this->dispatched_ = true;
|
||||
return modbus::ModbusClientDevice::queue_pdu(pdu, options);
|
||||
}
|
||||
/// Send a legacy raw frame (address + function code + data) to the frame's own address.
|
||||
/// Serves only the deprecated write_lambda buffer path. Remove before 2027.3.0.
|
||||
bool send_raw_frame_deprecated(std::span<const uint8_t> frame);
|
||||
|
||||
void clear_tx_queue_for_device() { modbus::ModbusClientDevice::clear_tx_queue_for_device(); }
|
||||
|
||||
// Entity plumbing, public because the owning WriterEntity holds the only reachable instance (device_ is
|
||||
// protected there and the hub sees just the masked base) - reachability is the access gate, not a friend.
|
||||
void set_controller(ModbusController *controller);
|
||||
bool dispatched() const { return this->dispatched_; }
|
||||
void set_dispatched() { this->dispatched_ = true; }
|
||||
void clear_dispatched() { this->dispatched_ = false; }
|
||||
/// Warn once per entity that filling the write_lambda buffer parameter is deprecated (the entity is now the
|
||||
/// command - call a write helper / queue_pdu() on `item` instead). The buffer parameter is removed in 2027.3.0.
|
||||
void warn_write_buffer_deprecated(const LogString *platform, uint16_t address);
|
||||
|
||||
protected:
|
||||
// Only the write side forwards to the typed callbacks (for item->queue_pdu() replies): a poll parses
|
||||
// its own response, and dispatching its errors would trip the base unhandled-custom-response warning.
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_error(std::span<const uint8_t> request_pdu, modbus::ExceptionCode exception_code) override;
|
||||
};
|
||||
|
||||
/// Gives a writer entity the write API of the WriterDevice it owns. The device is a member, not a base:
|
||||
/// the mixin declares no virtual function, so an entity mixing it in gains no second vtable and all the
|
||||
/// writer platforms share the single WriterDevice vtable instead of each emitting its own copy.
|
||||
/// The forwarders keep `item->write_*()` working unchanged inside a write_lambda.
|
||||
/// The forwarders keep `item->write_*()` working unchanged inside a write_lambda, and record every
|
||||
/// dispatch, so the write path can tell "the lambda sent it itself" from "use the default write".
|
||||
class WriterEntity {
|
||||
public:
|
||||
/// Whether the lambda called a request helper since the last clear_dispatched_(). Deliberately records
|
||||
/// the call, not the hub's accept/refuse: a refused lambda write must not fall through to the default write.
|
||||
bool dispatched() const { return this->device_.dispatched(); }
|
||||
bool write_single_register(uint16_t address, uint16_t value) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_single_register(address, value);
|
||||
}
|
||||
bool write_single_coil(uint16_t address, bool value) { return this->device_.write_single_coil(address, value); }
|
||||
bool write_single_coil(uint16_t address, bool value) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_single_coil(address, value);
|
||||
}
|
||||
bool write_multiple_registers(uint16_t address, std::span<const uint16_t> values) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_multiple_registers(address, values);
|
||||
}
|
||||
bool write_multiple_coils(uint16_t address, std::span<const bool> values) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_multiple_coils(address, values);
|
||||
}
|
||||
bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.write_multiple_coils(address, bits);
|
||||
}
|
||||
bool queue_pdu(std::span<const uint8_t> pdu, modbus::CommandOptions options = {}) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.queue_pdu(pdu, options);
|
||||
}
|
||||
void clear_tx_queue_for_device() { this->device_.clear_tx_queue_for_device(); }
|
||||
|
||||
protected:
|
||||
bool send_raw_frame_deprecated_(std::span<const uint8_t> frame) {
|
||||
this->device_.set_dispatched();
|
||||
return this->device_.send_raw_frame_deprecated(frame);
|
||||
}
|
||||
void set_controller_(ModbusController *controller) { this->device_.set_controller(controller); }
|
||||
@@ -338,13 +338,40 @@ class WriterEntity {
|
||||
this->device_.warn_write_buffer_deprecated(platform, address);
|
||||
}
|
||||
|
||||
private:
|
||||
// Private so a derived entity cannot reach the device except through the recording forwarders above.
|
||||
WriterDevice device_;
|
||||
};
|
||||
|
||||
/// A persistent hub device that polls one register range - the read-side mirror of WriterDevice.
|
||||
/// Owned by the controller, one per range; the response is parsed straight to the range's sensors.
|
||||
class PollingDevice final : public ControllerDevice {
|
||||
public:
|
||||
PollingDevice(ModbusController &controller, RegisterRange &&range);
|
||||
|
||||
/// Queue this range's read (or its sensor's custom PDU) on the hub. False = refused, no callback follows.
|
||||
bool queue(modbus::CommandOptions options = {});
|
||||
|
||||
uint16_t register_address() const { return this->range_.start_address; }
|
||||
uint16_t register_count() const { return this->range_.register_count; }
|
||||
EntityType register_type() const { return this->range_.register_type; }
|
||||
|
||||
protected:
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
|
||||
RegisterRange range_;
|
||||
};
|
||||
|
||||
/// A single modbus command. Each command is its own ModbusClientDevice: it sends its frame to the hub
|
||||
/// and the hub routes the response back to this object's on_modbus_* callbacks, so the controller no
|
||||
/// longer has to match responses to a FIFO queue.
|
||||
class ModbusCommandItem : public modbus::ModbusClientDevice {
|
||||
// The deprecated class references other deprecated names. Remove before 2027.3.0.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
class ESPDEPRECATED(
|
||||
"One-shot writes go through the entity write helpers (WriterDevice) or the modbus_client actions, and "
|
||||
"polling runs through PollingDevice. Removed in 2027.3.0",
|
||||
"2026.9.0") ModbusCommandItem : public modbus::ModbusClientDevice {
|
||||
public:
|
||||
/// Empty command with no controller connection (kept for source compatibility with value-type usage).
|
||||
ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address)
|
||||
@@ -489,6 +516,7 @@ class ModbusCommandItem : public modbus::ModbusClientDevice {
|
||||
const SmallInlineBuffer<8> *custom_pdu_{nullptr};
|
||||
ModbusController *controller_{nullptr};
|
||||
};
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
/// Whether an offline probe is due this update cycle: every offline_skip_updates + 1 cycles,
|
||||
/// anchored at the cycle the device went offline. Pure so the cadence (including update_counter
|
||||
@@ -522,14 +550,24 @@ class ModbusController final : public PollingComponent {
|
||||
modbus::ModbusClientHub *hub() const { return this->hub_; }
|
||||
uint8_t device_address() const { return this->address_; }
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
/// Queues a one-shot modbus command (writes, custom commands); taken by value, so std::move to avoid a copy.
|
||||
/// Remove with ModbusCommandItem before 2027.3.0.
|
||||
ESPDEPRECATED("Use the entity write helpers or the modbus_client actions instead. Removed in 2027.3.0", "2026.9.0")
|
||||
void queue_command(ModbusCommandItem command);
|
||||
/// Flags a finished one-shot command for removal. Called by the command as the last action of its own
|
||||
/// callback, so the item is not destroyed here (send() and the hub still touch it) but swept later.
|
||||
/// Remove with ModbusCommandItem before 2027.3.0.
|
||||
ESPDEPRECATED("Serves only ModbusCommandItem's own callbacks. Removed in 2027.3.0", "2026.9.0")
|
||||
void unqueue_command(const ModbusCommandItem *command);
|
||||
#pragma GCC diagnostic pop
|
||||
/// Registers a sensor with the controller. Called by esphomes code generator
|
||||
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
|
||||
/// Handles a write command acknowledgement (used by write command on_data_func handlers).
|
||||
/// Remove with ModbusCommandItem before 2027.3.0.
|
||||
ESPDEPRECATED("Write acknowledgements are handled by the writing entity's own device. Removed in 2027.3.0",
|
||||
"2026.9.0")
|
||||
void on_write_register_response(EntityType register_type, uint16_t start_address, std::span<const uint8_t> data);
|
||||
/// Update the online/offline state after a response or a run of timeouts, firing the callbacks.
|
||||
void set_online(bool online, int function_code, int register_address);
|
||||
@@ -568,32 +606,22 @@ class ModbusController final : public PollingComponent {
|
||||
const modbus::CommandOptions &read_options() const { return this->read_options_; }
|
||||
|
||||
protected:
|
||||
/// parse sensormap_ and create range of sequential addresses
|
||||
/// Group the registered sensors into contiguous ranges and create one polling command per range.
|
||||
/// Group the registered sensors into contiguous ranges and create one PollingDevice per range.
|
||||
void create_polling_commands_();
|
||||
/// build one persistent polling command from a range and add it to polling_command_items_
|
||||
void create_polling_command_(RegisterRange &&range) {
|
||||
// A custom range polls the first sensor's custom_pdu (referenced, not copied); the sensor constructor
|
||||
// decodes the real function code. The response still dispatches to every sensor in the range.
|
||||
if (range.register_type == EntityType::CUSTOM && !range.sensors.empty()) {
|
||||
auto &cmd = this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, *range.sensors.begin());
|
||||
cmd.sensors = std::move(range.sensors);
|
||||
} else {
|
||||
this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, std::move(range));
|
||||
}
|
||||
}
|
||||
/// The hub this controller's commands/entities send through, and the modbus address they target.
|
||||
modbus::ModbusClientHub *hub_{nullptr};
|
||||
uint8_t address_{0};
|
||||
/// Collection of all sensors for this component
|
||||
SensorSet sensorset_;
|
||||
/// One persistent command per register range, each its own ModbusClientDevice. Built once in setup()
|
||||
/// (create_polling_commands_ feeds each range straight in; the vector may reallocate as it grows, which
|
||||
/// is safe because no command has registered with the hub yet) and never appended to afterward, so the
|
||||
/// hub's device pointers stay valid once commands start sending.
|
||||
std::vector<ModbusCommandItem> polling_command_items_{};
|
||||
/// One persistent PollingDevice per register range. Built once in setup() with the exact count
|
||||
/// (FixedVector never reallocates), so the hub's device pointers stay valid once polls start sending.
|
||||
FixedVector<PollingDevice> polling_devices_;
|
||||
/// Dynamically queued one-shot commands (writes, custom commands). std::list keeps stable addresses.
|
||||
/// Remove with ModbusCommandItem before 2027.3.0.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
std::list<std::unique_ptr<ModbusCommandItem>> one_shot_command_items_;
|
||||
#pragma GCC diagnostic pop
|
||||
/// Erases one-shot commands flagged by unqueue_command(). Safe even when reached from inside a hub
|
||||
/// callback (via an on_online/on_offline/on_command_sent automation that queues a command): the
|
||||
/// destructor detaches via clear_tx_queue_for_device(), which the hub allows from callbacks, and the
|
||||
|
||||
@@ -41,7 +41,11 @@ MQTTClientComponent::MQTTClientComponent() {
|
||||
global_mqtt_client = this;
|
||||
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac_addr);
|
||||
this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr, MAC_ADDRESS_BUFFER_SIZE - 1);
|
||||
const StringRef &name = App.get_name();
|
||||
char client_id[MAX_NAME_WITH_SUFFIX_SIZE];
|
||||
size_t len = make_name_with_suffix_to(client_id, sizeof(client_id), name.c_str(), name.size(), '-', mac_addr,
|
||||
MAC_ADDRESS_BUFFER_SIZE - 1);
|
||||
this->credentials_.client_id.assign(client_id, len);
|
||||
}
|
||||
|
||||
// Connection
|
||||
|
||||
@@ -1462,7 +1462,7 @@ def hostname(value):
|
||||
Maximum length is 63 characters per RFC 1035.
|
||||
|
||||
Note: If this limit is changed, update MAX_NAME_WITH_SUFFIX_SIZE in
|
||||
esphome/core/helpers.cpp to accommodate the new maximum length.
|
||||
esphome/core/helpers.h to accommodate the new maximum length.
|
||||
"""
|
||||
value = string(value)
|
||||
if re.match(r"^[a-z0-9-]{1,63}$", value, re.IGNORECASE) is not None:
|
||||
|
||||
@@ -268,9 +268,6 @@ char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) {
|
||||
|
||||
// str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp
|
||||
|
||||
// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term)
|
||||
static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128;
|
||||
|
||||
size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep,
|
||||
const char *suffix_ptr, size_t suffix_len) {
|
||||
size_t total_len = name_len + 1 + suffix_len;
|
||||
@@ -291,17 +288,6 @@ size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *na
|
||||
return total_len;
|
||||
}
|
||||
|
||||
std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr,
|
||||
size_t suffix_len) {
|
||||
char buffer[MAX_NAME_WITH_SUFFIX_SIZE];
|
||||
size_t len = make_name_with_suffix_to(buffer, sizeof(buffer), name, name_len, sep, suffix_ptr, suffix_len);
|
||||
return std::string(buffer, len);
|
||||
}
|
||||
|
||||
std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) {
|
||||
return make_name_with_suffix(name.c_str(), name.size(), sep, suffix_ptr, suffix_len);
|
||||
}
|
||||
|
||||
// Parsing & formatting
|
||||
|
||||
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
|
||||
|
||||
+3
-21
@@ -1153,28 +1153,10 @@ inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Concatenate a name with a separator and suffix using an efficient stack-based approach.
|
||||
/// This avoids multiple heap allocations during string construction.
|
||||
/// Maximum name length supported is 120 characters for friendly names.
|
||||
/// @param name The base name string
|
||||
/// @param sep The separator character (e.g., '-', ' ', or '.')
|
||||
/// @param suffix_ptr Pointer to the suffix characters
|
||||
/// @param suffix_len Length of the suffix
|
||||
/// @return The concatenated string: name + sep + suffix
|
||||
std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len);
|
||||
/// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term)
|
||||
static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128;
|
||||
|
||||
/// Optimized string concatenation: name + separator + suffix (const char* overload)
|
||||
/// Uses a fixed stack buffer to avoid heap allocations.
|
||||
/// @param name The base name string
|
||||
/// @param name_len Length of the name
|
||||
/// @param sep Single character separator
|
||||
/// @param suffix_ptr Pointer to the suffix characters
|
||||
/// @param suffix_len Length of the suffix
|
||||
/// @return The concatenated string: name + sep + suffix
|
||||
std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr,
|
||||
size_t suffix_len);
|
||||
|
||||
/// Zero-allocation version: format name + separator + suffix directly into buffer.
|
||||
/// Format name + separator + suffix directly into buffer without heap allocation.
|
||||
/// @param buffer Output buffer (must have space for result + null terminator)
|
||||
/// @param buffer_size Size of the output buffer
|
||||
/// @param name The base name string
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ bleak==3.0.2
|
||||
smpclient==7.2.0
|
||||
requests==2.34.2
|
||||
py7zr==1.1.3
|
||||
platformdirs==4.11.3 # native esp-idf toolchain global cache dir
|
||||
platformdirs==4.11.4 # native esp-idf toolchain global cache dir
|
||||
ninja==1.13.0 # native esp8266 arduino toolchain build driver
|
||||
filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
|
||||
|
||||
|
||||
@@ -363,4 +363,47 @@ static void Snprintf_Uint32_Large(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(Snprintf_Uint32_Large);
|
||||
|
||||
// --- step_to_accuracy_decimals() ---
|
||||
// Called from climate traits and web_server for every number/climate step.
|
||||
|
||||
static void StepToAccuracyDecimals_Tenth(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += step_to_accuracy_decimals(0.1f);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(StepToAccuracyDecimals_Tenth);
|
||||
|
||||
static void StepToAccuracyDecimals_Whole(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += step_to_accuracy_decimals(1.0f);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(StepToAccuracyDecimals_Whole);
|
||||
|
||||
static void StepToAccuracyDecimals_Mixed(benchmark::State &state) {
|
||||
static constexpr float steps[] = {
|
||||
0.001f, 0.01f, 0.05f, 0.1f, 0.25f, 0.5f, 1.0f, 2.5f, 5.0f, 10.0f,
|
||||
};
|
||||
static constexpr int num_steps = sizeof(steps) / sizeof(steps[0]);
|
||||
for (auto _ : state) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += step_to_accuracy_decimals(steps[i % num_steps]);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(StepToAccuracyDecimals_Mixed);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
@@ -280,4 +281,71 @@ TEST(Base64, Rfc4648Vectors) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- step_to_accuracy_decimals() ---
|
||||
|
||||
TEST(StepToAccuracyDecimals, TypicalSteps) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.001f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.005f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.01f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.025f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.05f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.1f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.25f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.5f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1.5f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(2.5f), 1);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, WholeSteps) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(2.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(5.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(10.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(100.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1000.0f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, FiveSignificantDigits) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1.23456f), 4);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(12.345f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(123.45f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1234.5f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(12345.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.33333f), 5);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.0001f), 4);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, TrailingZerosDropped) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.3f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.7f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.125f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.0625f), 4);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, RoundsUpToWholeNumber) {
|
||||
// Rounds to five significant digits first, so this becomes 10 with no decimals.
|
||||
EXPECT_EQ(step_to_accuracy_decimals(9.999999f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, OutsideFixedNotationRange) {
|
||||
// %.5g prints these in exponent form, so the count comes from parsing "1e-05" or "1.2346e+05".
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.000125f), 6);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 8);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1000000.0f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, SignIgnored) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-0.1f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-0.25f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-1.0f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, NonFiniteAndZero) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(NAN), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(INFINITY), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
|
||||
#include "esphome/components/modbus_controller/modbus_controller.h"
|
||||
|
||||
// These tests pin the behaviour of the deprecated ModbusCommandItem until its removal.
|
||||
// Remove with ModbusCommandItem before 2027.3.0.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
namespace esphome::modbus_controller::testing {
|
||||
|
||||
// The coil write factory packs into an exact-size payload. Pinned at one past the protocol maximum
|
||||
@@ -29,3 +34,5 @@ TEST(ModbusCommandPayload, CoilWritePacksLsbFirstWithZeroPad) {
|
||||
}
|
||||
|
||||
} // namespace esphome::modbus_controller::testing
|
||||
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
Reference in New Issue
Block a user