ESPHome 2026.10.0-dev
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 // Remove before 2027.3.0
24 CUSTOM ESPDEPRECATED("0x00 is not a function code; use FunctionCode::INVALID for the sentinel. Removed in 2027.3.0",
25 "2026.9.0") = 0x00,
26 READ_COILS = 0x01,
30 WRITE_SINGLE_COIL = 0x05,
32 READ_EXCEPTION_STATUS = 0x07, // not implemented
33 DIAGNOSTICS = 0x08, // not implemented
34 GET_COMM_EVENT_COUNTER = 0x0B, // not implemented
35 GET_COMM_EVENT_LOG = 0x0C, // not implemented
38 REPORT_SERVER_ID = 0x11, // not implemented
39 READ_FILE_RECORD = 0x14, // not implemented
40 WRITE_FILE_RECORD = 0x15, // not implemented
41 MASK_WRITE_REGISTER = 0x16, // not implemented
43 READ_FIFO_QUEUE = 0x18, // not implemented
44};
45
46// Remove before 2027.2.0
47using ModbusFunctionCode ESPDEPRECATED("Use modbus::FunctionCode instead. Removed in 2027.2.0",
48 "2026.8.0") = FunctionCode;
49
50inline bool operator==(FunctionCode lhs, uint8_t rhs) { return static_cast<uint8_t>(lhs) == rhs; }
51inline bool operator==(uint8_t lhs, FunctionCode rhs) { return lhs == static_cast<uint8_t>(rhs); }
52inline bool operator!=(FunctionCode lhs, uint8_t rhs) { return !(static_cast<uint8_t>(lhs) == rhs); }
53inline bool operator!=(uint8_t lhs, FunctionCode rhs) { return !(lhs == static_cast<uint8_t>(rhs)); }
54
55// 4.3 MODBUS Data model. "Entity" is the spec's umbrella for the four primary tables; only the
56// 16-bit tables are registers (coils and discrete inputs are bits), so the enum is not named
57// RegisterType.
58enum class EntityType : uint8_t {
59 CUSTOM = 0x00,
60 COIL = 0x01,
61 DISCRETE_INPUT = 0x02,
62 HOLDING = 0x03,
63 // Named INPUT_REGISTER (not INPUT) because Arduino cores define INPUT as a macro.
64 INPUT_REGISTER = 0x04,
65 // Remove before 2027.2.0
66 READ ESPDEPRECATED("Use EntityType::INPUT_REGISTER instead. Removed in 2027.2.0", "2026.7.0") = INPUT_REGISTER,
67};
68
69// Remove before 2027.2.0
70using ModbusRegisterType ESPDEPRECATED("Use modbus::EntityType instead. Removed in 2027.2.0", "2026.8.0") = EntityType;
71
72// 7 MODBUS Exception Responses:
73const uint8_t FUNCTION_CODE_MASK = 0x7F;
74const uint8_t FUNCTION_CODE_EXCEPTION_MASK = 0x80;
75
76enum class ExceptionCode : uint8_t {
77 ILLEGAL_FUNCTION = 0x01,
79 ILLEGAL_DATA_VALUE = 0x03,
81 ACKNOWLEDGE = 0x05,
82 SERVER_DEVICE_BUSY = 0x06,
86};
87
88// Remove before 2027.2.0
89using ModbusExceptionCode ESPDEPRECATED("Use modbus::ExceptionCode instead. Removed in 2027.2.0",
90 "2026.8.0") = ExceptionCode;
91
92// 6.11 15 (0x0F) Write Multiple Coils
93static constexpr uint16_t MAX_NUM_OF_COILS_TO_WRITE = 1968; // 0x7B0
94
95// 6.12 16 (0x10) Write Multiple registers:
96static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B
97
98// 6.17 23 (0x17) Read/Write Multiple Registers:
99static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_WRITE_RW = 121; // 0x79
100
101// 6.1 01 (0x01) Read Coils
102// 6.2 02 (0x02) Read Discrete Inputs
103static constexpr uint16_t MAX_NUM_OF_COILS_TO_READ = 2000; // 0x7D0
104static constexpr uint16_t MAX_NUM_OF_DISCRETE_INPUTS_TO_READ = 2000; // 0x7D0
105
106// 6.3 03 (0x03) Read Holding Registers
107// 6.4 04 (0x04) Read Input Registers
108static constexpr uint16_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D
109
110// Smallest possible frame is 4 bytes (custom function with no data): address(1) + function(1) + CRC(2)
111static constexpr uint16_t MIN_FRAME_SIZE = 4;
112static constexpr uint16_t MIN_PDU_SIZE = 1;
113static constexpr uint16_t MAX_PDU_SIZE = 253; // Max PDU size is 256 - address(1) - CRC(2) = 253
114static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 256 - CRC(2) = 254
115// A read request PDU is always function code(1) + start address(2) + quantity(2)
116static constexpr uint16_t READ_PDU_SIZE = 5;
117// A single-write PDU is always function code(1) + address(2) + value(2)
118static constexpr uint16_t WRITE_SINGLE_PDU_SIZE = 5;
119// A multiple-write PDU starts with function code(1) + start address(2) + quantity(2) + byte count(1),
120// followed by two bytes per register.
121static constexpr uint16_t WRITE_MULTIPLE_HEADER_SIZE = 6;
122static constexpr uint16_t MAX_FRAME_SIZE = 256;
123
124// 4.1 Address 0 is the broadcast address: the request is processed by every device and never answered.
125static constexpr uint8_t BROADCAST_ADDRESS = 0;
126
127// Both send paths bound their payload so the framed result lands exactly on the RTU limit: a client
128// PDU gains an address byte and a CRC, a raw server frame gains a CRC. send_frame_() therefore never
129// has to check the framed size - it cannot be exceeded.
130static_assert(MAX_PDU_SIZE + 3 == MAX_FRAME_SIZE, "a framed client PDU must fill the RTU frame limit");
131static_assert(MAX_RAW_SIZE + 2 == MAX_FRAME_SIZE, "a framed raw server payload must fill the RTU frame limit");
133constexpr size_t packed_bit_bytes(size_t bits) { return (bits + 7) / 8; }
134
135// A coil/discrete-input read answers with byte count(1) + packed_bit_bytes(count) bytes, which has to fit
136// the raw frame body. The runtime check on that path catches a caller entering with bytes already written;
137// this catches the other way in, raising the ceiling past what a frame can carry.
138static_assert(1 + packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ) <= MAX_RAW_SIZE,
139 "MAX_NUM_OF_COILS_TO_READ yields a read response larger than MAX_RAW_SIZE");
140static_assert(1 + packed_bit_bytes(MAX_NUM_OF_DISCRETE_INPUTS_TO_READ) <= MAX_RAW_SIZE,
141 "MAX_NUM_OF_DISCRETE_INPUTS_TO_READ yields a read response larger than MAX_RAW_SIZE");
142
143// The coil and discrete-input ceilings are separate limits in the spec but hold the same value, so the
144// read paths validate both against MAX_NUM_OF_COILS_TO_READ. Should the spec ever split them, this fires.
145static_assert(MAX_NUM_OF_COILS_TO_READ == MAX_NUM_OF_DISCRETE_INPUTS_TO_READ,
146 "the coil and discrete-input read ceilings must match");
147
156 public:
157 PackedBits(std::span<const uint8_t> data, uint16_t count) : data_(data), count_(count) {}
159 bool operator[](size_t bit) const { return (this->data_[bit / 8] & (1 << (bit % 8))) != 0; }
161 uint16_t size() const { return this->count_; }
165 std::span<const uint8_t> bytes() const {
166 return this->data_.first(std::min<size_t>(packed_bit_bytes(this->count_), this->data_.size()));
167 }
168
169 private:
170 std::span<const uint8_t> data_; // must cover ceil(count_ / 8) bytes
171 uint16_t count_;
172};
173
178 public:
179 MutablePackedBits(std::span<uint8_t> data, uint16_t count) : data_(data), count_(count) {}
180 bool operator[](size_t bit) const { return (this->data_[bit / 8] & (1 << (bit % 8))) != 0; }
183 void set(size_t bit, bool value) {
184 if (bit >= this->count_ || bit / 8 >= this->data_.size())
185 return;
186 if (value) {
187 this->data_[bit / 8] |= (1 << (bit % 8));
188 } else {
189 this->data_[bit / 8] &= ~(1 << (bit % 8));
190 }
191 }
192 uint16_t size() const { return this->count_; }
193 operator PackedBits() const { return PackedBits(this->data_, this->count_); }
194
195 private:
196 std::span<uint8_t> data_; // must cover ceil(count_ / 8) bytes
197 uint16_t count_;
198};
199
201} // 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.