ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
enip.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file enip.h
6 * @brief EtherNet/IP encapsulation codec (PROTOCORE_ENABLE_ENIP) - zero-heap builder + parser
7 * for the ODVA EtherNet/IP encapsulation layer (TCP/UDP 44818), the transport that
8 * carries CIP. The reusable base for CIP / EtherNet/IP explicit messaging.
9 *
10 * The 24-octet encapsulation header (all fields LITTLE-endian):
11 * @code
12 * Command(2) Length(2) SessionHandle(4) Status(4) SenderContext(8) Options(4) <data>
13 * @endcode
14 * Length is the octet count of the command-specific data that follows the header.
15 * - RegisterSession (0x0065): data = protocol version(2)=1 + options flags(2)=0; the reply
16 * carries the assigned session handle.
17 * - SendRRData (0x006F): data = interface handle(4)=0 + timeout(2) + a Common Packet Format
18 * block: item count(2), then items (Type ID(2), Length(2), data). Unconnected explicit
19 * messaging uses a Null Address item (0x0000) then an Unconnected Data item (0x00B2)
20 * carrying the CIP request/response.
21 *
22 * Commands + CPF item types verified against the Wireshark ENIP dissector. This codec frames
23 * the encapsulation; the CIP message inside the Unconnected Data item is the application's.
24 *
25 * @author Douglas Quigg (dstroy0)
26 * @date 2026
27 */
28
29#ifndef PROTOCORE_ENIP_H
30#define PROTOCORE_ENIP_H
31
32#include "protocore_config.h" // the entry point: protocore_types.h for the widths
33
34#if PROTOCORE_ENABLE_ENIP
35
37
38// This module holds nothing between calls, so it carves no borrow and states none. An entry
39// takes one all the same, and never reads it, so every namespace in the tree is invoked the
40// same way.
41
42#define EIP_HEADER_SIZE 24
43
44// Encapsulation commands.
45#define EIP_CMD_LIST_SERVICES 0x0004
46#define EIP_CMD_LIST_IDENTITY 0x0063
47#define EIP_CMD_LIST_INTERFACES 0x0064
48#define EIP_CMD_REGISTER_SESSION 0x0065
49#define EIP_CMD_UNREGISTER_SESSION 0x0066
50#define EIP_CMD_SEND_RR_DATA 0x006F
51#define EIP_CMD_SEND_UNIT_DATA 0x0070
52
53#define EIP_STATUS_SUCCESS 0x00000000u
54
55// Common Packet Format item type ids.
56#define EIP_CPF_NULL 0x0000 ///< null address item
57#define EIP_CPF_CONNECTED_ADDRESS 0x00A1 ///< connected address item
58#define EIP_CPF_CONNECTED_DATA 0x00B1 ///< connected data item
59#define EIP_CPF_UNCONNECTED_DATA 0x00B2 ///< unconnected data item (carries the CIP message)
60#define EIP_CPF_LIST_IDENTITY 0x000C ///< List Identity response item (device identity)
61
62/** @brief The 24-octet encapsulation header. */
63typedef struct
64{
65 uint16_t command;
66 uint16_t length; ///< octets of command-specific data after the header
67 uint32_t session_handle;
68 uint32_t status;
69 uint8_t sender_context[8];
70 uint32_t options;
71} EipHeader;
72
73/** @brief The device identity decoded from a ListIdentity response item. @ref product_name points INTO the
74 * source buffer and is NOT NUL-terminated. The 16-octet CIP socket address is skipped (its sin_* fields are
75 * network-order, unlike the little-endian encapsulation, so this codec does not reinterpret them). */
76typedef struct
77{
78 uint16_t protocol_version; ///< encapsulation protocol version (1)
79 uint16_t vendor_id;
80 uint16_t device_type;
81 uint16_t product_code;
82 uint8_t revision_major;
83 uint8_t revision_minor;
84 uint16_t status;
85 uint32_t serial_number;
86 const char *product_name; ///< ASCII product name (into the buffer, not NUL-terminated)
87 uint8_t product_name_len;
88 uint8_t state; ///< device state
89} EipIdentity;
90
91/** @brief What build takes: buf, cap, h, data, data_len. */
92typedef struct
93{
94 uint8_t *buf;
95 size_t cap;
96 const EipHeader *h;
97 const uint8_t *data;
98 size_t data_len;
99} EnipBuildArgs;
100
101/** @brief What parse takes: buf, len, out, data, data_len. */
102typedef struct
103{
104 const uint8_t *buf;
105 size_t len;
106 EipHeader *out;
107 const uint8_t **data;
108 size_t *data_len;
109} EnipParseArgs;
110
111/** @brief What build_register_session takes: buf, cap, sender_context. */
112typedef struct
113{
114 uint8_t *buf;
115 size_t cap;
116 const uint8_t *sender_context; ///< 8 bytes.
117} EnipBuildRegisterSessionArgs;
118
119/** @brief What build_unregister_session takes: buf, cap, ... */
120typedef struct
121{
122 uint8_t *buf;
123 size_t cap;
124 uint32_t session_handle;
125 const uint8_t *sender_context; ///< 8 bytes.
126} EnipBuildUnregisterSessionArgs;
127
128/** @brief What build_send_rr_data takes: buf, cap, session_handle, ... */
129typedef struct
130{
131 uint8_t *buf;
132 size_t cap;
133 uint32_t session_handle;
134 const uint8_t *sender_context; ///< 8 bytes.
135 uint16_t timeout;
136 const uint8_t *cip;
137 size_t cip_len;
138} EnipBuildSendRrDataArgs;
139
140/** @brief What parse_send_rr_data takes: data, data_len, cip, cip_len. */
141typedef struct
142{
143 const uint8_t *data;
144 size_t data_len;
145 const uint8_t **cip;
146 size_t *cip_len;
147} EnipParseSendRrDataArgs;
148
149/** @brief What build_list_identity takes: buf, cap, sender_context. */
150typedef struct
151{
152 uint8_t *buf;
153 size_t cap;
154 const uint8_t *sender_context; ///< 8 bytes.
155} EnipBuildListIdentityArgs;
156
157/** @brief What parse_list_identity takes: data, data_len, out. */
158typedef struct
159{
160 const uint8_t *data;
161 size_t data_len;
162 EipIdentity *out;
163} EnipParseListIdentityArgs;
164
165/**
166 * @brief EtherNet/IP encapsulation codec (PROTOCORE_ENABLE_ENIP) - zero-heap builder + parser for the ODVA EtherNet/IP
167 * encapsulation layer (TCP/UDP 44818), the transport that carries CIP.
168 *
169 * A caller sets the members a call takes, invokes it through ::Enip with the bytes it runs
170 * out of, and reads the outcome off the same handle.
171 *
172 * Enip.build_args.buf = ...;
173 * Enip.build_args.cap = ...;
174 * Enip.build_args.h = ...;
175 * Enip.build_args.data = ...;
176 * Enip.build_args.data_len = ...;
177 * Enip.build(work);
178 * // Enip.n is what the call reports
179 *
180 * @var EnipNs::build_args what build takes: buf, cap, h, data, data_len
181 * @var EnipNs::parse_args what parse takes: buf, len, out, data, data_len
182 * @var EnipNs::build_register_session_args what build_register_session takes: buf, cap, sender_context
183 * @var EnipNs::build_unregister_session_args what build_unregister_session takes: buf, cap,
184 * @var EnipNs::build_send_rr_data_args what build_send_rr_data takes: buf, cap, session_handle,
185 * @var EnipNs::parse_send_rr_data_args what parse_send_rr_data takes: data, data_len, cip, cip_len
186 * @var EnipNs::build_list_identity_args what build_list_identity takes: buf, cap, sender_context
187 * @var EnipNs::parse_list_identity_args what parse_list_identity takes: data, data_len, out
188 * @var EnipNs::ok true iff a well-formed List Identity item is present and its ...
189 * @var EnipNs::n the count a call reports
190 * @var EnipNs::build build the encapsulation header + command data. Returns total ...
191 * @var EnipNs::parse parse the encapsulation header and slice the command data
192 * @var EnipNs::build_register_session build a RegisterSession request (protocol version 1). ...
193 * @var EnipNs::build_unregister_session build an UnRegisterSession request that closes session_handle (no ...
194 * @var EnipNs::build_send_rr_data build a SendRRData request wrapping cip as an unconnected message ...
195 * @var EnipNs::parse_send_rr_data from a SendRRData command-data block, extract the Unconnected Data ...
196 * @var EnipNs::build_list_identity build a ListIdentity request (command 0x0063, no command-specific ...
197 * @var EnipNs::parse_list_identity parse a ListIdentity response command-data block (the octets after ...
198 *
199 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
200 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
201 * a caller drives every namespace the same way.
202 */
203typedef struct
204{
205 EnipBuildArgs build_args;
206 EnipParseArgs parse_args;
207 EnipBuildRegisterSessionArgs build_register_session_args;
208 EnipBuildUnregisterSessionArgs build_unregister_session_args;
209 EnipBuildSendRrDataArgs build_send_rr_data_args;
210 EnipParseSendRrDataArgs parse_send_rr_data_args;
211 EnipBuildListIdentityArgs build_list_identity_args;
212 EnipParseListIdentityArgs parse_list_identity_args;
213 proto_bool ok;
214 size_t n;
215} EnipVars;
216
217/** @brief The operands and the outcome. */
218extern EnipVars EnipV;
219
220/** @brief The entries. */
221typedef struct
222{
223 void (*const build)(uint8_t *work);
224 void (*const parse)(uint8_t *work);
225 void (*const build_register_session)(uint8_t *work);
226 void (*const build_unregister_session)(uint8_t *work);
227 void (*const build_send_rr_data)(uint8_t *work);
228 void (*const parse_send_rr_data)(uint8_t *work);
229 void (*const build_list_identity)(uint8_t *work);
230 void (*const parse_list_identity)(uint8_t *work);
231} EnipNs;
232
233// What the table binds, defined once in the .c and taking one parameter each: everything
234// else an entry needs is an operand in EnipV or a region of the borrow at a fixed offset.
235void protocore_enip_build(uint8_t *work);
236void protocore_enip_parse(uint8_t *work);
237void protocore_enip_build_register_session(uint8_t *work);
238void protocore_enip_build_unregister_session(uint8_t *work);
239void protocore_enip_build_send_rr_data(uint8_t *work);
240void protocore_enip_parse_send_rr_data(uint8_t *work);
241void protocore_enip_build_list_identity(uint8_t *work);
242void protocore_enip_parse_list_identity(uint8_t *work);
243
244// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
245// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
246// `Enip.build(work)` resolves to a named function and becomes a DIRECT call. An extern table
247// leaves the call indirect and the symbol live at every level, -O2 -flto included.
248static const EnipNs Enip __attribute__((unused)) = {
249 .build = protocore_enip_build,
250 .parse = protocore_enip_parse,
251 .build_register_session = protocore_enip_build_register_session,
252 .build_unregister_session = protocore_enip_build_unregister_session,
253 .build_send_rr_data = protocore_enip_build_send_rr_data,
254 .parse_send_rr_data = protocore_enip_parse_send_rr_data,
255 .build_list_identity = protocore_enip_build_list_identity,
256 .parse_list_identity = protocore_enip_parse_list_identity,
257};
258
260
261#endif // PROTOCORE_ENABLE_ENIP
262
263#endif // PROTOCORE_ENIP_H
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97