ESPHome 2026.8.0-dev
Loading...
Searching...
No Matches
modbus_server.cpp
Go to the documentation of this file.
1#include "modbus_server.h"
3#include "esphome/core/log.h"
4
8
9static const char *const TAG = "modbus_server";
10
11// The widest Modbus value type (QWORD) spans four registers.
12static constexpr uint8_t MAX_REGISTERS_PER_VALUE = 4;
13// number_to_payload() encodes the 64-bit value returned by read_lambda() into 16-bit registers, so the
14// widest possible value spans exactly sizeof(int64_t) / sizeof(uint16_t) registers. Tie the bound to that
15// source so a future wider value type -- which would require widening the encoded value itself -- can't
16// silently overflow the value_words buffer below (StaticVector::push_back drops words past capacity).
17static_assert(MAX_REGISTERS_PER_VALUE == sizeof(int64_t) / sizeof(uint16_t),
18 "MAX_REGISTERS_PER_VALUE must match the register span of the widest encodable value");
19
21 for (auto *server_register : this->server_registers_) {
22 if (address >= server_register->address &&
23 address < static_cast<uint32_t>(server_register->address) + server_register->register_count) {
24 return server_register;
25 }
26 }
27 return nullptr;
28}
29
30modbus::ResponseStatus ModbusServer::on_read_registers(uint16_t start_address, uint16_t number_of_registers,
31 modbus::RegisterValues &registers) {
32 ESP_LOGV(TAG,
33 "Received read holding/input registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%X.",
34 this->address_, start_address, number_of_registers);
35
36 const uint32_t end_address = static_cast<uint32_t>(start_address) + number_of_registers;
37 uint32_t current_address = start_address;
38 while (current_address < end_address) {
39 ServerRegister *server_register = this->find_containing_register_(current_address);
40
41 if (server_register == nullptr) {
42 // Unregistered address: optionally answer with the courtesy default, otherwise reject.
44 current_address <= this->server_courtesy_response_.register_last_address) {
45 ESP_LOGV(TAG, "No register at 0x%04X; returning courtesy default %" PRIu16 ".",
46 static_cast<uint16_t>(current_address), this->server_courtesy_response_.register_value);
48 current_address += 1; // the courtesy default is always a single register
49 continue;
50 }
51 ESP_LOGW(TAG, "No register at 0x%04X and courtesy default not allowed. Sending exception response.",
52 static_cast<uint16_t>(current_address));
54 }
55
56 if (!server_register->read_lambda) {
57 // Registered but not readable (write-only); don't mask it with the courtesy default.
58 ESP_LOGW(TAG, "Register at 0x%04X is not readable. Sending exception response.", server_register->address);
60 }
61
62 // A multi-register value is normally atomic: the request must start at its first register and cover all of
63 // it. A value may opt in to partial reads, in which case the request may start inside it or stop short of
64 // its end and we return only the covered words.
65 const uint16_t value_offset = static_cast<uint16_t>(current_address - server_register->address);
66 const uint16_t words_available = static_cast<uint16_t>(server_register->register_count - value_offset);
67 const uint16_t words_wanted = static_cast<uint16_t>(end_address - current_address);
68 const uint16_t take = words_available < words_wanted ? words_available : words_wanted;
69 const bool clipped = value_offset != 0 || take != server_register->register_count;
70 if (clipped && !server_register->allow_partial_read) {
71 ESP_LOGW(TAG,
72 "Read clips the multi-register value at 0x%04X, which does not allow partial reads. "
73 "Sending exception response.",
74 server_register->address);
76 }
77
78 int64_t value = server_register->read_lambda();
80 ESP_LOGV(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
81 server_register->address, static_cast<size_t>(server_register->value_type),
82 server_register->register_count, server_register->format_value(value, value_buf, sizeof(value_buf)));
83
84 // Encode the whole value once (wire word order) and emit only the covered words. Slicing the encoded words
85 // handles the reversed value types for free, since number_to_payload already emits in wire order.
87 modbus::helpers::number_to_payload(value_words, value, server_register->value_type);
88 if (value_offset + take > value_words.size()) {
89 // The value encoded to fewer words than its register span (e.g. a RAW register); treat as a device fault.
90 ESP_LOGE(TAG, "Register at 0x%04X did not encode to %u registers", server_register->address,
91 server_register->register_count);
93 }
94 for (uint16_t i = 0; i < take; i++) {
95 registers.push_back(value_words[value_offset + i]);
96 }
97 current_address += take;
98 }
99
100 return {};
101}
102
104 const modbus::RegisterValues &registers) {
105 // registers holds the values to write in host byte order; its size is the register count.
106 ESP_LOGV(TAG, "Received write registers for device 0x%X. Start address: 0x%X. Number of registers: 0x%zX.",
107 this->address_, start_address, registers.size());
108
109 auto for_each_register =
110 [this, start_address,
111 &registers](const std::function<bool(ServerRegister *, uint16_t register_offset)> &callback) -> bool {
112 uint16_t register_offset = 0;
113 for (uint32_t current_address = start_address; current_address < start_address + registers.size();) {
114 bool ok = false;
115 for (auto *server_register : this->server_registers_) {
116 if (server_register->address == current_address) {
117 ok = callback(server_register, register_offset);
118 current_address += server_register->register_count;
119 register_offset += server_register->register_count;
120 break;
121 }
122 }
123
124 if (!ok) {
125 return false;
126 }
127 }
128 return true;
129 };
130
131 // Pre-flight: every targeted register must be writable AND have its full value present in the request,
132 // so we never apply a partial write before discovering a problem. The commit pass below re-runs
133 // registers_to_number rather than caching the decoded values: using the same function for the check and
134 // the write keeps a single source of truth for the decode bound, independent of how register_count was set.
135 ModbusExceptionCode precheck = ModbusExceptionCode::ILLEGAL_DATA_ADDRESS; // unmatched or unwritable register
136 if (!for_each_register([&precheck, &registers](ServerRegister *server_register, uint16_t register_offset) -> bool {
137 if (server_register->write_lambda == nullptr) {
138 return false; // unwritable -> ILLEGAL_DATA_ADDRESS
139 }
140 if (!registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
141 server_register->value_type)
142 .has_value()) {
143 precheck = ModbusExceptionCode::ILLEGAL_DATA_VALUE; // request doesn't supply the full value
144 return false;
145 }
146 return true;
147 })) {
148 ESP_LOGW(TAG, "Write request rejected before applying any register. Sending exception response.");
149 return precheck;
150 }
151
152 // Commit: every value is known writable and decodable, so the only failure now is a user write callback
153 // rejecting the value at runtime -- which cannot be rolled back.
154 if (!for_each_register([&registers](ServerRegister *server_register, uint16_t register_offset) {
155 int64_t number = registers_to_number(registers.data() + register_offset, registers.size() - register_offset,
156 server_register->value_type)
157 .value_or(0);
158 return server_register->write_lambda(number);
159 })) {
160 ESP_LOGW(TAG, "A register write callback failed mid-sequence; earlier writes were already applied.");
162 }
163
164 // Success: the caller builds the write response (an echo of the request header).
165 return {};
166}
167
169 ESP_LOGCONFIG(TAG,
170 "ModbusServer:\n"
171 " Address: 0x%02X\n"
172 " Server Courtesy Response:\n"
173 " Enabled: %s\n"
174 " Register Last Address: 0x%02X\n"
175 " Register Value: %" PRIu16,
176 this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
177 this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
178
179#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
180 ESP_LOGCONFIG(TAG, "server registers");
181 for (auto &r : this->server_registers_) {
182 ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
183 static_cast<uint8_t>(r->value_type), r->register_count);
184 }
185#endif
186}
187
188} // namespace esphome::modbus_server
uint8_t address
Definition bl0906.h:4
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:222
size_t size() const
Definition helpers.h:282
void push_back(const T &value)
Definition helpers.h:255
modbus::ResponseStatus on_write_registers(uint16_t start_address, const modbus::RegisterValues &registers) final
called when a modbus request (function code 0x06 or 0x10) was parsed without errors
ServerRegister * find_containing_register_(uint32_t address) const
Find the registered value whose register span contains address, or nullptr if none does.
std::vector< ServerRegister * > server_registers_
Collection of all server registers for this component.
modbus::ResponseStatus on_read_registers(uint16_t start_address, uint16_t number_of_registers, modbus::RegisterValues &registers) final
called when a modbus request (function code 0x03 or 0x04) was parsed without errors
ServerCourtesyResponse server_courtesy_response_
Server courtesy response.
const char * format_value(int64_t value, char *buf, size_t buf_size) const
static constexpr size_t FORMAT_VALUE_BUF_SIZE
void number_to_payload(Container &data, int64_t value, SensorValueType value_type)
Append the Modbus register words for value to data.
std::optional< int64_t > registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type)
Reconstruct a number from register words (host byte order).
std::optional< ModbusExceptionCode > ResponseStatus
Definition modbus.h:221
static void uint32_t