ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
modbus_controller.cpp
Go to the documentation of this file.
1#include "modbus_controller.h"
3#include "esphome/core/log.h"
4
6
7static const char *const TAG = "modbus_controller";
8
10
11/*
12 To work with the existing modbus class and avoid polling for responses a command queue is used.
13 send_next_command will submit the command at the top of the queue and set the corresponding callback
14 to handle the response from the device.
15 Once the response has been processed it is removed from the queue and the next command is sent
16*/
18 uint32_t last_send = millis() - this->last_command_timestamp_;
19
20 if ((last_send > this->command_throttle_) && this->ready_for_immediate_send() && !this->command_queue_.empty()) {
21 auto &command = this->command_queue_.front();
22
23 // remove from queue if command was sent too often
24 if (!command->should_retry(this->max_cmd_retries_)) {
25 if (!this->module_offline_) {
26 ESP_LOGW(TAG, "Modbus device=%d set offline", this->address_);
27
28 if (this->offline_skip_updates_ > 0) {
29 // Update skip_updates_counter to stop flooding channel with timeouts
30 for (auto &r : this->register_ranges_) {
31 r.skip_updates_counter = this->offline_skip_updates_;
32 }
33 }
34
35 this->module_offline_ = true;
36 this->offline_callback_.call((int) command->function_code, command->register_address);
37 }
38 ESP_LOGD(TAG, "Modbus command to device=%d register=0x%02X no response received - removed from send queue",
39 this->address_, command->register_address);
40 this->command_queue_.pop_front();
41 } else {
42 ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
43 command->register_address, command->register_count);
44 command->send();
45
47
48 this->command_sent_callback_.call((int) command->function_code, command->register_address);
49
50 // remove from queue if no handler is defined
51 if (!command->on_data_func) {
52 this->command_queue_.pop_front();
53 }
54 }
55 }
56 return (!this->command_queue_.empty());
57}
58
59// Queue incoming response
60void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
61 if (this->command_queue_.empty()) {
62 ESP_LOGW(TAG, "Received modbus data but command queue is empty");
63 return;
64 }
65 auto &current_command = this->command_queue_.front();
66 if (current_command != nullptr) {
67 if (this->module_offline_) {
68 ESP_LOGW(TAG, "Modbus device=%d back online", this->address_);
69
70 if (this->offline_skip_updates_ > 0) {
71 // Restore skip_updates_counter to restore commands updates
72 for (auto &r : this->register_ranges_) {
73 r.skip_updates_counter = 0;
74 }
75 }
76 // Restore module online state
77 this->module_offline_ = false;
78 this->online_callback_.call((int) current_command->function_code, current_command->register_address);
79 }
80
81 // Move the commandItem to the response queue
82 current_command->payload = data;
83 this->incoming_queue_.push(std::move(current_command));
84 ESP_LOGV(TAG, "Modbus response queued");
85 this->command_queue_.pop_front();
86 }
87}
88
89// Dispatch the response to the registered handler
91 ESP_LOGV(TAG, "Process modbus response for address 0x%X size: %zu", response->register_address,
92 response->payload.size());
93 response->on_data_func(response->register_type, response->register_address, response->payload);
94}
95
96void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) {
97 ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code);
98 if (this->command_queue_.empty()) {
99 return;
100 }
101 // Remove pending command waiting for a response
102 auto &current_command = this->command_queue_.front();
103 if (current_command != nullptr) {
104 ESP_LOGE(TAG,
105 "Modbus error - last command: function code=0x%X register address = 0x%X "
106 "registers count=%d "
107 "payload size=%zu",
108 function_code, current_command->register_address, current_command->register_count,
109 current_command->payload.size());
110 this->command_queue_.pop_front();
111 }
112}
113
114SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
115 auto reg_it = std::find_if(
116 std::begin(this->register_ranges_), std::end(this->register_ranges_),
117 [=](RegisterRange const &r) { return (r.start_address == start_address && r.register_type == register_type); });
118
119 if (reg_it == this->register_ranges_.end()) {
120 ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address);
121 } else {
122 return reg_it->sensors;
123 }
124
125 // not found
126 return {};
127}
128void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address,
129 const std::vector<uint8_t> &data) {
130 ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address);
131
132 // loop through all sensors with the same start address
133 auto sensors = find_sensors_(register_type, start_address);
134 for (auto *sensor : sensors) {
135 sensor->parse_and_publish(data);
136 }
137}
138
140 if (!this->allow_duplicate_commands_) {
141 // check if this command is already qeued.
142 // not very effective but the queue is never really large
143 for (auto &item : this->command_queue_) {
144 if (item->is_equal(command)) {
145 ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u",
146 static_cast<uint8_t>(command.register_type), command.register_address, command.register_count);
147 // update the payload of the queued command
148 // replaces a previous command
149 item->payload = command.payload;
150 return;
151 }
152 }
153 }
154 this->command_queue_.push_back(make_unique<ModbusCommandItem>(command));
155}
156
158 ESP_LOGV(TAG, "Range : %X Size: %x (%d) skip: %d", r.start_address, r.register_count, (int) r.register_type,
160 if (r.skip_updates_counter == 0) {
161 // if a custom command is used the user supplied custom_data is only available in the SensorItem.
163 auto sensors = this->find_sensors_(r.register_type, r.start_address);
164 if (!sensors.empty()) {
165 auto sensor = sensors.cbegin();
167 this, (*sensor)->custom_data,
168 [this](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
169 this->on_register_data(ModbusRegisterType::CUSTOM, start_address, data);
170 });
171 command_item.register_address = (*sensor)->start_address;
172 command_item.register_count = (*sensor)->register_count;
173 command_item.function_code = ModbusFunctionCode::CUSTOM;
174 queue_command(command_item);
175 }
176 } else {
178 }
179 r.skip_updates_counter = r.skip_updates; // reset counter to config value
180 } else {
182 }
183}
184//
185// Queue the modbus requests to be send.
186// Once we get a response to the command it is removed from the queue and the next command is send
187//
189 if (!this->command_queue_.empty()) {
190 ESP_LOGV(TAG, "%zu modbus commands already in queue", this->command_queue_.size());
191 } else {
192 ESP_LOGV(TAG, "Updating modbus component");
193 }
194
195 for (auto &r : this->register_ranges_) {
196 ESP_LOGVV(TAG, "Updating range 0x%X", r.start_address);
197 update_range_(r);
198 }
199}
200
201// walk through the sensors and determine the register ranges to read
203 this->register_ranges_.clear();
204 if (this->parent_->role == modbus::ModbusRole::CLIENT && this->sensorset_.empty()) {
205 ESP_LOGW(TAG, "No sensors registered");
206 return 0;
207 }
208
209 // iterator is sorted see SensorItemsComparator for details
210 auto ix = this->sensorset_.begin();
211 RegisterRange r = {};
212 uint8_t buffer_offset = 0;
213 SensorItem *prev = nullptr;
214 while (ix != this->sensorset_.end()) {
215 SensorItem *curr = *ix;
216
217 ESP_LOGV(TAG, "Register: 0x%X %d %d %zu offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
218 curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr);
219
220 if (r.register_count == 0) {
221 // this is the first register in range
225 r.sensors.insert(curr);
226 r.skip_updates = curr->skip_updates;
228 buffer_offset = curr->get_register_size();
229
230 ESP_LOGV(TAG, "Started new range");
231 } else {
232 // this is not the first register in range so it might be possible
233 // to reuse the last register or extend the current range
234 if (!curr->force_new_range && r.register_type == curr->register_type &&
236 if (curr->start_address == (r.start_address + r.register_count - prev->register_count) &&
237 curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) {
238 // this register can re-use the data from the previous register
239
240 // remove this sensore because start_address is changed (sort-order)
241 ix = this->sensorset_.erase(ix);
242
244 curr->offset += prev->offset;
245
246 this->sensorset_.insert(curr);
247 // move iterator backwards because it will be incremented later
248 ix--;
249
250 ESP_LOGV(TAG, "Re-use previous register - change to register: 0x%X %d offset=%u", curr->start_address,
251 curr->register_count, curr->offset);
252 } else if (curr->start_address == (r.start_address + r.register_count)) {
253 // this register can extend the current range
254
255 // remove this sensore because start_address is changed (sort-order)
256 ix = this->sensorset_.erase(ix);
257
259 curr->offset += buffer_offset;
260 buffer_offset += curr->get_register_size();
262
263 this->sensorset_.insert(curr);
264 // move iterator backwards because it will be incremented later
265 ix--;
266
267 ESP_LOGV(TAG, "Extend range - change to register: 0x%X %d offset=%u", curr->start_address,
268 curr->register_count, curr->offset);
269 }
270 }
271 }
272
273 if (curr->start_address == r.start_address && curr->register_type == r.register_type) {
274 // use the lowest non zero value for the whole range
275 // Because zero is the default value for skip_updates it is excluded from getting the min value.
276 if (curr->skip_updates != 0) {
277 if (r.skip_updates != 0) {
278 r.skip_updates = std::min(r.skip_updates, curr->skip_updates);
279 } else {
280 r.skip_updates = curr->skip_updates;
281 }
282 }
283
284 // add sensor to this range
285 r.sensors.insert(curr);
286
287 ix++;
288 } else {
289 ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
290 this->register_ranges_.push_back(r);
291 r = {};
292 buffer_offset = 0;
293 // do not increment the iterator here because the current sensor has to be re-evaluated
294 }
295
296 prev = curr;
297 }
298
299 if (r.register_count > 0) {
300 // Add the last range
301 ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
302 this->register_ranges_.push_back(r);
303 }
304
305 return this->register_ranges_.size();
306}
307
309 ESP_LOGCONFIG(TAG,
310 "ModbusController:\n"
311 " Address: 0x%02X\n"
312 " Max Command Retries: %d\n"
313 " Offline Skip Updates: %d\n",
315
316#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
317 ESP_LOGCONFIG(TAG, "sensormap");
318 for (auto &it : this->sensorset_) {
319 ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X count=%d size=%zu",
320 static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
321 it->get_register_size());
322 }
323 ESP_LOGCONFIG(TAG, "ranges");
324 for (auto &it : this->register_ranges_) {
325 ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
326 it.start_address, it.register_count, it.skip_updates);
327 }
328#endif
329}
330
332 // Incoming data to process?
333 if (!this->incoming_queue_.empty()) {
334 auto &message = this->incoming_queue_.front();
335 if (message != nullptr)
336 this->process_modbus_data_(message.get());
337 this->incoming_queue_.pop();
338
339 } else {
340 // all messages processed send pending commands
341 this->send_next_command_();
342 }
343}
344
345void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
346 const std::vector<uint8_t> &data) {
347 ESP_LOGV(TAG, "Command ACK 0x%X %d ", modbus::helpers::get_data<uint16_t>(data, 0),
349}
350
352 ESP_LOGV(TAG, "sensors");
353 for (auto &it : this->sensorset_) {
354 ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%zu offset=%d", it->start_address, it->register_count,
355 it->get_register_size(), it->offset);
356 }
357}
358
360 ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count,
361 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
362 &&handler) {
367 cmd.register_address = start_address;
369 cmd.on_data_func = std::move(handler);
370 return cmd;
371}
372
374 ModbusRegisterType register_type, uint16_t start_address,
375 uint16_t register_count) {
380 cmd.register_address = start_address;
382 cmd.on_data_func = [modbusdevice](ModbusRegisterType register_type, uint16_t start_address,
383 const std::vector<uint8_t> &data) {
385 };
386 return cmd;
387}
388
390 uint16_t start_address, uint16_t register_count,
391 const std::vector<uint16_t> &values) {
396 cmd.register_address = start_address;
398 cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
399 const std::vector<uint8_t> &data) {
401 };
402 for (auto v : values) {
403 auto decoded_value = decode_value(v);
404 cmd.payload.push_back(decoded_value[0]);
405 cmd.payload.push_back(decoded_value[1]);
406 }
407 return cmd;
408}
409
411 bool value) {
417 cmd.register_count = 1;
418 cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
419 const std::vector<uint8_t> &data) {
421 };
422 cmd.payload.push_back(value ? 0xFF : 0);
423 cmd.payload.push_back(0);
424 return cmd;
425}
426
428 const std::vector<bool> &values) {
433 cmd.register_address = start_address;
434 cmd.register_count = values.size();
435 cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
436 const std::vector<uint8_t> &data) {
438 };
439
440 uint8_t bitmask = 0;
441 int bitcounter = 0;
442 for (auto coil : values) {
443 if (coil) {
444 bitmask |= (1 << bitcounter);
445 }
446 bitcounter++;
447 if (bitcounter % 8 == 0) {
448 cmd.payload.push_back(bitmask);
449 bitmask = 0;
450 }
451 }
452 // add remaining bits
453 if (bitcounter % 8) {
454 cmd.payload.push_back(bitmask);
455 }
456 return cmd;
457}
458
460 uint16_t value) {
465 cmd.register_address = start_address;
466 cmd.register_count = 1; // not used here anyways
467 cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
468 const std::vector<uint8_t> &data) {
470 };
471
472 auto decoded_value = decode_value(value);
473 cmd.payload.push_back(decoded_value[0]);
474 cmd.payload.push_back(decoded_value[1]);
475 return cmd;
476}
477
479 ModbusController *modbusdevice, const std::vector<uint8_t> &values,
480 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
481 &&handler) {
485 if (handler == nullptr) {
486 cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
487 ESP_LOGI(TAG, "Custom Command sent");
488 };
489 } else {
490 cmd.on_data_func = handler;
491 }
492 cmd.payload = values;
493
494 return cmd;
495}
496
498 ModbusController *modbusdevice, const std::vector<uint16_t> &values,
499 std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
500 &&handler) {
501 ModbusCommandItem cmd = {};
504 if (handler == nullptr) {
505 cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
506 ESP_LOGI(TAG, "Custom Command sent");
507 };
508 } else {
509 cmd.on_data_func = handler;
510 }
511 for (auto v : values) {
512 cmd.payload.push_back((v >> 8) & 0xFF);
513 cmd.payload.push_back(v & 0xFF);
514 }
515
516 return cmd;
517}
518
521 modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(),
522 this->payload.empty() ? nullptr : &this->payload[0]);
523 } else {
525 }
526 this->send_count_++;
527 ESP_LOGV(TAG, "Command sent %d 0x%X %d send_count: %d", uint8_t(this->function_code), this->register_address,
528 this->register_count, this->send_count_);
529 return true;
530}
531
533 // for custom commands we have to check for identical payloads, since
534 // address/count/type fields will be set to zero
536 ? this->payload == other.payload
537 : other.register_address == this->register_address && other.register_count == this->register_count &&
538 other.register_type == this->register_type && other.function_code == this->function_code;
539}
540
541} // namespace esphome::modbus_controller
uint8_t address
Definition bl0906.h:4
void send_raw(const std::vector< uint8_t > &payload)
Definition modbus.h:103
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:99
static ModbusCommandItem create_custom_command(ModbusController *modbusdevice, const std::vector< uint8_t > &values, std::function< void(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)> &&handler=nullptr)
Create custom modbus command.
bool is_equal(const ModbusCommandItem &other)
static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address, const std::vector< bool > &values)
Create modbus write multiple registers command Function 15 (0Fhex) Write Multiple Coils.
static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value)
Create modbus write single registers command Function 05 (05hex) Write Single Coil.
uint8_t send_count_
How many times this command has been sent.
static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address, uint16_t value)
Create modbus write multiple registers command Function 16 (10hex) Write Multiple Registers.
static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count, std::function< void(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)> &&handler)
factory methods
static ModbusCommandItem create_write_multiple_command(ModbusController *modbusdevice, uint16_t start_address, uint16_t register_count, const std::vector< uint16_t > &values)
Create modbus read command Function code 02-04.
std::function< void(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)> on_data_func
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)
default delegate called by process_modbus_data when a response has retrieved from the incoming queue
std::queue< std::unique_ptr< ModbusCommandItem > > incoming_queue_
modbus response data waiting to get processed
uint16_t command_throttle_
min time in ms between sending modbus commands
void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address, const std::vector< uint8_t > &data)
default delegate called by process_modbus_data when a response for a write response has retrieved fro...
bool allow_duplicate_commands_
if duplicate commands can be sent
CallbackManager< void(int, int)> command_sent_callback_
Command sent callback.
std::vector< RegisterRange > register_ranges_
Continuous range of modbus registers.
uint32_t last_command_timestamp_
when was the last send operation
CallbackManager< void(int, int)> offline_callback_
Server offline callback.
void dump_sensors_()
dump the parsed sensormap for diagnostics
SensorSet sensorset_
Collection of all sensors for this component.
uint8_t max_cmd_retries_
How many times we will retry a command if we get no response.
bool send_next_command_()
send the next modbus command from the send queue
std::list< std::unique_ptr< ModbusCommandItem > > command_queue_
Hold the pending requests to be sent.
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override
called when a modbus error response was received
void process_modbus_data_(const ModbusCommandItem *response)
parse incoming modbus data
void update_range_(RegisterRange &r)
submit the read command for the address range to the send queue
bool module_offline_
if module didn't respond the last command
size_t create_register_ranges_()
parse sensormap_ and create range of sequential addresses
uint16_t offline_skip_updates_
how many updates to skip if module is offline
void on_modbus_data(const std::vector< uint8_t > &data) override
called when a modbus response was parsed without errors
void queue_command(const ModbusCommandItem &command)
queues a modbus command in the send queue
SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const
CallbackManager< void(int, int)> online_callback_
Server online callback.
const LogString * message
Definition component.cpp:35
ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type)
T get_data(const std::vector< uint8_t > &data, size_t buffer_offset)
Extract data from modbus response buffer.
std::set< SensorItem *, SensorItemsComparator > SensorSet
const std::vector< uint8_t > & data
constexpr std::array< uint8_t, sizeof(T)> decode_value(T val)
Decode a value into its constituent bytes (from most to least significant).
Definition helpers.h:888
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t