ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
modbus_master.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file pc_modbus_master.h
6 * @brief Modbus TCP master codec + register scanner (PC_ENABLE_MODBUS_MASTER).
7 *
8 * The master/client side of Modbus: build a read-request ADU (MBAP header + PDU)
9 * and parse the slave's response into register values, so an application can poll
10 * or auto-discover a slave's registers. Pure - no sockets, no heap - so it is
11 * host-tested as a full round-trip against the slave codec (pc_modbus_process_adu).
12 * The app supplies the transport (send the ADU, receive the reply).
13 *
14 * Auto-discovery pattern: walk the address space one read at a time; a register
15 * exists where the response parses without a Modbus exception.
16 *
17 * @author Douglas Quigg (dstroy0)
18 * @date 2026
19 */
20
21#ifndef PROTOCORE_MODBUS_MASTER_H
22#define PROTOCORE_MODBUS_MASTER_H
23
24#include "protocore_config.h"
25#include <stddef.h>
26#include <stdint.h>
27
28#if PC_ENABLE_MODBUS_MASTER
29
30/**
31 * @brief Build a read-request ADU (FC 0x03 holding or 0x04 input registers).
32 *
33 * @param fc ModbusFunction::MODBUS_FC_READ_HOLDING_REGS (0x03) or ModbusFunction::MODBUS_FC_READ_INPUT_REGS (0x04).
34 * @param txid transaction id echoed by the slave (caller's correlation token).
35 * @param unit unit / slave id.
36 * @param start first register address.
37 * @param count number of registers (1..125).
38 * @param out destination buffer.
39 * @param cap destination capacity (>= 12).
40 * @return bytes written (12), or 0 on a bad argument / too-small buffer.
41 */
42size_t pc_modbus_build_read(uint8_t fc, uint16_t txid, uint8_t unit, uint16_t start, uint16_t count, uint8_t *out,
43 size_t cap);
44
45/**
46 * @brief Parse a read-response ADU into register values.
47 *
48 * @param adu response bytes (MBAP + PDU).
49 * @param len response length.
50 * @param regs_out destination for parsed 16-bit register values.
51 * @param max_regs capacity of @p regs_out.
52 * @param exception_out set to the Modbus exception code if the slave returned one
53 * (then the function returns 0 registers); 0 otherwise.
54 * @return number of registers parsed (>= 0), or -1 on a malformed/short frame.
55 */
56int pc_modbus_parse_response(const uint8_t *adu, size_t len, uint16_t *regs_out, size_t max_regs,
57 uint8_t *exception_out);
58
59/**
60 * @brief Build a read-bits request ADU (FC 0x01 coils or 0x02 discrete inputs).
61 *
62 * @param fc ModbusFunction::MODBUS_FC_READ_COILS (0x01) or ::MODBUS_FC_READ_DISCRETE_INPUTS (0x02).
63 * @param txid transaction id echoed by the slave.
64 * @param unit unit / slave id.
65 * @param start first bit address.
66 * @param count number of bits (1..2000).
67 * @param out destination buffer.
68 * @param cap destination capacity (>= 12).
69 * @return bytes written (12), or 0 on a bad argument / too-small buffer.
70 */
71size_t pc_modbus_build_read_bits(uint8_t fc, uint16_t txid, uint8_t unit, uint16_t start, uint16_t count, uint8_t *out,
72 size_t cap);
73
74/**
75 * @brief Parse a read-bits response ADU (FC 0x01 / 0x02) into one byte (0/1) per bit.
76 *
77 * The response carries only a byte count, so the caller passes the @p count it requested; the byte count
78 * must equal ceil(count/8). Each requested bit is unpacked LSB-first into @p bits_out as 0 or 1.
79 * @param count the number of bits requested (1..2000).
80 * @param bits_out destination for @p count unpacked bits (nullable to just validate).
81 * @param max_bits capacity of @p bits_out.
82 * @param exception_out set to the Modbus exception code if the slave returned one (then 0 is returned).
83 * @return the number of bits unpacked (@p count, capped by @p max_bits), 0 on an exception, or -1 on a
84 * malformed / short frame or a byte count that disagrees with @p count.
85 */
86int pc_modbus_parse_read_bits_response(const uint8_t *adu, size_t len, uint16_t count, uint8_t *bits_out,
87 size_t max_bits, uint8_t *exception_out);
88
89/**
90 * @brief Build a Write Single Coil request ADU (FC 0x05).
91 *
92 * @param on the coil value; encoded on the wire as 0xFF00 (on) or 0x0000 (off) per the Modbus spec.
93 * @param cap destination capacity (>= 12).
94 * @return bytes written (12), or 0 on a null / too-small buffer.
95 */
96size_t pc_modbus_build_write_single_coil(uint16_t txid, uint8_t unit, uint16_t addr, bool on, uint8_t *out, size_t cap);
97
98/**
99 * @brief Build a Write Multiple Coils request ADU (FC 0x0F).
100 *
101 * @param bits one byte (0/1) per coil to write; packed LSB-first into the wire bytes.
102 * @param count number of coils (1..1968).
103 * @param cap destination capacity (>= 14 + ceil(count/8)).
104 * @return bytes written, or 0 on a bad argument / too-small buffer.
105 */
106size_t pc_modbus_build_write_multiple_coils(uint16_t txid, uint8_t unit, uint16_t start, const uint8_t *bits,
107 uint16_t count, uint8_t *out, size_t cap);
108
109/**
110 * @brief Build a Write Single Register request ADU (FC 0x06).
111 *
112 * @param txid transaction id echoed by the slave.
113 * @param unit unit / slave id.
114 * @param addr register address.
115 * @param value 16-bit value to write.
116 * @param out destination buffer.
117 * @param cap destination capacity (>= 12).
118 * @return bytes written (12), or 0 on a null / too-small buffer.
119 */
120size_t pc_modbus_build_write_single(uint16_t txid, uint8_t unit, uint16_t addr, uint16_t value, uint8_t *out,
121 size_t cap);
122
123/**
124 * @brief Build a Write Multiple Registers request ADU (FC 0x10).
125 *
126 * @param txid transaction id echoed by the slave.
127 * @param unit unit / slave id.
128 * @param start first register address.
129 * @param values the @p count register values.
130 * @param count number of registers (1..123).
131 * @param out destination buffer.
132 * @param cap destination capacity (>= 13 + 2*count).
133 * @return bytes written (13 + 2*count), or 0 on a bad argument / too-small buffer.
134 */
135size_t pc_modbus_build_write_multiple(uint16_t txid, uint8_t unit, uint16_t start, const uint16_t *values,
136 uint16_t count, uint8_t *out, size_t cap);
137
138/**
139 * @brief Parse a write-response ADU (FC 0x05, 0x06, 0x0F, or 0x10).
140 *
141 * A normal reply echoes the address and the written value (single: 0x05 / 0x06) or the start address and
142 * the quantity written (multiple: 0x0F / 0x10) - the coil and register replies share a wire format.
143 * @param adu response bytes (MBAP + PDU).
144 * @param len response length.
145 * @param addr_out set to the echoed address / start (nullable).
146 * @param exception_out set to the Modbus exception code if the slave returned one (then 0 is returned).
147 * @return number written (1 for a single 0x05 / 0x06, the count for a multiple 0x0F / 0x10), 0 on an
148 * exception, or -1 on a malformed / short frame.
149 */
150int pc_modbus_parse_write_response(const uint8_t *adu, size_t len, uint16_t *addr_out, uint8_t *exception_out);
151
152/**
153 * @brief Build a Mask Write Register request ADU (FC 0x16).
154 *
155 * The slave computes reg = (reg AND @p and_mask) OR (@p or_mask AND NOT @p and_mask), so a client can set
156 * or clear individual bits of a register without a read-modify-write race.
157 * @param cap destination capacity (>= 14).
158 * @return bytes written (14), or 0 on a null / too-small buffer.
159 */
160size_t pc_modbus_build_mask_write(uint16_t txid, uint8_t unit, uint16_t addr, uint16_t and_mask, uint16_t or_mask,
161 uint8_t *out, size_t cap);
162
163/**
164 * @brief Build a Read/Write Multiple Registers request ADU (FC 0x17): write a span, then read a span, in
165 * one transaction (the write is applied before the read).
166 * @param read_start / @p read_count the registers to read back (1..125).
167 * @param write_start / @p write_count the registers to write (1..121); @p values holds @p write_count words.
168 * @param cap destination capacity (>= 17 + 2*write_count).
169 * @return bytes written, or 0 on a bad argument / too-small buffer.
170 */
171size_t pc_modbus_build_read_write_multiple(uint16_t txid, uint8_t unit, uint16_t read_start, uint16_t read_count,
172 uint16_t write_start, const uint16_t *values, uint16_t write_count,
173 uint8_t *out, size_t cap);
174
175/**
176 * @brief Parse a Mask Write Register response (FC 0x16), which echoes the address and both masks.
177 * @param addr_out / @p and_out / @p or_out receive the echoed fields (each nullable).
178 * @param exception_out set to the Modbus exception code if the slave returned one.
179 * @return 1 on a normal echo, 0 on an exception, or -1 on a malformed / short frame.
180 */
181int pc_modbus_parse_mask_write_response(const uint8_t *adu, size_t len, uint16_t *addr_out, uint16_t *and_out,
182 uint16_t *or_out, uint8_t *exception_out);
183
184#endif // PC_ENABLE_MODBUS_MASTER
185#endif // PROTOCORE_MODBUS_MASTER_H
User-facing configuration for ProtoCore.