[modbus] Add allow_broadcast_read and expect_broadcast_write_response options (#19304)

This commit is contained in:
Bonne Eggleston
2026-09-16 12:00:03 +12:00
committed by Jesse Hills
parent 45362dbc5b
commit fe0f04b2e4
21 changed files with 994 additions and 129 deletions
@@ -24,7 +24,7 @@ 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;
return this->parent_->queue_pdu(frame[0], frame.subspan(1), this);
return this->parent_->queue_pdu(frame[0], frame.subspan(1), this, this->write_options_);
}
void ControllerDevice::set_controller(ModbusController *controller) {
@@ -234,10 +234,13 @@ void ModbusCommandItem::on_sent(std::span<const uint8_t> request_pdu) {
// (frame[0]), which may differ from this controller's. (unqueue_command() is a no-op for a poll.)
// A custom polling command sends its PDU to this controller's own address, so only a factory custom
// command (a raw frame staged in payload) can carry a different address byte.
// An address-0 read with allow_broadcast_read is answered, so it keeps its terminal callback.
uint8_t wire_address = this->address_;
if (this->function_code_ == FunctionCode::CUSTOM && !this->payload.empty())
wire_address = this->payload.data()[0];
if (wire_address == modbus::BROADCAST_ADDRESS)
const bool answered = this->controller_->read_options().allow_broadcast_read &&
!modbus::helpers::is_function_code_broadcastable(request_pdu[0]);
if (wire_address == modbus::BROADCAST_ADDRESS && !answered)
this->controller_->unqueue_command(this);
}
@@ -285,8 +288,8 @@ void ModbusController::queue_command(ModbusCommandItem command) {
this->one_shot_command_items_.push_back(make_unique<ModbusCommandItem>(std::move(command)));
// A refused frame gets no terminal callback (see the hub contract), so reclaim the item here.
auto &item = this->one_shot_command_items_.back();
// We intentionally do not pass read_options_ here, because one-shot commands are usually writes, and are non-polling.
if (!item->send()) {
// One-shots never poll, so only the broadcast flag is passed (the hub strips it from writes).
if (!item->send({.allow_broadcast_read = this->read_options_.allow_broadcast_read})) {
// The caller (e.g. a write entity) has usually already published optimistically - surface the loss.
ESP_LOGW(TAG, "Command refused by hub: type=0x%X address=0x%X", static_cast<uint8_t>(item->register_type()),
item->register_address());
@@ -340,7 +343,7 @@ void ModbusController::update() {
if (this->can_send()) {
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).
// read_options_ carries the controller's read-side flags (the offline probe above sends them too).
// A refusal is already logged by the hub; note the affected range for controller-level diagnostics.
if (!poll.queue(this->read_options_)) {
ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", poll.register_address());