25static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128;
26static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
30static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8;
40 uint8_t *buf = this->data.
init(pdu_len + 3);
42 memcpy(buf + 1,
pdu, pdu_len);
43 auto crc =
crc16(buf, pdu_len + 1);
44 buf[pdu_len + 1] = crc >> 0;
45 buf[pdu_len + 2] = crc >> 8;
48 uint16_t
size()
const {
return static_cast<uint16_t
>(this->data.
size()); }
55 std::span<const uint8_t>
pdu()
const {
return std::span<const uint8_t>(this->data.
data() + 1, this->size() - 3u); }
62 void setup()
override;
78 void clear_rx_buffer_(
const LogString *reason,
bool warn =
false,
size_t bytes_to_clear = 0);
162 const uint8_t fc = this->frame.
pdu()[0];
164 return (requeueable && !this->
continuous) ? 2 : 1;
172 this->device =
nullptr;
199 this->continuous =
true;
203 this->continuous =
false;
249 bool response(std::span<const uint8_t> response_pdu);
257 const auto own_pdu = this->frame.
pdu();
258 return own_pdu.size() == pdu.size() && this->frame.
address() ==
address &&
259 memcmp(own_pdu.data(), pdu.data(), pdu.size()) == 0;
267 void loop()
override;
272 ESPDEPRECATED(
"Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0",
"2026.4.0")
291 ESPDEPRECATED(
"Use queue_pdu() instead - the call queues a request, it does not send one, and it "
292 "reports whether the request was accepted. Removed in 2027.2.0",
297 ESPDEPRECATED(
"Use queue_pdu(payload[0], <pdu bytes>, device) instead. Removed in 2027.2.0",
"2026.8.0")
382 uint16_t &start_address, uint16_t &count);
392 std::span<const uint8_t> &packed_bytes);
401 std::span<uint8_t> response_buffer, uint16_t &response_len);
407 void send_response_(uint8_t
address, uint8_t function_code,
const uint8_t *payload, uint16_t payload_len);
446 this->clear_tx_queue_for_device();
458 virtual void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
459 this->dispatch_response_(request_pdu, response_pdu, std::nullopt);
465 this->dispatch_response_(request_pdu, {}, exception_code);
470#pragma GCC diagnostic push
471#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
472 this->on_modbus_not_sent();
473#pragma GCC diagnostic pop
476 virtual void on_sent(std::span<const uint8_t> request_pdu) {}
480#pragma GCC diagnostic push
481#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
482 return this->on_modbus_no_response();
483#pragma GCC diagnostic pop
486 ESPDEPRECATED(
"Override on_not_sent() instead. Removed in 2027.2.0",
"2026.8.0")
487 virtual
void on_modbus_not_sent() {}
489 ESPDEPRECATED(
"Override on_no_response() instead. Removed in 2027.2.0",
"2026.8.0")
490 virtual
bool on_modbus_no_response() {
return false; }
534 virtual void on_custom_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
536 ESPDEPRECATED(
"Use the typed read_*/write_* helpers or queue_pdu() instead. Removed in 2027.2.0",
"2026.8.0")
552 ESPDEPRECATED(
"Use queue_pdu() instead - the call queues a request, it does not send one, and it "
553 "reports whether the request was accepted. Removed in 2027.2.0",
555 void send_pdu(
std::span<const uint8_t> pdu) { this->queue_pdu(pdu); }
556 ESPDEPRECATED(
"Use queue_pdu() instead (the device address is prepended for you). Removed in 2027.2.0",
"2026.8.0")
557 void send_raw(const
std::vector<uint8_t> &
payload) {
560 this->parent_->queue_pdu(
payload[0], std::span<const uint8_t>(
payload).subspan(1),
this);
574 bool read_input_registers(uint16_t
start_address, uint16_t number_of_registers, CommandOptions
options = {}) {
575 return this->queue_pdu(
578 bool read_holding_registers(uint16_t
start_address, uint16_t number_of_registers, CommandOptions
options = {}) {
579 return this->queue_pdu(
582 bool read_coils(uint16_t
start_address, uint16_t number_of_coils, CommandOptions
options = {}) {
585 bool read_discrete_inputs(uint16_t
start_address, uint16_t number_of_inputs, CommandOptions
options = {}) {
586 return this->queue_pdu(
589 bool write_single_register(uint16_t
start_address, uint16_t value) {
592 bool write_single_coil(uint16_t
address,
bool value) {
595 bool write_multiple_registers(uint16_t
start_address, std::span<const uint16_t> values) {
600 bool write_multiple_coils(uint16_t
start_address, std::span<const bool> values) {
605 bool write_multiple_coils(uint16_t
start_address, PackedBits bits) {
613 bool read_write_multiple_registers(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address,
614 std::span<const uint16_t> write_values) {
616 write_start_address, write_values));
622 ESPDEPRECATED(
"Use ready_for_immediate_send() instead. Removed in 2026.9.0",
"2026.3.0")
623 bool waiting_for_response() {
return !this->ready_for_immediate_send(); }
628 void dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
642class ESPDEPRECATED(
"Subclass ModbusClientDevice and override on_response()/on_error() instead. Removed in 2027.2.0",
643 "2026.8.0") ModbusDevice :
public ModbusClientDevice {
645 using ModbusClientDevice::ModbusClientDevice;
646 virtual void on_modbus_data(
const std::vector<uint8_t> &data) {}
647 virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
649 void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu)
override {
653 auto payload = !response_pdu.empty() && helpers::is_function_code_custom(response_pdu[0])
655 : helpers::server_pdu_payload(response_pdu);
656 this->on_modbus_data(std::vector<uint8_t>(payload.begin(), payload.end()));
658 void on_error(std::span<const uint8_t> request_pdu, ExceptionCode exception_code)
override {
659 this->on_modbus_error(request_pdu.empty() ? 0 : request_pdu[0],
static_cast<uint8_t
>(exception_code));
Small buffer optimization - stores data inline when small, heap-allocates for large data This avoids ...
uint8_t * init(size_t size)
Resize to size bytes of (uninitialized) storage and return a writable pointer to fill.
Minimal static vector - saves memory by avoiding std::vector overhead.
virtual void on_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu)
Low-level response hook: called with the request PDU this device sent and the response PDU received T...
virtual void on_write_multiple_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
ModbusClientDevice()=default
ModbusClientDevice & operator=(ModbusClientDevice &&)=delete
virtual void on_read_holding_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
virtual void on_write_multiple_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
virtual void on_sent(std::span< const uint8_t > request_pdu)
Called when this device's frame is actually written to the wire.
ModbusClientDevice(const ModbusClientDevice &)=delete
virtual void on_write_single_register(uint16_t address, uint16_t value, ResponseStatus status)
Write acknowledgements.
uint16_t uint16_t uint8_t payload_len
virtual void on_read_bits(EntityType entity_type, uint16_t start_address, PackedBits bits, ResponseStatus status)
Coil/discrete-input reads are delivered as a PackedBits view (bit 0 = the bit at start_address,...
ModbusClientDevice & operator=(const ModbusClientDevice &)=delete
void set_parent(ModbusClientHub *parent)
ESPDEPRECATED("Use the typed read_*/write_* helpers or queue_pdu() instead. Removed in 2027.2.0", "2026.8.0") void send(uint8_t function
virtual bool on_no_response(std::span< const uint8_t > request_pdu)
Called when no matching, uninterrupted response arrived; return true to have the hub re-queue the fra...
virtual void on_custom_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu, ResponseStatus status)
Catch-all for custom function codes and anything that is not a standard-conformant transaction (see d...
ModbusClientDevice(ModbusClientDevice &&)=delete
virtual void on_read_discrete_inputs(uint16_t start_address, PackedBits bits, ResponseStatus status)
uint16_t uint16_t number_of_entities
ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0") virtual bool on_modbus_no_response()
virtual void on_read_registers(EntityType entity_type, uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
High-level typed response callbacks, fired by the default on_response()/on_error() with arguments par...
virtual void on_error(std::span< const uint8_t > request_pdu, ExceptionCode exception_code)
Low-level error hook: called with the request PDU and the modbus exception code from the error respon...
ModbusClientDevice(ModbusClientHub *parent, uint8_t address)
virtual ~ModbusClientDevice()
void set_address(uint8_t address)
virtual void on_not_sent(std::span< const uint8_t > request_pdu)
Called when an accepted request was dropped before transmission by clear_tx_queue_for_address().
ModbusClientHub * parent_
virtual void on_read_coils(uint16_t start_address, PackedBits bits, ResponseStatus status)
virtual void on_write_single_coil(uint16_t address, bool value, ResponseStatus status)
bool custom_response_warned_
uint16_t uint16_t uint8_t const uint8_t * payload
virtual void on_read_input_registers(uint16_t start_address, std::span< const uint16_t > registers, ResponseStatus status)
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0") virtual void on_modbus_not_sent()
uint8_t uint16_t start_address
void clear_tx_queue_for_device(ModbusClientDevice *device)
void parse_modbus_frames() override
ModbusDeviceCommand * find_waiting_()
std::span< const uint8_t > pdu
void dump_config() override
uint16_t turnaround_delay_ms_
uint8_t uint16_t uint16_t uint8_t const uint8_t ModbusClientDevice * device
ModbusClientHub()=default
uint8_t uint16_t uint16_t number_of_entities
bool waiting_for_response_
ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it " "reports whether the request was accepted. Removed in 2027.2.0", "2026.8.0") void send_pdu(uint8_t address
ModbusDeviceCommand * select_next_ready_()
uint8_t uint16_t uint16_t uint8_t const uint8_t * payload
int32_t tx_delay_remaining() override
std::deque< ModbusDeviceCommand > tx_buffer_
bool tx_blocked() override
ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0") void send(uint8_t address
uint8_t uint16_t uint16_t uint8_t payload_len
bool queue_pdu(uint8_t address, std::span< const uint8_t > pdu, ModbusClientDevice *device=nullptr, CommandOptions options={})
Queue a request.
void clear_tx_queue_for_address(uint8_t address)
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
void set_send_wait_time(uint16_t time_in_ms)
void set_turnaround_time(uint16_t time_in_ms)
void set_flow_control_pin(GPIOPin *flow_control_pin)
virtual void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu)=0
bool parse_modbus_server_frame_()
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
uint32_t last_modbus_byte_
GPIOPin * flow_control_pin_
uint32_t last_send_tx_offset_
virtual bool tx_blocked()
void clear_rx_buffer_(const LogString *reason, bool warn=false, size_t bytes_to_clear=0)
float get_setup_priority() const override
uint16_t long_rx_buffer_delay_ms_
virtual int32_t tx_delay_remaining()
uint16_t find_frame_end_by_crc_(uint16_t min_length) const
std::vector< uint8_t > rx_buffer_
uint32_t last_receive_check_
ModbusServerDevice & operator=(ModbusServerDevice &&)=delete
virtual ~ModbusServerDevice()=default
virtual ResponseStatus on_read_coils(uint16_t start_address, MutablePackedBits bits)
ModbusServerDevice(const ModbusServerDevice &)=delete
ModbusServerDevice & operator=(const ModbusServerDevice &)=delete
ModbusServerDevice()=default
void set_address(uint8_t address)
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues ®isters)
virtual ResponseStatus on_read_bits(uint16_t start_address, MutablePackedBits bits)
Coil/discrete-input reads: set the requested bits (bit 0 = the coil at start_address) with bits....
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues ®isters)
virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues ®isters)
virtual ResponseStatus on_read_discrete_inputs(uint16_t start_address, MutablePackedBits bits)
virtual ResponseStatus on_write_coils(uint16_t start_address, PackedBits bits)
Coil writes deliver the values as a PackedBits view over the hub's receive buffer (only valid during ...
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues ®isters)
uint8_t get_address() const
ModbusServerDevice(ModbusServerDevice &&)=delete
std::vector< ModbusServerDevice * > devices_
ResponseStatus check_address_range_(uint16_t start_address, uint16_t count)
ResponseStatus parse_read_request_(std::span< const uint8_t > data, uint16_t max_entities, const LogString *entity_name, uint16_t &start_address, uint16_t &count)
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span< const uint8_t > data)
void parse_modbus_frames() override
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
ResponseStatus parse_write_multiple_coils_(std::span< const uint8_t > data, uint16_t &start_address, uint16_t &count, std::span< const uint8_t > &packed_bytes)
void process_broadcast_frame_(uint8_t function_code, std::span< const uint8_t > data)
ModbusServerHub()=default
uint16_t deferred_payload_len_
void register_device(ModbusServerDevice *device)
ModbusServerDevice * find_device_(uint8_t address)
uint32_t last_unaccepted_broadcast_warn_
ResponseStatus parse_write_single_coil_(std::span< const uint8_t > data, uint16_t &start_address, bool &value)
bool build_or_reject_read_response_(uint8_t address, uint8_t function_code, ResponseStatus status, uint16_t number_of_registers, const RegisterValues ®isters, std::span< uint8_t > response_buffer, uint16_t &response_len)
void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code)
void assemble_registers_(std::span< const uint8_t > values, RegisterValues ®isters)
void send_raw_(const uint8_t *payload, uint16_t len)
bool parse_modbus_client_frame_()
void dump_config() override
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
ResponseStatus parse_write_multiple_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues ®isters)
bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status)
ResponseStatus parse_write_single_(std::span< const uint8_t > data, uint16_t &start_address, RegisterValues ®isters)
std::array< uint8_t, MAX_RAW_SIZE > deferred_payload_
uint8_t expecting_peer_response_
Mutable counterpart of PackedBits: set() writes bits in place (deliberately no proxy operator[]=).
Read-only view of Modbus-packed bits: bit 0 of byte 0 is the first bit (LSB first),...
PduBuffer create_write_coils_pdu(uint16_t start_address, PackedBits bits)
Create modbus write multiple coils command (function 0x0F) from bits packed as on the wire.
bool is_function_code_read_only(uint8_t function_code)
WriteSinglePdu create_write_single_coil_pdu(uint16_t address, bool value)
Create modbus write single coil command Function 0x05 Write Single Coil.
ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities)
Create a modbus read request PDU.
bool is_function_code_write(uint8_t function_code)
WriteSinglePdu create_write_single_register_pdu(uint16_t start_address, uint16_t value)
Create modbus write single register command Function 0x06 Write Single Register.
PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, const uint8_t *values, size_t values_len)
Create a modbus client pdu for reading/writing single/multiple coils/register/inputs.
FunctionCode modbus_register_read_function(EntityType reg_type)
PduBuffer create_write_registers_pdu(uint16_t start_address, std::span< const uint16_t > values)
Create modbus write multiple registers command Function 0x10 Write Multiple Registers.
PduBuffer create_read_write_multiple_registers_pdu(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address, std::span< const uint16_t > write_values)
Create modbus read/write multiple registers command Function 0x17 Read/Write Multiple Registers Write...
bool is_function_code_exception(uint8_t function_code)
std::optional< ExceptionCode > ResponseStatus
bool succeeded(ResponseStatus status)
True when a transaction carried no exception.
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout)
Calculate a CRC-16 checksum of data with size len.
bool response(std::span< const uint8_t > response_pdu)
void complete_broadcast()
CommandPriority priority() const
void make_continuous(bool continuous)
static CommandPriority classify(uint8_t function_code)
bool error(ExceptionCode exception_code)
bool waiting_state() const
void requeue(uint16_t seq)
bool same_frame(uint8_t address, std::span< const uint8_t > pdu) const
True if this command carries the same wire frame (address + PDU) as the given one.
ModbusClientDevice * device
uint8_t max_pending() const
ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, std::span< const uint8_t > pdu, bool continuous=false, uint16_t seq=0)
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len)
SmallInlineBuffer< MODBUS_FRAME_INLINE_SIZE > data
std::span< const uint8_t > pdu() const
The PDU: function code + data, without address or CRC.