ESPHome  2024.6.1
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;
315  static const uint8_t MAX_SEND_REPEATS = 5;
318  uint16_t register_count;
321  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
322  on_data_func;
323  std::vector<uint8_t> payload = {};
324  bool send();
325  // wrong commands (esp. custom commands) can block the send queue
326  // limit the number of repeats
327  uint8_t send_countdown{MAX_SEND_REPEATS};
329 
338  static ModbusCommandItem create_read_command(
339  ModbusController *modbusdevice, ModbusRegisterType register_type, uint16_t start_address, uint16_t register_count,
340  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
341  &&handler);
350  static ModbusCommandItem create_read_command(ModbusController *modbusdevice, ModbusRegisterType register_type,
351  uint16_t start_address, uint16_t register_count);
361  static ModbusCommandItem create_write_multiple_command(ModbusController *modbusdevice, uint16_t start_address,
362  uint16_t register_count, const std::vector<uint16_t> &values);
371  static ModbusCommandItem create_write_single_command(ModbusController *modbusdevice, uint16_t start_address,
372  uint16_t value);
380  static ModbusCommandItem create_write_single_coil(ModbusController *modbusdevice, uint16_t address, bool value);
381 
389  static ModbusCommandItem create_write_multiple_coils(ModbusController *modbusdevice, uint16_t start_address,
390  const std::vector<bool> &values);
398  static ModbusCommandItem create_custom_command(
399  ModbusController *modbusdevice, const std::vector<uint8_t> &values,
400  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
401  &&handler = nullptr);
402 
410  static ModbusCommandItem create_custom_command(
411  ModbusController *modbusdevice, const std::vector<uint16_t> &values,
412  std::function<void(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data)>
413  &&handler = nullptr);
414 
415  bool is_equal(const ModbusCommandItem &other);
416 };
417 
427  public:
428  void dump_config() override;
429  void loop() override;
430  void setup() override;
431  void update() override;
432 
434  void queue_command(const ModbusCommandItem &command);
436  void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
438  void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
440  void on_modbus_data(const std::vector<uint8_t> &data) override;
442  void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
444  void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
446  void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
449  void on_write_register_response(ModbusRegisterType register_type, uint16_t start_address,
450  const std::vector<uint8_t> &data);
452  void set_command_throttle(uint16_t command_throttle) { this->command_throttle_ = command_throttle; }
454  void set_offline_skip_updates(uint16_t offline_skip_updates) { this->offline_skip_updates_ = offline_skip_updates; }
456  size_t get_command_queue_length() { return command_queue_.size(); }
458  bool get_module_offline() { return module_offline_; }
459 
460  protected:
462  size_t create_register_ranges_();
463  // find register in sensormap. Returns iterator with all registers having the same start address
464  SensorSet find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const;
466  void update_range_(RegisterRange &r);
468  void process_modbus_data_(const ModbusCommandItem *response);
470  bool send_next_command_();
472  void dump_sensors_();
476  std::vector<ServerRegister *> server_registers_;
478  std::vector<RegisterRange> register_ranges_;
480  std::list<std::unique_ptr<ModbusCommandItem>> command_queue_;
482  std::queue<std::unique_ptr<ModbusCommandItem>> incoming_queue_;
491 };
492 
498 inline float payload_to_float(const std::vector<uint8_t> &data, const SensorItem &item) {
499  int64_t number = payload_to_number(data, item.sensor_value_type, item.offset, item.bitmask);
500 
501  float float_value;
503  float_value = bit_cast<float>(static_cast<uint32_t>(number));
504  } else {
505  float_value = static_cast<float>(number);
506  }
507 
508  return float_value;
509 }
510 
511 inline std::vector<uint16_t> float_to_payload(float value, SensorValueType value_type) {
512  int64_t val;
513 
514  if (value_type == SensorValueType::FP32 || value_type == SensorValueType::FP32_R) {
515  val = bit_cast<uint32_t>(value);
516  } else {
517  val = llroundf(value);
518  }
519 
520  std::vector<uint16_t> data;
521  number_to_payload(data, val, value_type);
522  return data;
523 }
524 
525 } // namespace modbus_controller
526 } // 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
mopeka_std_values val[4]
This class simplifies creating components that periodically check a state.
Definition: component.h:283
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
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)
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
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)
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
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