ESPHome  2023.8.3
modbus_controller.cpp
Go to the documentation of this file.
1 #include "modbus_controller.h"
3 #include "esphome/core/log.h"
4 
5 namespace esphome {
6 namespace modbus_controller {
7 
8 static const char *const TAG = "modbus_controller";
9 
11  // Modbus::setup();
13 }
14 
15 /*
16  To work with the existing modbus class and avoid polling for responses a command queue is used.
17  send_next_command will submit the command at the top of the queue and set the corresponding callback
18  to handle the response from the device.
19  Once the response has been processed it is removed from the queue and the next command is sent
20 */
22  uint32_t last_send = millis() - this->last_command_timestamp_;
23 
24  if ((last_send > this->command_throttle_) && !waiting_for_response() && !command_queue_.empty()) {
25  auto &command = command_queue_.front();
26 
27  // remove from queue if command was sent too often
28  if (command->send_countdown < 1) {
29  ESP_LOGD(
30  TAG,
31  "Modbus command to device=%d register=0x%02X countdown=%d no response received - removed from send queue",
32  this->address_, command->register_address, command->send_countdown);
33  command_queue_.pop_front();
34  } else {
35  ESP_LOGV(TAG, "Sending next modbus command to device %d register 0x%02X count %d", this->address_,
36  command->register_address, command->register_count);
37  command->send();
39  // remove from queue if no handler is defined
40  if (!command->on_data_func) {
41  command_queue_.pop_front();
42  }
43  }
44  }
45  return (!command_queue_.empty());
46 }
47 
48 // Queue incoming response
49 void ModbusController::on_modbus_data(const std::vector<uint8_t> &data) {
50  auto &current_command = this->command_queue_.front();
51  if (current_command != nullptr) {
52  // Move the commandItem to the response queue
53  current_command->payload = data;
54  this->incoming_queue_.push(std::move(current_command));
55  ESP_LOGV(TAG, "Modbus response queued");
56  command_queue_.pop_front();
57  }
58 }
59 
60 // Dispatch the response to the registered handler
62  ESP_LOGV(TAG, "Process modbus response for address 0x%X size: %zu", response->register_address,
63  response->payload.size());
64  response->on_data_func(response->register_type, response->register_address, response->payload);
65 }
66 
67 void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_code) {
68  ESP_LOGE(TAG, "Modbus error function code: 0x%X exception: %d ", function_code, exception_code);
69  // Remove pending command waiting for a response
70  auto &current_command = this->command_queue_.front();
71  if (current_command != nullptr) {
72  ESP_LOGE(TAG,
73  "Modbus error - last command: function code=0x%X register address = 0x%X "
74  "registers count=%d "
75  "payload size=%zu",
76  function_code, current_command->register_address, current_command->register_count,
77  current_command->payload.size());
78  command_queue_.pop_front();
79  }
80 }
81 
82 SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
83  auto reg_it = find_if(begin(register_ranges_), end(register_ranges_), [=](RegisterRange const &r) {
84  return (r.start_address == start_address && r.register_type == register_type);
85  });
86 
87  if (reg_it == register_ranges_.end()) {
88  ESP_LOGE(TAG, "No matching range for sensor found - start_address : 0x%X", start_address);
89  } else {
90  return reg_it->sensors;
91  }
92 
93  // not found
94  return {};
95 }
96 void ModbusController::on_register_data(ModbusRegisterType register_type, uint16_t start_address,
97  const std::vector<uint8_t> &data) {
98  ESP_LOGV(TAG, "data for register address : 0x%X : ", start_address);
99 
100  // loop through all sensors with the same start address
101  auto sensors = find_sensors_(register_type, start_address);
102  for (auto *sensor : sensors) {
103  sensor->parse_and_publish(data);
104  }
105 }
106 
108  // check if this command is already qeued.
109  // not very effective but the queue is never really large
110  for (auto &item : command_queue_) {
111  if (item->is_equal(command)) {
112  ESP_LOGW(TAG, "Duplicate modbus command found: type=0x%x address=%u count=%u",
113  static_cast<uint8_t>(command.register_type), command.register_address, command.register_count);
114  // update the payload of the queued command
115  // replaces a previous command
116  item->payload = command.payload;
117  return;
118  }
119  }
120  command_queue_.push_back(make_unique<ModbusCommandItem>(command));
121 }
122 
124  ESP_LOGV(TAG, "Range : %X Size: %x (%d) skip: %d", r.start_address, r.register_count, (int) r.register_type,
126  if (r.skip_updates_counter == 0) {
127  // if a custom command is used the user supplied custom_data is only available in the SensorItem.
129  auto sensors = this->find_sensors_(r.register_type, r.start_address);
130  if (!sensors.empty()) {
131  auto sensor = sensors.cbegin();
132  auto command_item = ModbusCommandItem::create_custom_command(
133  this, (*sensor)->custom_data,
134  [this](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
135  this->on_register_data(ModbusRegisterType::CUSTOM, start_address, data);
136  });
137  command_item.register_address = (*sensor)->start_address;
138  command_item.register_count = (*sensor)->register_count;
139  command_item.function_code = ModbusFunctionCode::CUSTOM;
140  queue_command(command_item);
141  }
142  } else {
144  }
145  r.skip_updates_counter = r.skip_updates; // reset counter to config value
146  } else {
148  }
149 }
150 //
151 // Queue the modbus requests to be send.
152 // Once we get a response to the command it is removed from the queue and the next command is send
153 //
155  if (!command_queue_.empty()) {
156  ESP_LOGV(TAG, "%zu modbus commands already in queue", command_queue_.size());
157  } else {
158  ESP_LOGV(TAG, "Updating modbus component");
159  }
160 
161  for (auto &r : this->register_ranges_) {
162  ESP_LOGVV(TAG, "Updating range 0x%X", r.start_address);
163  update_range_(r);
164  }
165 }
166 
167 // walk through the sensors and determine the register ranges to read
169  register_ranges_.clear();
170  if (sensorset_.empty()) {
171  ESP_LOGW(TAG, "No sensors registered");
172  return 0;
173  }
174 
175  // iterator is sorted see SensorItemsComparator for details
176  auto ix = sensorset_.begin();
177  RegisterRange r = {};
178  uint8_t buffer_offset = 0;
179  SensorItem *prev = nullptr;
180  while (ix != sensorset_.end()) {
181  SensorItem *curr = *ix;
182 
183  ESP_LOGV(TAG, "Register: 0x%X %d %d %d offset=%u skip=%u addr=%p", curr->start_address, curr->register_count,
184  curr->offset, curr->get_register_size(), curr->offset, curr->skip_updates, curr);
185 
186  if (r.register_count == 0) {
187  // this is the first register in range
188  r.start_address = curr->start_address;
189  r.register_count = curr->register_count;
190  r.register_type = curr->register_type;
191  r.sensors.insert(curr);
192  r.skip_updates = curr->skip_updates;
193  r.skip_updates_counter = 0;
194  buffer_offset = curr->get_register_size();
195 
196  ESP_LOGV(TAG, "Started new range");
197  } else {
198  // this is not the first register in range so it might be possible
199  // to reuse the last register or extend the current range
200  if (!curr->force_new_range && r.register_type == curr->register_type &&
202  if (curr->start_address == (r.start_address + r.register_count - prev->register_count) &&
203  curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) {
204  // this register can re-use the data from the previous register
205 
206  // remove this sensore because start_address is changed (sort-order)
207  ix = sensorset_.erase(ix);
208 
209  curr->start_address = r.start_address;
210  curr->offset += prev->offset;
211 
212  sensorset_.insert(curr);
213  // move iterator backwards because it will be incremented later
214  ix--;
215 
216  ESP_LOGV(TAG, "Re-use previous register - change to register: 0x%X %d offset=%u", curr->start_address,
217  curr->register_count, curr->offset);
218  } else if (curr->start_address == (r.start_address + r.register_count)) {
219  // this register can extend the current range
220 
221  // remove this sensore because start_address is changed (sort-order)
222  ix = sensorset_.erase(ix);
223 
224  curr->start_address = r.start_address;
225  curr->offset += buffer_offset;
226  buffer_offset += curr->get_register_size();
227  r.register_count += curr->register_count;
228 
229  sensorset_.insert(curr);
230  // move iterator backwards because it will be incremented later
231  ix--;
232 
233  ESP_LOGV(TAG, "Extend range - change to register: 0x%X %d offset=%u", curr->start_address,
234  curr->register_count, curr->offset);
235  }
236  }
237  }
238 
239  if (curr->start_address == r.start_address && curr->register_type == r.register_type) {
240  // use the lowest non zero value for the whole range
241  // Because zero is the default value for skip_updates it is excluded from getting the min value.
242  if (curr->skip_updates != 0) {
243  if (r.skip_updates != 0) {
244  r.skip_updates = std::min(r.skip_updates, curr->skip_updates);
245  } else {
246  r.skip_updates = curr->skip_updates;
247  }
248  }
249 
250  // add sensor to this range
251  r.sensors.insert(curr);
252 
253  ix++;
254  } else {
255  ESP_LOGV(TAG, "Add range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
256  register_ranges_.push_back(r);
257  r = {};
258  buffer_offset = 0;
259  // do not increment the iterator here because the current sensor has to be re-evaluated
260  }
261 
262  prev = curr;
263  }
264 
265  if (r.register_count > 0) {
266  // Add the last range
267  ESP_LOGV(TAG, "Add last range 0x%X %d skip:%d", r.start_address, r.register_count, r.skip_updates);
268  register_ranges_.push_back(r);
269  }
270 
271  return register_ranges_.size();
272 }
273 
275  ESP_LOGCONFIG(TAG, "ModbusController:");
276  ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_);
277 #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
278  ESP_LOGCONFIG(TAG, "sensormap");
279  for (auto &it : sensorset_) {
280  ESP_LOGCONFIG(TAG, " Sensor type=%zu start=0x%X offset=0x%X count=%d size=%d",
281  static_cast<uint8_t>(it->register_type), it->start_address, it->offset, it->register_count,
282  it->get_register_size());
283  }
284  ESP_LOGCONFIG(TAG, "ranges");
285  for (auto &it : register_ranges_) {
286  ESP_LOGCONFIG(TAG, " Range type=%zu start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
287  it.start_address, it.register_count, it.skip_updates);
288  }
289 #endif
290 }
291 
293  // Incoming data to process?
294  if (!incoming_queue_.empty()) {
295  auto &message = incoming_queue_.front();
296  if (message != nullptr)
297  process_modbus_data_(message.get());
298  incoming_queue_.pop();
299 
300  } else {
301  // all messages processed send pending commands
303  }
304 }
305 
306 void ModbusController::on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
307  const std::vector<uint8_t> &data) {
308  ESP_LOGV(TAG, "Command ACK 0x%X %d ", get_data<uint16_t>(data, 0), get_data<int16_t>(data, 1));
309 }
310 
312  ESP_LOGV(TAG, "sensors");
313  for (auto &it : sensorset_) {
314  ESP_LOGV(TAG, " Sensor start=0x%X count=%d size=%d offset=%d", it->start_address, it->register_count,
315  it->get_register_size(), it->offset);
316  }
317 }
318 
320  ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count,
321  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
322  &&handler) {
324  cmd.modbusdevice = modbusdevice;
325  cmd.register_type = register_type;
326  cmd.function_code = modbus_register_read_function(register_type);
327  cmd.register_address = start_address;
328  cmd.register_count = register_count;
329  cmd.on_data_func = std::move(handler);
330  return cmd;
331 }
332 
334  ModbusRegisterType register_type, uint16_t start_address,
335  uint16_t register_count) {
337  cmd.modbusdevice = modbusdevice;
338  cmd.register_type = register_type;
339  cmd.function_code = modbus_register_read_function(register_type);
340  cmd.register_address = start_address;
341  cmd.register_count = register_count;
342  cmd.on_data_func = [modbusdevice](ModbusRegisterType register_type, uint16_t start_address,
343  const std::vector<uint8_t> &data) {
344  modbusdevice->on_register_data(register_type, start_address, data);
345  };
346  return cmd;
347 }
348 
350  uint16_t start_address, uint16_t register_count,
351  const std::vector<uint16_t> &values) {
353  cmd.modbusdevice = modbusdevice;
356  cmd.register_address = start_address;
357  cmd.register_count = register_count;
358  cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
359  const std::vector<uint8_t> &data) {
360  modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
361  };
362  for (auto v : values) {
363  auto decoded_value = decode_value(v);
364  cmd.payload.push_back(decoded_value[0]);
365  cmd.payload.push_back(decoded_value[1]);
366  }
367  return cmd;
368 }
369 
371  bool value) {
373  cmd.modbusdevice = modbusdevice;
376  cmd.register_address = address;
377  cmd.register_count = 1;
378  cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
379  const std::vector<uint8_t> &data) {
380  modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
381  };
382  cmd.payload.push_back(value ? 0xFF : 0);
383  cmd.payload.push_back(0);
384  return cmd;
385 }
386 
388  const std::vector<bool> &values) {
390  cmd.modbusdevice = modbusdevice;
393  cmd.register_address = start_address;
394  cmd.register_count = values.size();
395  cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
396  const std::vector<uint8_t> &data) {
397  modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
398  };
399 
400  uint8_t bitmask = 0;
401  int bitcounter = 0;
402  for (auto coil : values) {
403  if (coil) {
404  bitmask |= (1 << bitcounter);
405  }
406  bitcounter++;
407  if (bitcounter % 8 == 0) {
408  cmd.payload.push_back(bitmask);
409  bitmask = 0;
410  }
411  }
412  // add remaining bits
413  if (bitcounter % 8) {
414  cmd.payload.push_back(bitmask);
415  }
416  return cmd;
417 }
418 
420  uint16_t value) {
422  cmd.modbusdevice = modbusdevice;
425  cmd.register_address = start_address;
426  cmd.register_count = 1; // not used here anyways
427  cmd.on_data_func = [modbusdevice, cmd](ModbusRegisterType register_type, uint16_t start_address,
428  const std::vector<uint8_t> &data) {
429  modbusdevice->on_write_register_response(cmd.register_type, start_address, data);
430  };
431 
432  auto decoded_value = decode_value(value);
433  cmd.payload.push_back(decoded_value[0]);
434  cmd.payload.push_back(decoded_value[1]);
435  return cmd;
436 }
437 
439  ModbusController *modbusdevice, const std::vector<uint8_t> &values,
440  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
441  &&handler) {
443  cmd.modbusdevice = modbusdevice;
445  if (handler == nullptr) {
446  cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
447  ESP_LOGI(TAG, "Custom Command sent");
448  };
449  } else {
450  cmd.on_data_func = handler;
451  }
452  cmd.payload = values;
453 
454  return cmd;
455 }
456 
458  ModbusController *modbusdevice, const std::vector<uint16_t> &values,
459  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
460  &&handler) {
461  ModbusCommandItem cmd = {};
462  cmd.modbusdevice = modbusdevice;
464  if (handler == nullptr) {
465  cmd.on_data_func = [](ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data) {
466  ESP_LOGI(TAG, "Custom Command sent");
467  };
468  } else {
469  cmd.on_data_func = handler;
470  }
471  for (auto v : values) {
472  cmd.payload.push_back((v >> 8) & 0xFF);
473  cmd.payload.push_back(v & 0xFF);
474  }
475 
476  return cmd;
477 }
478 
480  if (this->function_code != ModbusFunctionCode::CUSTOM) {
481  modbusdevice->send(uint8_t(this->function_code), this->register_address, this->register_count, this->payload.size(),
482  this->payload.empty() ? nullptr : &this->payload[0]);
483  } else {
484  modbusdevice->send_raw(this->payload);
485  }
486  ESP_LOGV(TAG, "Command sent %d 0x%X %d", uint8_t(this->function_code), this->register_address, this->register_count);
487  send_countdown--;
488  return true;
489 }
490 
492  // for custom commands we have to check for identical payloads, since
493  // address/count/type fields will be set to zero
494  return this->function_code == ModbusFunctionCode::CUSTOM
495  ? this->payload == other.payload
496  : other.register_address == this->register_address && other.register_count == this->register_count &&
497  other.register_type == this->register_type && other.function_code == this->function_code;
498 }
499 
500 void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type) {
501  switch (value_type) {
504  data.push_back(value & 0xFFFF);
505  break;
509  data.push_back((value & 0xFFFF0000) >> 16);
510  data.push_back(value & 0xFFFF);
511  break;
515  data.push_back(value & 0xFFFF);
516  data.push_back((value & 0xFFFF0000) >> 16);
517  break;
520  data.push_back((value & 0xFFFF000000000000) >> 48);
521  data.push_back((value & 0xFFFF00000000) >> 32);
522  data.push_back((value & 0xFFFF0000) >> 16);
523  data.push_back(value & 0xFFFF);
524  break;
527  data.push_back(value & 0xFFFF);
528  data.push_back((value & 0xFFFF0000) >> 16);
529  data.push_back((value & 0xFFFF00000000) >> 32);
530  data.push_back((value & 0xFFFF000000000000) >> 48);
531  break;
532  default:
533  ESP_LOGE(TAG, "Invalid data type for modbus number to payload conversation: %d",
534  static_cast<uint16_t>(value_type));
535  break;
536  }
537 }
538 
539 int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
540  uint32_t bitmask) {
541  int64_t value = 0; // int64_t because it can hold signed and unsigned 32 bits
542 
543  switch (sensor_value_type) {
545  value = mask_and_shift_by_rightbit(get_data<uint16_t>(data, offset), bitmask); // default is 0xFFFF ;
546  break;
549  value = get_data<uint32_t>(data, offset);
550  value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
551  break;
554  value = get_data<uint32_t>(data, offset);
555  value = static_cast<uint32_t>(value & 0xFFFF) << 16 | (value & 0xFFFF0000) >> 16;
556  value = mask_and_shift_by_rightbit((uint32_t) value, bitmask);
557  break;
559  value = mask_and_shift_by_rightbit(get_data<int16_t>(data, offset),
560  bitmask); // default is 0xFFFF ;
561  break;
563  value = mask_and_shift_by_rightbit(get_data<int32_t>(data, offset), bitmask);
564  break;
566  value = get_data<uint32_t>(data, offset);
567  // Currently the high word is at the low position
568  // the sign bit is therefore at low before the switch
569  uint32_t sign_bit = (value & 0x8000) << 16;
571  static_cast<int32_t>(((value & 0x7FFF) << 16 | (value & 0xFFFF0000) >> 16) | sign_bit), bitmask);
572  } break;
575  // Ignore bitmask for QWORD
576  value = get_data<uint64_t>(data, offset);
577  break;
580  // Ignore bitmask for QWORD
581  uint64_t tmp = get_data<uint64_t>(data, offset);
582  value = (tmp << 48) | (tmp >> 48) | ((tmp & 0xFFFF0000) << 16) | ((tmp >> 16) & 0xFFFF0000);
583  } break;
585  default:
586  break;
587  }
588  return value;
589 }
590 
591 } // namespace modbus_controller
592 } // namespace esphome
void queue_command(const ModbusCommandItem &command)
queues a modbus command in the send queue
N mask_and_shift_by_rightbit(N data, uint32_t mask)
Extract bits from value and shift right according to the bitmask if the bitmask is 0x00F0 we want the...
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...
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...
uint16_t command_throttle_
min time in ms between sending modbus commands
uint32_t last_command_timestamp_
when was the last send operation
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:27
bool send_next_command_()
send the next modbus command from the send queue
SensorSet sensorset_
Collection of all sensors for this component.
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.
SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const
void number_to_payload(std::vector< uint16_t > &data, int64_t value, SensorValueType value_type)
Convert float value to vector<uint16_t> suitable for sending.
void dump_sensors_()
dump the parsed sensormap for diagnostics
void send_raw(const std::vector< uint8_t > &payload)
Definition: modbus.h:57
constexpr14 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:209
void on_modbus_data(const std::vector< uint8_t > &data) override
called when a modbus response was parsed without errors
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override
called when a modbus error response was received
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 ...
void process_modbus_data_(const ModbusCommandItem *response)
parse incoming modbus data
std::list< std::unique_ptr< ModbusCommandItem > > command_queue_
Hold the pending requests to be sent.
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
ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type)
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...
int64_t payload_to_number(const std::vector< uint8_t > &data, SensorValueType sensor_value_type, uint8_t offset, uint32_t bitmask)
Convert vector<uint8_t> response payload to number.
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::vector< RegisterRange > register_ranges_
Continuous range of modbus registers.
void update_range_(RegisterRange &r)
submit the read command for the address range to the send queue
bool is_equal(const ModbusCommandItem &other)
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:53
std::set< SensorItem *, SensorItemsComparator > SensorSet
std::queue< std::unique_ptr< ModbusCommandItem > > incoming_queue_
modbus response data waiting to get processed
size_t create_register_ranges_()
parse sensormap_ and create range of sequential addresses
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.
stm32_cmd_t * cmd
Definition: stm32flash.h:96