ESPHome 2026.8.0
Loading...
Searching...
No Matches
modbus_output.cpp
Go to the documentation of this file.
1#include "modbus_output.h"
3#include "esphome/core/log.h"
4
6
7static const char *const TAG = "modbus_controller.output";
8
9// Maximum bytes to log in verbose hex output
10static constexpr size_t MODBUS_OUTPUT_MAX_LOG_BYTES = 64;
11
16 std::vector<uint16_t> data;
17 auto original_value = value;
18 // Is there are lambda configured?
19 if (this->write_transform_func_.has_value()) {
20 // data is passed by reference
21 // the lambda can fill the empty vector directly
22 // in that case the return value is ignored
23 auto val = (*this->write_transform_func_)(this, value, data);
24 if (val.has_value()) {
25 ESP_LOGV(TAG, "Value overwritten by lambda");
26 value = val.value();
27 } else {
28 ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
29 return;
30 }
31 } else {
32 value = this->multiply_by_ * value;
33 }
34 // lambda didn't set payload
35 if (data.empty()) {
37 }
38
39 ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)",
40 this->start_address, this->register_count, value, original_value);
41
42 // The command declares register_count registers, so the payload must be exactly that many words;
43 // anything else would put a byte count on the wire that disagrees with the quantity field.
44 // number_to_payload() appends nothing for RAW, so an empty payload must be caught before data[0].
45 if (data.empty()) {
46 ESP_LOGW(TAG, "No payload was created for updating output");
47 return;
48 }
49
50 // register_count declares the READ range width - it may pull neighboring registers into one poll -
51 // so a write covers exactly the registers the value occupies: the quantity comes from the payload,
52 // never from register_count (padding to it would zero registers the user only declared for reading).
53 // A payload wider than the declared range means the config and the lambda disagree - drop it.
54 if (data.size() > this->register_count) {
55 ESP_LOGE(TAG, "Payload has %zu registers but register_count is %u; dropping write", data.size(),
56 this->register_count);
57 return;
58 }
59
60 // Create and send the write command
61 optional<ModbusCommandItem> write_cmd;
62 if (this->register_count == 1 && !this->use_write_multiple_) {
63 write_cmd.emplace(
65 } else {
67 this->parent_, this->start_address + this->offset, data.size(), data));
68 }
69 this->parent_->queue_command(std::move(*write_cmd));
70}
71
73 ESP_LOGCONFIG(TAG, "Modbus Float Output:");
74 LOG_FLOAT_OUTPUT(this);
75 ESP_LOGCONFIG(TAG,
76 " Device start address: 0x%X\n"
77 " Register count: %d\n"
78 " Value type: %d",
79 this->start_address, this->register_count, static_cast<int>(this->sensor_value_type));
80}
81
82// ModbusBinaryOutput
84 // This will be called every time the user requests a state change.
85 optional<ModbusCommandItem> cmd;
86 std::vector<uint8_t> data;
87
88 // Is there are lambda configured?
89 if (this->write_transform_func_.has_value()) {
90 // data is passed by reference
91 // the lambda can fill the empty vector directly
92 // in that case the return value is ignored
93 auto val = (*this->write_transform_func_)(this, state, data);
94 if (val.has_value()) {
95 ESP_LOGV(TAG, "Value overwritten by lambda");
96 state = val.value();
97 } else {
98 ESP_LOGV(TAG, "Communication handled by lambda - exiting control");
99 return;
100 }
101 }
102 if (!data.empty()) {
103#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
104 char hex_buf[format_hex_pretty_size(MODBUS_OUTPUT_MAX_LOG_BYTES)];
105#endif
106 ESP_LOGV(TAG, "Modbus binary output write raw: %s",
107 format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
109 this->parent_, data,
110 [this](modbus::EntityType register_type, uint16_t start_address, std::span<const uint8_t> data) {
111 this->parent_->on_write_register_response(register_type, this->start_address, data);
112 }));
113 } else {
114 ESP_LOGV(TAG, "Write new state: value is %s, type is %d address = %X, offset = %x", ONOFF(state),
115 (int) this->register_type, this->start_address, this->offset);
116
117 // offset for coil and discrete inputs is the coil/register number not bytes
118 if (this->use_write_multiple_) {
119 std::vector<bool> states{state};
120 cmd.emplace(
122 } else {
123 cmd.emplace(
125 }
126 }
127 this->parent_->queue_command(std::move(*cmd));
128}
129
131 ESP_LOGCONFIG(TAG, "Modbus Binary Output:");
132 LOG_BINARY_OUTPUT(this);
133 ESP_LOGCONFIG(TAG,
134 " Device start address: 0x%X\n"
135 " Register count: %d\n"
136 " Value type: %d",
137 this->start_address, this->register_count, static_cast<int>(this->sensor_value_type));
138}
139
140} // namespace esphome::modbus_controller
optional< write_transform_func_t > write_transform_func_
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.
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_custom_command(ModbusController *modbusdevice, const std::vector< uint8_t > &values, std::function< void(EntityType register_type, uint16_t start_address, std::span< const uint8_t > data)> &&handler=nullptr)
Create custom modbus command.
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.
void queue_command(ModbusCommandItem command)
Queues a one-shot modbus command (writes, custom commands); taken by value, so std::move to avoid a c...
void on_write_register_response(EntityType register_type, uint16_t start_address, std::span< const uint8_t > data)
Handles a write command acknowledgement (used by write command on_data_func handlers).
void write_state(float value) override
Write a value to the device.
optional< write_transform_func_t > write_transform_func_
uint8_t offset
Position of this sensor's data within its range's response - a byte offset for registers,...
bool state
Definition fan.h:2
mopeka_std_values val[3]
void float_to_payload(Container &data, float value, SensorValueType value_type)
Append a float converted to register words to any push_back container (heap-free with StaticVector).
const std::vector< uint8_t > & data
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:406
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1426