ESPHome 2026.8.2
Loading...
Searching...
No Matches
modbus_definitions.h
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cstdint>
5#include <span>
6
9
10namespace esphome::modbus {
11
14// 5 Function Code Categories
15const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT = 65; // 0x41
16const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END = 72; // 0x48
17
18const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT = 100; // 0x64
19const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E
20
21enum class FunctionCode : uint8_t {
22 INVALID = 0x00, // 0x00 is not a valid function code (even for custom functions).
23 CUSTOM = 0x00, // The CUSTOM alias should be removed in future.
24 READ_COILS = 0x01,
28 WRITE_SINGLE_COIL = 0x05,
30 READ_EXCEPTION_STATUS = 0x07, // not implemented
31 DIAGNOSTICS = 0x08, // not implemented
32 GET_COMM_EVENT_COUNTER = 0x0B, // not implemented
33 GET_COMM_EVENT_LOG = 0x0C, // not implemented
36 REPORT_SERVER_ID = 0x11, // not implemented
37 READ_FILE_RECORD = 0x14, // not implemented
38 WRITE_FILE_RECORD = 0x15, // not implemented
39 MASK_WRITE_REGISTER = 0x16, // not implemented
41 READ_FIFO_QUEUE = 0x18, // not implemented
42};
43
44// Remove before 2027.2.0
45using ModbusFunctionCode ESPDEPRECATED("Use modbus::FunctionCode instead. Removed in 2027.2.0",
46 "2026.8.0") = FunctionCode;
47
48/*Allow direct comparison operators between FunctionCode and uint8_t*/
49inline bool operator==(FunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) == rhs; }
50inline bool operator==(uint8_t lhs, FunctionCode rhs) { return lhs == static_cast<uint8_t>(rhs); }
51inline bool operator!=(FunctionCode lhs, uint8_t rhs) { return !(static_cast<uint8_t>(lhs) == rhs); }
52inline bool operator!=(uint8_t lhs, FunctionCode rhs) { return !(lhs == static_cast<uint8_t>(rhs)); }
53
54// 4.3 MODBUS Data model. "Entity" is the spec's umbrella for the four primary tables; only the
55// 16-bit tables are registers (coils and discrete inputs are bits), so the enum is not named
56// RegisterType.
57enum class EntityType : uint8_t {
58 CUSTOM = 0x00,
59 COIL = 0x01,
60 DISCRETE_INPUT = 0x02,
61 HOLDING = 0x03,
62 // Named INPUT_REGISTER (not INPUT) because Arduino cores define INPUT as a macro.
63 INPUT_REGISTER = 0x04,
64 // Remove before 2027.2.0
65 READ ESPDEPRECATED("Use EntityType::INPUT_REGISTER instead. Removed in 2027.2.0", "2026.7.0") = INPUT_REGISTER,
66};
67
68// Remove before 2027.2.0
69using ModbusRegisterType ESPDEPRECATED("Use modbus::EntityType instead. Removed in 2027.2.0", "2026.8.0") = EntityType;
70
71// 7 MODBUS Exception Responses:
72const uint8_t FUNCTION_CODE_MASK = 0x7F;
73const uint8_t FUNCTION_CODE_EXCEPTION_MASK = 0x80;
74
75enum class ExceptionCode : uint8_t {
76 ILLEGAL_FUNCTION = 0x01,
78 ILLEGAL_DATA_VALUE = 0x03,
80 ACKNOWLEDGE = 0x05,
81 SERVER_DEVICE_BUSY = 0x06,
85};
86
87// Remove before 2027.2.0
88using ModbusExceptionCode ESPDEPRECATED("Use modbus::ExceptionCode instead. Removed in 2027.2.0",
89 "2026.8.0") = ExceptionCode;
90
91// 6.11 15 (0x0F) Write Multiple Coils
92static constexpr uint16_t MAX_NUM_OF_COILS_TO_WRITE = 1968; // 0x7B0
93
94// 6.12 16 (0x10) Write Multiple registers:
95static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B
96
97// 6.17 23 (0x17) Read/Write Multiple Registers:
98static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_WRITE_RW = 121; // 0x79
99
100// 6.1 01 (0x01) Read Coils
101// 6.2 02 (0x02) Read Discrete Inputs
102static constexpr uint16_t MAX_NUM_OF_COILS_TO_READ = 2000; // 0x7D0
103static constexpr uint16_t MAX_NUM_OF_DISCRETE_INPUTS_TO_READ = 2000; // 0x7D0
104
105// 6.3 03 (0x03) Read Holding Registers
106// 6.4 04 (0x04) Read Input Registers
107static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D
108
109// Smallest possible frame is 4 bytes (custom function with no data): address(1) + function(1) + CRC(2)
110static constexpr uint16_t MIN_FRAME_SIZE = 4;
111static constexpr uint16_t MIN_PDU_SIZE = 1;
112static constexpr uint16_t MAX_PDU_SIZE = 253; // Max PDU size is 256 - address(1) - CRC(2) = 253
113static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 256 - CRC(2) = 254
114// A read request PDU is always function code(1) + start address(2) + quantity(2)
115static constexpr uint16_t READ_PDU_SIZE = 5;
116// A single-write PDU is always function code(1) + address(2) + value(2)
117static constexpr uint16_t WRITE_SINGLE_PDU_SIZE = 5;
118static constexpr uint16_t MAX_FRAME_SIZE = 256;
119
120// 4.1 Address 0 is the broadcast address: the request is processed by every device and never answered.
121static constexpr uint8_t BROADCAST_ADDRESS = 0;
122
123// Both send paths bound their payload so the framed result lands exactly on the RTU limit: a client
124// PDU gains an address byte and a CRC, a raw server frame gains a CRC. send_frame_() therefore never
125// has to check the framed size - it cannot be exceeded.
126static_assert(MAX_PDU_SIZE + 3 == MAX_FRAME_SIZE, "a framed client PDU must fill the RTU frame limit");
127static_assert(MAX_RAW_SIZE + 2 == MAX_FRAME_SIZE, "a framed raw server payload must fill the RTU frame limit");
129constexpr size_t packed_bit_bytes(size_t bits) { return (bits + 7) / 8; }
130
131// A coil/discrete-input read answers with byte count(1) + packed_bit_bytes(count) bytes, which has to fit
132// the raw frame body. The runtime check on that path catches a caller entering with bytes already written;
133// this catches the other way in, raising the ceiling past what a frame can carry.
134static_assert(1 + packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ) <= MAX_RAW_SIZE,
135 "MAX_NUM_OF_COILS_TO_READ yields a read response larger than MAX_RAW_SIZE");
136static_assert(1 + packed_bit_bytes(MAX_NUM_OF_DISCRETE_INPUTS_TO_READ) <= MAX_RAW_SIZE,
137 "MAX_NUM_OF_DISCRETE_INPUTS_TO_READ yields a read response larger than MAX_RAW_SIZE");
138
139// The coil and discrete-input ceilings are separate limits in the spec but hold the same value, so the
140// read paths validate both against MAX_NUM_OF_COILS_TO_READ. Should the spec ever split them, this fires.
141static_assert(MAX_NUM_OF_COILS_TO_READ == MAX_NUM_OF_DISCRETE_INPUTS_TO_READ,
142 "the coil and discrete-input read ceilings must match");
143
152 public:
153 PackedBits(std::span<const uint8_t> data, uint16_t count) : data_(data), count_(count) {}
155 bool operator[](size_t bit) const { return (this->data_[bit / 8] & (1 << (bit % 8))) != 0; }
157 uint16_t size() const { return this->count_; }
161 std::span<const uint8_t> bytes() const {
162 return this->data_.first(std::min<size_t>(packed_bit_bytes(this->count_), this->data_.size()));
163 }
164
165 private:
166 std::span<const uint8_t> data_; // must cover ceil(count_ / 8) bytes
167 uint16_t count_;
168};
169
174 public:
175 MutablePackedBits(std::span<uint8_t> data, uint16_t count) : data_(data), count_(count) {}
176 bool operator[](size_t bit) const { return (this->data_[bit / 8] & (1 << (bit % 8))) != 0; }
179 void set(size_t bit, bool value) {
180 if (bit >= this->count_ || bit / 8 >= this->data_.size())
181 return;
182 if (value) {
183 this->data_[bit / 8] |= (1 << (bit % 8));
184 } else {
185 this->data_[bit / 8] &= ~(1 << (bit % 8));
186 }
187 }
188 uint16_t size() const { return this->count_; }
189 operator PackedBits() const { return PackedBits(this->data_, this->count_); }
190
191 private:
192 std::span<uint8_t> data_; // must cover ceil(count_ / 8) bytes
193 uint16_t count_;
194};
195
197} // namespace esphome::modbus
Mutable counterpart of PackedBits: set() writes bits in place (deliberately no proxy operator[]=).
MutablePackedBits(std::span< uint8_t > data, uint16_t count)
void set(size_t bit, bool value)
Set or clear the given bit.
Read-only view of Modbus-packed bits: bit 0 of byte 0 is the first bit (LSB first),...
bool operator[](size_t bit) const
Value of the given bit; bit must be < size().
std::span< const uint8_t > bytes() const
The underlying packed bytes: exactly ceil(size() / 8) bytes, even when the view was constructed over ...
uint16_t size() const
Number of bits in the view.
PackedBits(std::span< const uint8_t > data, uint16_t count)
const uint8_t FUNCTION_CODE_MASK
const uint8_t FUNCTION_CODE_EXCEPTION_MASK
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_INIT
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_INIT
Modbus definitions from specs: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3....
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END
const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_1_END
bool operator!=(FunctionCode lhs, uint8_t rhs)
bool operator==(FunctionCode lhs, uint8_t rhs)
constexpr size_t packed_bit_bytes(size_t bits)
Bits pack 8 per data byte, rounded up to whole bytes.