ESPHome 2026.7.4
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 <vector>
13#include <deque>
14#include <optional>
15
16namespace esphome::modbus {
17
18static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15;
19static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
20
21// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
22// (address + 5-byte PDU + 2-byte CRC) and fit inline with no heap allocation.
23static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8;
24
26 // Frame held in a small-buffer-optimized buffer. Typical frames fit inline; only larger
27 // multi-register or custom frames spill to a single heap allocation. This keeps the common,
28 // high-frequency tx traffic off the heap entirely, avoiding per-frame alloc/free churn.
29 // The buffer tracks its own length, so no separate size field is needed.
30 SmallInlineBuffer<MODBUS_FRAME_INLINE_SIZE> data; // Modbus RTU max is 256 bytes
31
32 ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) {
33 uint8_t *buf = this->data.init(pdu_len + 3);
34 buf[0] = address;
35 memcpy(buf + 1, pdu, pdu_len);
36 auto crc = crc16(buf, pdu_len + 1);
37 buf[pdu_len + 1] = crc >> 0;
38 buf[pdu_len + 2] = crc >> 8;
39 }
40
41 uint16_t size() const { return static_cast<uint16_t>(this->data.size()); }
42};
43
44class Modbus : public uart::UARTDevice, public Component {
45 public:
46 Modbus() = default;
47
48 void setup() override;
49 void loop() override;
50
51 float get_setup_priority() const override;
52 virtual bool tx_blocked();
53
54 void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
55
56 protected:
57 void receive_bytes_();
58 bool timeout_();
59 virtual int32_t tx_delay_remaining();
60 virtual void parse_modbus_frames() = 0;
62 virtual void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data,
63 uint16_t len) = 0;
64 void clear_rx_buffer_(const LogString *reason, bool warn = false, size_t bytes_to_clear = 0);
65 bool send_frame_(const ModbusFrame &frame);
66 // Scans forward from min_length to find a frame boundary by CRC match for custom function codes.
67 // Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE.
68 uint16_t find_custom_frame_end_(uint16_t min_length) const;
69
74 uint16_t frame_delay_ms_{5};
76
78
79 std::vector<uint8_t> rx_buffer_;
80};
81
84
93
94class ModbusClientHub : public Modbus {
95 public:
96 ModbusClientHub() = default;
97 void dump_config() override;
98 void loop() override;
99 void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; }
100 void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; }
101 bool tx_buffer_empty();
102 bool tx_blocked() override;
103 ESPDEPRECATED("Use send_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
104 void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
105 uint8_t payload_len = 0, const uint8_t *payload = nullptr, ModbusClientDevice *device = nullptr) {
106 this->send_pdu(address,
109 device);
110 };
111 void send_pdu(uint8_t address, std::span<const uint8_t> pdu, ModbusClientDevice *device = nullptr) {
112 this->queue_raw_(address, pdu.data(), pdu.size(), device);
113 }
114 void send_raw(const std::vector<uint8_t> &payload, ModbusClientDevice *device = nullptr);
115 void clear_tx_queue_for_address(uint8_t address, bool clear_sent = true);
117
118 protected:
119 int32_t tx_delay_remaining() override;
120 void parse_modbus_frames() override;
121 // Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here.
122 void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override;
123 void send_next_frame_();
124 // Notify the waiting device of no response; re-queues the frame if on_modbus_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 // Parsers need to handle standard (ModbusFunctionCode) and custom (uint8_t) function codes, so we use uint8_t here.
150 void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override;
151 void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data);
153 // Returns true if [start_address, start_address + number_of_registers) fits in the 16-bit address space.
154 // On failure, logs and sends an ILLEGAL_DATA_ADDRESS exception to the client.
155 bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address,
156 uint16_t number_of_registers);
157 void send_raw_(const uint8_t *payload, uint16_t len);
158 void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code);
159 void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len);
161 std::vector<ModbusServerDevice *> devices_;
162
163 // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time.
164 // Only one server reply can be in flight at once, so a single fixed buffer avoids heap allocation.
165 std::array<uint8_t, MAX_RAW_SIZE> deferred_payload_;
167};
168
170 public:
174 if (this->parent_ != nullptr)
176 }
181 void set_parent(ModbusClientHub *parent) { this->parent_ = parent; }
182 void set_address(uint8_t address) { this->address_ = address; }
183 virtual void on_modbus_data(const std::vector<uint8_t> &data) {}
184 virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code) {}
185 virtual void on_modbus_not_sent() {}
189 virtual bool on_modbus_no_response() { return false; }
190 void send(uint8_t function, uint16_t start_address, uint16_t number_of_entities, uint8_t payload_len = 0,
191 const uint8_t *payload = nullptr) {
192 this->parent_->send_pdu(this->address_,
193 helpers::create_client_pdu((ModbusFunctionCode) function, start_address, number_of_entities,
194 payload, payload_len),
195 this);
196 }
197 void send_pdu(std::span<const uint8_t> pdu) { this->parent_->send_pdu(this->address_, pdu, this); }
198 void send_raw(const std::vector<uint8_t> &payload) { this->parent_->send_raw(payload, this); }
199 inline void clear_tx_queue_for_address(bool clear_sent = true) {
200 this->parent_->clear_tx_queue_for_address(this->address_, clear_sent);
201 }
203
204 // If more than one device is connected block sending a new command before a response is received
205 ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0")
206 bool waiting_for_response() { return !this->ready_for_immediate_send(); }
207 bool ready_for_immediate_send() { return this->parent_->tx_buffer_empty() && !this->parent_->tx_blocked(); }
208
209 protected:
211 uint8_t address_{0};
212};
213
214// This is for compatibility with external components using the former class name
215// Remove before 2026.12.0
216using ModbusDevice ESPDEPRECATED("Use ModbusClientDevice instead. Removed in 2026.12.0",
217 "2026.6.0") = ModbusClientDevice;
218
219// Transaction status: std::nullopt on success, otherwise the Modbus exception code. Server handlers return it;
220// (future) client response callbacks receive it. Named without a side prefix so both directions share it.
221using ResponseStatus = std::optional<ModbusExceptionCode>;
222// Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol
223// maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by
224// the capacity of this type.
226
228 public:
229 virtual ~ModbusServerDevice() = default;
231 // Polymorphic base: non-copyable and non-movable to prevent slicing (Rule of Five).
236 void set_address(uint8_t address) { this->address_ = address; }
237 uint8_t get_address() const { return this->address_; }
238 virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers,
239 RegisterValues &registers) {
241 };
242 virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers,
243 RegisterValues &registers) {
244 return this->on_read_registers(start_address, number_of_registers, registers);
245 };
246 virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers,
247 RegisterValues &registers) {
248 return this->on_read_registers(start_address, number_of_registers, registers);
249 };
250 virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers) {
252 };
253
254 protected:
255 uint8_t address_{0};
256};
257
258} // 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 bool on_modbus_no_response()
Called when no (valid) response arrived; return true to have the hub re-queue the frame for a retry.
Definition modbus.h:189
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:190
ModbusClientDevice & operator=(ModbusClientDevice &&)=delete
void send_pdu(std::span< const uint8_t > pdu)
Definition modbus.h:197
ModbusClientDevice(const ModbusClientDevice &)=delete
virtual void on_modbus_data(const std::vector< uint8_t > &data)
Definition modbus.h:183
ModbusClientDevice & operator=(const ModbusClientDevice &)=delete
void set_parent(ModbusClientHub *parent)
Definition modbus.h:181
ModbusClientDevice(ModbusClientDevice &&)=delete
ModbusClientDevice(ModbusClientHub *parent, uint8_t address)
Definition modbus.h:172
void set_address(uint8_t address)
Definition modbus.h:182
ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0") bool waiting_for_response()
Definition modbus.h:205
virtual void on_modbus_error(uint8_t function_code, uint8_t exception_code)
Definition modbus.h:184
void send_raw(const std::vector< uint8_t > &payload)
Definition modbus.h:198
void clear_tx_queue_for_address(bool clear_sent=true)
Definition modbus.h:199
std::optional< ModbusDeviceCommand > waiting_for_response_
Definition modbus.h:132
uint8_t uint16_t start_address
Definition modbus.h:104
void clear_tx_queue_for_device(ModbusClientDevice *device)
Definition modbus.cpp:628
void parse_modbus_frames() override
Definition modbus.cpp:139
void requeue_waiting_frame_(ModbusDeviceCommand &wfr)
Definition modbus.cpp:577
void notify_no_response_(ModbusDeviceCommand &wfr)
Definition modbus.cpp:565
uint8_t uint16_t uint16_t uint8_t const uint8_t ModbusClientDevice * device
Definition modbus.h:105
void clear_tx_queue_for_address(uint8_t address, bool clear_sent=true)
Definition modbus.cpp:612
uint8_t uint16_t uint16_t number_of_entities
Definition modbus.h:104
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override
Definition modbus.cpp:261
uint8_t uint16_t uint16_t uint8_t const uint8_t * payload
Definition modbus.h:105
void queue_raw_(uint8_t address, const uint8_t *pdu, uint16_t pdu_len, ModbusClientDevice *device=nullptr)
Definition modbus.cpp:589
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:644
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:105
void send_pdu(uint8_t address, std::span< const uint8_t > pdu, ModbusClientDevice *device=nullptr)
Definition modbus.h:111
void set_send_wait_time(uint16_t time_in_ms)
Definition modbus.h:99
void set_turnaround_time(uint16_t time_in_ms)
Definition modbus.h:100
void set_flow_control_pin(GPIOPin *flow_control_pin)
Definition modbus.h:54
void setup() override
Definition modbus.cpp:18
uint16_t frame_delay_ms_
Definition modbus.h:74
bool parse_modbus_server_frame_()
Definition modbus.cpp:195
virtual void parse_modbus_frames()=0
bool send_frame_(const ModbusFrame &frame)
Definition modbus.cpp:454
uint32_t last_modbus_byte_
Definition modbus.h:70
GPIOPin * flow_control_pin_
Definition modbus.h:77
uint32_t last_send_tx_offset_
Definition modbus.h:73
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:682
void loop() override
Definition modbus.cpp:39
virtual void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len)=0
float get_setup_priority() const override
Definition modbus.cpp:536
uint16_t long_rx_buffer_delay_ms_
Definition modbus.h:75
virtual int32_t tx_delay_remaining()
Definition modbus.cpp:80
std::vector< uint8_t > rx_buffer_
Definition modbus.h:79
uint16_t find_custom_frame_end_(uint16_t min_length) const
Definition modbus.cpp:183
uint32_t last_receive_check_
Definition modbus.h:71
ModbusServerDevice & operator=(ModbusServerDevice &&)=delete
ModbusServerDevice(const ModbusServerDevice &)=delete
ModbusServerDevice & operator=(const ModbusServerDevice &)=delete
void set_address(uint8_t address)
Definition modbus.h:236
virtual ResponseStatus on_write_registers(uint16_t start_address, const RegisterValues &registers)
Definition modbus.h:250
virtual ResponseStatus on_read_holding_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:246
virtual ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:238
virtual ResponseStatus on_read_input_registers(uint16_t start_address, uint16_t number_of_registers, RegisterValues &registers)
Definition modbus.h:242
ModbusServerDevice(ModbusServerDevice &&)=delete
std::vector< ModbusServerDevice * > devices_
Definition modbus.h:161
void process_modbus_client_frame_(uint8_t address, uint8_t function_code, const uint8_t *data)
Definition modbus.cpp:353
void parse_modbus_frames() override
Definition modbus.cpp:152
bool check_register_range_(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_registers)
Definition modbus.cpp:342
void register_device(ModbusServerDevice *device)
Definition modbus.h:144
ModbusServerDevice * find_device_(uint8_t address)
Definition modbus.cpp:333
void send_exception_(uint8_t address, uint8_t function_code, ModbusExceptionCode exception_code)
Definition modbus.cpp:556
void send_raw_(const uint8_t *payload, uint16_t len)
Definition modbus.cpp:654
void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len)
Definition modbus.cpp:541
void process_modbus_server_frame(uint8_t address, uint8_t function_code, const uint8_t *data, uint16_t len) override
Definition modbus.cpp:317
std::array< uint8_t, MAX_RAW_SIZE > deferred_payload_
Definition modbus.h:165
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:221
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:90
ModbusClientDevice * device
Definition modbus.h:86
ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len)
Definition modbus.h:32
SmallInlineBuffer< MODBUS_FRAME_INLINE_SIZE > data
Definition modbus.h:30
uint16_t size() const
Definition modbus.h:41