ESPHome  2024.9.0
modbus_controller.h
Go to the documentation of this file.
1 #pragma once
2 
4 
7 
8 #include <list>
9 #include <queue>
10 #include <set>
11 #include <utility>
12 #include <vector>
13 
14 namespace esphome {
15 namespace modbus_controller {
16 
17 class ModbusController;
18 
19 enum class ModbusFunctionCode {
20  CUSTOM = 0x00,
21  READ_COILS = 0x01,
22  READ_DISCRETE_INPUTS = 0x02,
24  READ_INPUT_REGISTERS = 0x04,
25  WRITE_SINGLE_COIL = 0x05,
26  WRITE_SINGLE_REGISTER = 0x06,
27  READ_EXCEPTION_STATUS = 0x07, // not implemented
28  DIAGNOSTICS = 0x08, // not implemented
29  GET_COMM_EVENT_COUNTER = 0x0B, // not implemented
30  GET_COMM_EVENT_LOG = 0x0C, // not implemented
31  WRITE_MULTIPLE_COILS = 0x0F,
33  REPORT_SERVER_ID = 0x11, // not implemented
34  READ_FILE_RECORD = 0x14, // not implemented
35  WRITE_FILE_RECORD = 0x15, // not implemented
36  MASK_WRITE_REGISTER = 0x16, // not implemented
37  READ_WRITE_MULTIPLE_REGISTERS = 0x17, // not implemented
38  READ_FIFO_QUEUE = 0x18, // not implemented
39 };
40 
41 enum class ModbusRegisterType : uint8_t {
42  CUSTOM = 0x0,
43  COIL = 0x01,
44  DISCRETE_INPUT = 0x02,
45  HOLDING = 0x03,
46  READ = 0x04,
47 };
48 
49 enum class SensorValueType : uint8_t {
50  RAW = 0x00, // variable length
51  U_WORD = 0x1, // 1 Register unsigned
52  U_DWORD = 0x2, // 2 Registers unsigned
53  S_WORD = 0x3, // 1 Register signed
54  S_DWORD = 0x4, // 2 Registers signed
55  BIT = 0x5,
56  U_DWORD_R = 0x6, // 2 Registers unsigned
57  S_DWORD_R = 0x7, // 2 Registers unsigned
58  U_QWORD = 0x8,
59  S_QWORD = 0x9,
60  U_QWORD_R = 0xA,
61  S_QWORD_R = 0xB,
62  FP32 = 0xC,
63  FP32_R = 0xD
64 };
65 
67  switch (reg_type) {
70  break;
73  break;
76  break;
79  break;
80  default:
82  break;
83  }
84 }
86  switch (reg_type) {
89  break;
92  break;
95  break;
97  default:
99  break;
100  }
101 }
102 
103 inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10) : (c - 'A' + 10) : (c - '0'); }
104 
113 inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) {
114  if (value.length() < pos * 2 + 1)
115  return 0;
116  return (c_to_hex(value[pos * 2]) << 4) | c_to_hex(value[pos * 2 + 1]);
117 }
118 
125 inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) {
126  return byte_from_hex_str(value, pos) << 8 | byte_from_hex_str(value, pos + 1);
127 }
128 
135 inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) {
136  return word_from_hex_str(value, pos) << 16 | word_from_hex_str(value, pos + 2);
137 }
138 
145 inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) {
146  return static_cast<uint64_t>(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4);
147 }
148 
149 // Extract data from modbus response buffer
156 template<typename T> T get_data(const std::vector<uint8_t> &data, size_t buffer_offset) {
157  if (sizeof(T) == sizeof(uint8_t)) {
158  return T(data[buffer_offset]);
159  }
160  if (sizeof(T) == sizeof(uint16_t)) {
161  return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0));
162  }
163 
164  if (sizeof(T) == sizeof(uint32_t)) {
165  return get_data<uint16_t>(data, buffer_offset) << 16 | get_data<uint16_t>(data, (buffer_offset + 2));
166  }
167 
168  if (sizeof(T) == sizeof(uint64_t)) {
169  return static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset)) << 32 |
170  (static_cast<uint64_t>(get_data<uint32_t>(data, buffer_offset + 4)));
171  }
172 }
173 
182 inline bool coil_from_vector(int coil, const std::vector<uint8_t> &data) {
183  auto data_byte = coil / 8;
184  return (data[data_byte] & (1 << (coil % 8))) > 0;
185 }
186 
197 template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
198  auto result = (mask & data);
199  if (result == 0 || mask == 0xFFFFFFFF) {
200  return result;
201  }
202  for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
203  if ((mask & (1 << pos)) != 0)
204  return result >> pos;
205  }
206  return 0;
207 }
208 
215 void number_to_payload(std::vector<uint16_t> &data, int64_t value, SensorValueType value_type);
216 
224 int64_t payload_to_number(const std::vector<uint8_t> &data, SensorValueType sensor_value_type, uint8_t offset,
225  uint32_t bitmask);
226 
227 class ModbusController;
228 
229 class SensorItem {
230  public:
231  virtual void parse_and_publish(const std::vector<uint8_t> &data) = 0;
232 
233  void set_custom_data(const std::vector<uint8_t> &data) { custom_data = data; }
234  size_t virtual get_register_size() const {
235  if (register_type == ModbusRegisterType::COIL || register_type == ModbusRegisterType::DISCRETE_INPUT) {
236  return 1;
237  } else { // if CONF_RESPONSE_BYTES is used override the default
238  return response_bytes > 0 ? response_bytes : register_count * 2;
239  }
240  }
241  // Override register size for modbus devices not using 1 register for one dword
242  void set_register_size(uint8_t register_size) { response_bytes = register_size; }
245  uint16_t start_address;
246  uint32_t bitmask;
247  uint8_t offset;
248  uint8_t register_count;
249  uint8_t response_bytes{0};
250  uint16_t skip_updates;
251  std::vector<uint8_t> custom_data{};
252  bool force_new_range{false};
253 };
254 
256  public:
257  ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count,
258  std::function<float()> read_lambda) {
259  this->address = address;
260  this->value_type = value_type;
261  this->register_count = register_count;
262  this->read_lambda = std::move(read_lambda);
263  }
264  uint16_t address;
266  uint8_t register_count;
267  std::function<float()> read_lambda;
268 };
269 
270 // ModbusController::create_register_ranges_ tries to optimize register range
271 // for this the sensors must be ordered by register_type, start_address and bitmask
273  public:
274  bool operator()(const SensorItem *lhs, const SensorItem *rhs) const {
275  // first sort according to register type
276  if (lhs->register_type != rhs->register_type) {
277  return lhs->register_type < rhs->register_type;
278  }
279 
280  // ensure that sensor with force_new_range set are before the others
281  if (lhs->force_new_range != rhs->force_new_range) {
282  return lhs->force_new_range > rhs->force_new_range;
283  }
284 
285  // sort by start address
286  if (lhs->start_address != rhs->start_address) {
287  return lhs->start_address < rhs->start_address;
288  }
289 
290  // sort by offset (ensures update of sensors in ascending order)
291  if (lhs->offset != rhs->offset) {
292  return lhs->offset < rhs->offset;
293  }
294 
295  // The pointer to the sensor is used last to ensure that
296  // multiple sensors with the same values can be added with a stable sort order.
297  return lhs < rhs;
298  }
299 };
300 
301 using SensorSet = std::set<SensorItem *, SensorItemsComparator>;
302 
304  uint16_t start_address;
306  uint8_t register_count;
307  uint16_t skip_updates; // the config value
308  SensorSet sensors; // all sensors of this range
309  uint16_t skip_updates_counter; // the running value
310 };
311 
313  public:
314  static const size_t MAX_PAYLOAD_BYTES = 240;
317  uint16_t register_count;
320  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
321  on_data_func;
322  std::vector<uint8_t> payload = {};
323  bool send();
325  bool should_retry(uint8_t max_retries) { return this->send_count_ <= max_retries; };
326 
328 
337  static ModbusCommandItem create_read_command(
338  ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count,
339  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
340  &&handler);
349  static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type,
350  uint16_t start_address, uint16_t register_count);
360  static ModbusCommandItem create_write_multiple_command(ModbusController *modbusdevice, uint16_t start_address,
361  uint16_t register_count, const std::vector<uint16_t> &values);
370  static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
371  uint16_t value);
379  static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value);
380 
388  static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address,
389  const std::vector<bool> &values);
397  static ModbusCommandItem create_custom_command(
398  ModbusController *modbusdevice, const std::vector<uint8_t> &values,
399  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
400  &&handler = nullptr);
401 
409  static ModbusCommandItem create_custom_command(
410  ModbusController *modbusdevice, const std::vector<uint16_t> &values,
411  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
412  &&handler = nullptr);
413 
414  bool is_equal(const ModbusCommandItem &other);
415 
416  protected:
417  // wrong commands (esp. custom commands) can block the send queue, limit the number of repeats.
419  uint8_t send_count_{0};
420 };
421 
431  public:
432  void dump_config() override;
433  void loop() override;
434  void setup() override;
435  void update() override;
436 
438  void queue_command(const ModbusCommandItem &command);
440  void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
442  void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
444  void on_modbus_data(const std::vector<uint8_t> &data) override;
446  void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
448  void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
450  void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
453  void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
454  const std::vector<uint8_t> &data);
456  void set_allow_duplicate_commands(bool allow_duplicate_commands) {
457  this->allow_duplicate_commands_ = allow_duplicate_commands;
458  }
460  bool get_allow_duplicate_commands() { return this->allow_duplicate_commands_; }
462  void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; }
464  void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
466  size_t get_command_queue_length() { return command_queue_.size(); }
468  bool get_module_offline() { return module_offline_; }
470  void add_on_command_sent_callback(std::function<void(int, int)> &&callback);
472  void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
474  uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
475 
476  protected:
478  size_t create_register_ranges_();
479  // find register in sensormap. Returns iterator with all registers having the same start address
480  SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const;
482  void update_range_(RegisterRange &r);
484  void process_modbus_data_(const ModbusCommandItem *response);
486  bool send_next_command_();
488  void dump_sensors_();
492  std::vector<ServerRegister *> server_registers_;
494  std::vector<RegisterRange> register_ranges_;
496  std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
498  std::queue<std::unique_ptr<ModbusCommandItem>> incoming_queue_;
510  uint8_t max_cmd_retries_{4};
511  CallbackManager<void(int, int)> command_sent_callback_{};
512 };
513 
519 inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
520  int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
521 
522  float float_value;
524  float_value = bit_cast<float>(static_cast<uint32_t>(number));
525  } else {
526  float_value = static_cast<float>(number);
527  }
528 
529  return float_value;
530 }
531 
532 inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
533  int64_t val;
534 
535  if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) {
536  val = bit_cast<uint32_t>(value);
537  } else {
538  val = llroundf(value);
539  }
540 
541  std::vector<uint16_t> data;
542  number_to_payload(data, val, value_type);
543  return data;
544 }
545 
546 } // namespace modbus_controller
547 } // namespace esphome
void setup()
void loop()
void add_server_register(ServerRegister *server_register)
Registers a server register with the controller. Called by esphomes code generator.
bool operator()(const SensorItem *lhs, const SensorItem *rhs) const
uint16_t word_from_hex_str(const std::string &value, uint8_t pos)
Get a word from a hex string.
bool module_offline_
if module didn&#39;t respond the last command
std::vector< ServerRegister * > server_registers_
Collection of all server registers for this component.
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...
std::vector< uint16_t > float_to_payload(float value, SensorValueType value_type)
uint16_t command_throttle_
min time in ms between sending modbus commands
bool allow_duplicate_commands_
if duplicate commands can be sent
mopeka_std_values val[4]
This class simplifies creating components that periodically check a state.
Definition: component.h:283
bool should_retry(uint8_t max_retries)
Check if the command should be retried based on the max_retries parameter.
uint8_t get_max_cmd_retries()
get how many times a command will be (re)sent if no response is received
T get_data(const std::vector< uint8_t > &data, size_t buffer_offset)
Extract data from modbus response buffer.
uint32_t last_command_timestamp_
when was the last send operation
bool coil_from_vector(int coil, const std::vector< uint8_t > &data)
Extract coil data from modbus response buffer Responses for coil are packed into bytes ...
void set_register_size(uint8_t register_size)
SensorSet sensorset_
Collection of all sensors for this component.
uint64_t qword_from_hex_str(const std::string &value, uint8_t pos)
Get a qword from a hex string.
void set_offline_skip_updates(uint16_t offline_skip_updates)
called by esphome generated code to set the offline_skip_updates
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count, std::function< float()> read_lambda)
uint32_t dword_from_hex_str(const std::string &value, uint8_t pos)
Get a dword from a hex string.
float payload_to_float(const std::vector< uint8_t > &data, const SensorItem &item)
Convert vector<uint8_t> response payload to float.
size_t get_command_queue_length()
get the number of queued modbus commands (should be mostly empty)
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.
uint8_t byte_from_hex_str(const std::string &value, uint8_t pos)
Get a byte from a hex string hex_byte_from_str("1122",1) returns uint_8 value 0x22 == 34 hex_byte_fro...
void add_sensor_item(SensorItem *item)
Registers a sensor with the controller. Called by esphomes code generator.
void set_command_throttle(uint16_t command_throttle)
called by esphome generated code to set the command_throttle period
void set_allow_duplicate_commands(bool allow_duplicate_commands)
Allow a duplicate command to be sent.
std::list< std::unique_ptr< ModbusCommandItem > > command_queue_
Hold the pending requests to be sent.
ModbusFunctionCode modbus_register_write_function(ModbusRegisterType reg_type)
ModbusFunctionCode modbus_register_read_function(ModbusRegisterType reg_type)
bool get_allow_duplicate_commands()
get if a duplicate command can be sent
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.
std::vector< RegisterRange > register_ranges_
Continuous range of modbus registers.
bool get_module_offline()
get if the module is offline, didn&#39;t respond the last command
void set_max_cmd_retries(uint8_t max_cmd_retries)
called by esphome generated code to set the max_cmd_retries.
To bit_cast(const From &src)
Convert data between types, without aliasing issues or undefined behaviour.
Definition: helpers.h:121
void set_custom_data(const std::vector< uint8_t > &data)
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
uint8_t address
Definition: bl0906.h:211
uint16_t offline_skip_updates_
how many updates to skip if module is offline
std::set< SensorItem *, SensorItemsComparator > SensorSet
std::queue< std::unique_ptr< ModbusCommandItem > > incoming_queue_
modbus response data waiting to get processed