ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
modbus.h
Go to the documentation of this file.
1#pragma once
2
5
8
9#include <array>
10#include <cstring>
11#include <memory>
12#include <span>
13#include <vector>
14#include <deque>
15#include <optional>
16
17namespace esphome::modbus {
18
19static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15;
20static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
21
22// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
23// (address + 5-byte PDU + 2-byte CRC) and fit inline with no heap allocation.
24static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8;
25
27 // Frame held in a small-buffer-optimized buffer. Typical frames fit inline; only larger
28 // multi-register or custom frames spill to a single heap allocation. This keeps the common,
29 // high-frequency tx traffic off the heap entirely, avoiding per-frame alloc/free churn.
30 // The buffer tracks its own length, so no separate size field is needed.
31 SmallInlineBuffer<MODBUS_FRAME_INLINE_SIZE> data; // Modbus RTU max is 256 bytes
32
33 ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) {
34 uint8_t *buf = this->data.init(pdu_len + 3);
35 buf[0] = address;
36 memcpy(buf + 1, pdu, pdu_len);
37 auto crc = crc16(buf, pdu_len + 1);
38 buf[pdu_len + 1] = crc >> 0;
39 buf[pdu_len + 2] = crc >> 8;
40 }
41
42 uint16_t size() const { return static_cast<uint16_t>(this->data.size()); }
43};
44
45class Modbus : public uart::UARTDevice, public Component {
46 public:
47 Modbus() = default;
48
49 void setup() override;
50 void loop() override;
51
52 float get_setup_priority() const override;
53 virtual bool tx_blocked();
54
55 void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
56
57 protected:
58 void receive_bytes_();
59 bool timeout_();
60 virtual int32_t tx_delay_remaining();
61 virtual void parse_modbus_frames() = 0;
63 // pdu is the whole PDU (function code + payload, no address/CRC); pdu[0] is the (standard or custom) function code.
64 virtual void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) = 0;
65 void clear_rx_buffer_(const LogString *reason, bool warn = false, size_t bytes_to_clear = 0);
66 bool send_frame_(const ModbusFrame &frame);
67 // Scans forward from min_length to find a frame boundary by CRC match for custom function codes.
68 // Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE.
69 uint16_t find_custom_frame_end_(uint16_t min_length) const;
70
75 uint16_t frame_delay_ms_{5};
77
79
80 std::vector<uint8_t> rx_buffer_;
81};
82
85
94
95class ModbusClientHub : public Modbus {
96 public:
97 ModbusClientHub() = default;
98 void dump_config() override;
99 void loop() override;
100 void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; }
101 void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; }
102 bool tx_buffer_empty();
103 bool tx_blocked() override;
104 ESPDEPRECATED("Use send_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
105 void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
106 uint8_t payload_len = 0, const uint8_t *payload = nullptr, ModbusClientDevice *device = nullptr) {
107 this->send_pdu(address,
110 device);
111 };
112 void send_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr) {
113 this->queue_raw_(address, pdu.data(), pdu.size(), device);
114 }
115 void send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device = nullptr);
116 void clear_tx_queue_for_address(uint8_t address, bool clear_sent = true);
118
119 protected:
120 int32_t tx_delay_remaining() override;
121 void parse_modbus_frames() override;
122 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
123 void send_next_frame_();
124 // Notify the waiting device of no response; re-queues the frame if on_no_response() returns true.
125 // wfr is the caller's checked reference to waiting_for_response_.
128 void queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device = nullptr);
129
130 uint16_t send_wait_time_{2000};
132 std::optional<ModbusDeviceCommand> waiting_for_response_;
133
134 // std::deque is appropriate here since we need a FIFO buffer, and we can't know ahead of time how many
135 // requests will be queued. Each modbus component may queue multiple requests, and the sequence of scheduling
136 // may change at run time.
137 std::deque<ModbusDeviceCommand> tx_buffer_;
138};
139
140class ModbusServerHub : public Modbus {
141 public:
142 ModbusServerHub() = default;
143 void dump_config() override;
144 void register_device(ModbusServerDevice *device) { this->devices_.push_back(device); }
145
146 protected:
147 void parse_modbus_frames() override;
149 void process_modbus_server_frame(uint8_t address, std::span<const uint8_t> pdu) override;
150 void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data);
152 // Returns true if [start_address, start_address + number_of_registers) fits in the 16-bit address space.
153 // On failure, logs and sends an ILLEGAL_DATA_ADDRESS exception to the client.
154 bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address,
155 uint16_t number_of_registers);
156 void send_raw_(const uint8_t *payload, uint16_t len);
157 void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code);
158 void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
160 std::vector<ModbusServerDevice *> devices_;
161
162 // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time.
163 // Only one server reply can be in flight at once, so a single fixed buffer avoids heap allocation.
164 std::array<uint8_t, MAX_RAW_SIZE> deferred_payload_;
166};
167
169 public:
173 if (this->parent_ != nullptr)
175 }
180 void set_parent(ModbusClientHub *parent) { this->parent_ = parent; }
181 void set_address(uint8_t address) { this->address_ = address; }
185 virtual void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {}
187 virtual void on_error(std::span<const uint8_t> request_pdu, ModbusExceptionCode exception_code) {}
188 // The on_modbus_* names are signature-identical renames, so the new defaults forward to the old
189 // virtuals: external devices overriding the old names keep working through the deprecation window.
190 // Remove the forwards together with the deprecated names.
191 virtual void on_not_sent() {
192#pragma GCC diagnostic push
193#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
194 this->on_modbus_not_sent();
195#pragma GCC diagnostic pop
196 }
200 virtual bool on_no_response() {
201#pragma GCC diagnostic push
202#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
203 return this->on_modbus_no_response();
204#pragma GCC diagnostic pop
205 }
206 // Remove before 2027.2.0
207 ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0")
208 virtual void on_modbus_not_sent() {}
209 // Remove before 2027.2.0
210 ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0")
211 virtual bool on_modbus_no_response() { return false; }
212 void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
213 const uint8_t *payload = nullptr) {
214 this->parent_->send_pdu(this->address_,
215 helpers::create_client_pdu((ModbusFunctionCode) function, start_address, number_of_entities,
216 payload, payload_len),
217 this);
218 }
219 void send_pdu(std::span<const uint8_t> pdu) { this->parent_->send_pdu(this->address_, pdu, this); }
220 void send_raw(const std::vector<uint8_t> &payload) { this->parent_->send_raw(payload, this); }
221 inline void clear_tx_queue_for_address(bool clear_sent = true) {
222 this->parent_->clear_tx_queue_for_address(this->address_, clear_sent);
223 }
225
226 // If more than one device is connected block sending a new command before a response is received
227 ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0")
228 bool waiting_for_response() { return !this->ready_for_immediate_send(); }
229 bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
230
231 protected:
233 uint8_t address_{0};
234};
235
236// This is for compatibility with external components using the former class name
237// Remove before 2026.12.0
238using ModbusDevice ESPDEPRECATED("Use ModbusClientDevice instead. Removed in 2026.12.0",
239 "2026.6.0") = ModbusClientDevice;
240
241// Transaction status: std::nullopt on success, otherwise the Modbus exception code. Server handlers return it;
242// (future) client response callbacks receive it. Named without a side prefix so both directions share it.
243using ResponseStatus = std::optional<ModbusExceptionCode>;
244// Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol
245// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
246// the capacity of this type.
248
250 public:
251 virtual ~ModbusServerDevice() = default;
253 // Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
258 void set_address(uint8_t address) { this->address_ = address; }
259 uint8_t get_address() const { return this->address_; }
260 virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers,
261 RegisterValues &registers) {
263 };
264 virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
265 RegisterValues &registers) {
266 return this->on_read_registers(start_address, number_of_registers, registers);
267 };
268 virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
269 RegisterValues &registers) {
270 return this->on_read_registers(start_address, number_of_registers, registers);
271 };
272 virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers) {
274 };
275
276 protected:
277 uint8_t address_{0};
278};
279
280} // namespace esphome::modbus
uint8_t address
Definition bl0906.h:4
Small buffer optimization - stores data inline when small, heap-allocates for large data This avoids ...
Definition helpers.h:147
uint8_t * init(size_t size)
Resize to size bytes of (uninitialized) storage and return a writable pointer to fill.
Definition helpers.h:190
size_t size() const
Definition helpers.h:209
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:222
virtual void on_response(std::span< const uint8_t > request_pdu, std::span< const uint8_t > response_pdu)
Called with the request PDU this device sent and the response PDU received (both: function code + dat...
Definition modbus.h:185
void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len=0, const uint8_t *payload=nullptr)
Definition modbus.h:212
ModbusClientDevice & operator=(ModbusClientDevice &&)=delete
void send_pdu(std::span< const uint8_t > pdu)
Definition modbus.h:219
ModbusClientDevice(const ModbusClientDevice &)=delete
ModbusClientDevice & operator=(const ModbusClientDevice &)=delete
void set_parent(ModbusClientHub *parent)
Definition modbus.h:180
ModbusClientDevice(ModbusClientDevice &&)=delete
ESPDEPRECATED("Override on_no_response() instead. Removed in 2027.2.0", "2026.8.0") virtual bool on_modbus_no_response()
Definition modbus.h:210
ModbusClientDevice(ModbusClientHub *parent, uint8_t address)
Definition modbus.h:171
void set_address(uint8_t address)
Definition modbus.h:181
virtual void on_error(std::span< const uint8_t > request_pdu, ModbusExceptionCode exception_code)
Called with the request PDU and the modbus exception code decoded from the error response.
Definition modbus.h:187
ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0") bool waiting_for_response()
Definition modbus.h:227
void send_raw(const std::vector< uint8_t > &payload)
Definition modbus.h:220
void clear_tx_queue_for_address(bool clear_sent=true)
Definition modbus.h:221
virtual bool on_no_response()
Called when no (valid) response arrived; return true to have the hub re-queue the frame for a retry.
Definition modbus.h:200
ESPDEPRECATED("Override on_not_sent() instead. Removed in 2027.2.0", "2026.8.0") virtual void on_modbus_not_sent()
Definition modbus.h:207
std::optional< ModbusDeviceCommand > waiting_for_response_
Definition modbus.h:132
uint8_t uint16_t start_address
Definition modbus.h:105
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:638
void parse_modbus_frames() override
Definition modbus.cpp:139
void requeue_waiting_frame_(ModbusDeviceCommand &wfr)
Definition modbus.cpp:586
void notify_no_response_(ModbusDeviceCommand &wfr)
Definition modbus.cpp:574
uint8_t uint16_t uint16_t uint8_t const uint8_t ModbusClientDevice * device
Definition modbus.h:106
void clear_tx_queue_for_address(uint8_t address, bool clear_sent=true)
Definition modbus.cpp:622
uint8_t uint16_t uint16_t number_of_entities
Definition modbus.h:105
uint8_t uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:106
void queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device=nullptr)
Definition modbus.cpp:599
int32_t tx_delay_remaining() override
Definition modbus.cpp:92
void send_raw(const std::vector< uint8_t > &payload, ModbusClientDevice *device=nullptr)
Definition modbus.cpp:654
ESPDEPRECATED("Use send_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0") void send(uint8_t address
std::deque< ModbusDeviceCommand > tx_buffer_
Definition modbus.h:137
uint8_t uint16_t uint16_t uint8_t payload_len
Definition modbus.h:106
void send_pdu(uint8_t address, std::span< const uint8_t > pdu, ModbusClientDevice *device=nullptr)
Definition modbus.h:112
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:268
void set_send_wait_time(uint16_t time_in_ms)
Definition modbus.h:100
void set_turnaround_time(uint16_t time_in_ms)
Definition modbus.h:101
void set_flow_control_pin(GPIOPin *flow_control_pin)
Definition modbus.h:55
void setup() override
Definition modbus.cpp:18
uint16_t frame_delay_ms_
Definition modbus.h:75
virtual void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu)=0
bool parse_modbus_server_frame_()
Definition modbus.cpp:195
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
Definition modbus.cpp:464
uint32_t last_modbus_byte_
Definition modbus.h:71
GPIOPin * flow_control_pin_
Definition modbus.h:78
uint32_t last_send_tx_offset_
Definition modbus.h:74
virtual bool tx_blocked()
Definition modbus.cpp:100
void clear_rx_buffer_(const LogString *reason, bool warn=false, size_t bytes_to_clear=0)
Definition modbus.cpp:692
void loop() override
Definition modbus.cpp:39
float get_setup_priority() const override
Definition modbus.cpp:546
uint16_t long_rx_buffer_delay_ms_
Definition modbus.h:76
virtual int32_t tx_delay_remaining()
Definition modbus.cpp:80
std::vector< uint8_t > rx_buffer_
Definition modbus.h:80
uint16_t find_custom_frame_end_(uint16_t min_length) const
Definition modbus.cpp:183
uint32_t last_receive_check_
Definition modbus.h:72
ModbusServerDevice & operator=(ModbusServerDevice &&)=delete
ModbusServerDevice(const ModbusServerDevice &)=delete
ModbusServerDevice & operator=(const ModbusServerDevice &)=delete
void set_address(uint8_t address)
Definition modbus.h:258
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:272
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:268
virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:260
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:264
ModbusServerDevice(ModbusServerDevice &&)=delete
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:160
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data)
Definition modbus.cpp:363
void parse_modbus_frames() override
Definition modbus.cpp:152
void process_modbus_server_frame(uint8_t address, std::span< const uint8_t > pdu) override
Definition modbus.cpp:327
bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_registers)
Definition modbus.cpp:352
void register_device(ModbusServerDevice *device)
Definition modbus.h:144
ModbusServerDevice * find_device_(uint8_t address)
Definition modbus.cpp:343
void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code)
Definition modbus.cpp:566
void send_raw_(const uint8_t *payload, uint16_t len)
Definition modbus.cpp:664
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
Definition modbus.cpp:551
std::array< uint8_t, MAX_RAW_SIZE > deferred_payload_
Definition modbus.h:164
StaticVector< uint8_t, MAX_PDU_SIZE > create_client_pdu(ModbusFunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, const uint8_t *values, size_t values_len)
Create a modbus clinet pdu for reading/writing single/multiple coils/register/inputs.
std::optional< ModbusExceptionCode > ResponseStatus
Definition modbus.h:243
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.
Definition helpers.cpp:86
const void size_t len
Definition hal.h:64
const void * src
Definition hal.h:64
static void uint32_t
ModbusDeviceCommand(ModbusClientDevice *device, uint8_t address, const uint8_t *src, uint16_t len)
Definition modbus.h:91
ModbusClientDevice * device
Definition modbus.h:87
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len)
Definition modbus.h:33
SmallInlineBuffer< MODBUS_FRAME_INLINE_SIZE > data
Definition modbus.h:31
uint16_t size() const
Definition modbus.h:42