ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
cip.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 cip.h
6 * @brief CIP (Common Industrial Protocol) message codec (PROTOCORE_ENABLE_CIP) - zero-heap
7 * request builder + response parser for the message that rides inside an EtherNet/IP
8 * Unconnected Data item (services/fieldbus/enip). Together they form a working CIP read path.
9 *
10 * A CIP message request is:
11 * @code
12 * Service(1) RequestPathSize(1, in 16-bit words) RequestPath(EPATH) ServiceData
13 * @endcode
14 * The EPATH addresses an object with logical segments. A logical segment byte is
15 * `0x20 | logical-type | format`, where logical-type is class (0x00), instance (0x04), or
16 * attribute (0x10), and format is 8-bit (0x00, then a 1-octet id) or 16-bit (0x01, then a
17 * pad octet and a little-endian 2-octet id). A response is `Service|0x80 reserved(0)
18 * GeneralStatus(1) AdditionalStatusSize(1, words) [additional status] ServiceData`.
19 *
20 * Service codes + the logical-segment encoding are verified against the Wireshark CIP
21 * dissector. This codec is the CIP message; wrap it with `Enip.build_send_rr_data`.
22 *
23 * @author Douglas Quigg (dstroy0)
24 * @date 2026
25 */
26
27#ifndef PROTOCORE_CIP_H
28#define PROTOCORE_CIP_H
29
30#include "protocore_config.h" // the entry point: protocore_types.h for the widths
31
32#if PROTOCORE_ENABLE_CIP
33
35
36// This module holds nothing between calls, so it carves no borrow and states none. An entry
37// takes one all the same, and never reads it, so every namespace in the tree is invoked the
38// same way.
39
40// Common service codes.
41#define CIP_SC_GET_ATTR_ALL 0x01
42#define CIP_SC_GET_ATTR_LIST 0x03
43#define CIP_SC_SET_ATTR_LIST 0x04
44#define CIP_SC_GET_ATTR_SINGLE 0x0E
45#define CIP_SC_SET_ATTR_SINGLE 0x10
46#define CIP_REPLY_FLAG 0x80 ///< OR'd into the service code in a reply
47
48// Logical-segment EPATH encoding (segment byte = base | logical-type | format).
49#define CIP_SEG_LOGICAL 0x20 ///< logical segment, segment-type bits
50#define CIP_SEG_CLASS 0x00 ///< logical type: class id
51#define CIP_SEG_INSTANCE 0x04 ///< logical type: instance id
52#define CIP_SEG_ATTRIBUTE 0x10 ///< logical type: attribute id
53#define CIP_SEG_8BIT 0x00 ///< format: an 8-bit id follows
54#define CIP_SEG_16BIT 0x01 ///< format: a pad octet then a 16-bit (LE) id follows
55
56#define CIP_STATUS_SUCCESS 0x00 ///< General Status: success
57
58/** @brief A parsed CIP response. @ref data points INTO the source buffer. */
59typedef struct
60{
61 uint8_t service; ///< reply service (the 0x80 reply bit is set)
62 uint8_t general_status; ///< CIP_STATUS_SUCCESS on success
63 const uint8_t *data; ///< service data (the attribute value on a read)
64 size_t data_len;
65} CipResponse;
66
67/** @brief What build_epath takes: buf, cap, class_id, instance_id, ... */
68typedef struct
69{
70 uint8_t *buf;
71 size_t cap;
72 uint16_t class_id;
73 uint16_t instance_id;
74 uint16_t attribute_id;
75 proto_bool with_attribute; ///< include the attribute segment
76} CipBuildEpathArgs;
77
78/** @brief What build_request takes: buf, cap, service, epath, ... */
79typedef struct
80{
81 uint8_t *buf;
82 size_t cap;
83 uint8_t service;
84 const uint8_t *epath;
85 size_t epath_len;
86 const uint8_t *data;
87 size_t data_len;
88} CipBuildRequestArgs;
89
90/** @brief What build_get_attr_single takes: buf, cap, class_id, ... */
91typedef struct
92{
93 uint8_t *buf;
94 size_t cap;
95 uint16_t class_id;
96 uint16_t instance_id;
97 uint16_t attribute_id;
98} CipBuildGetAttrSingleArgs;
99
100/** @brief What build_get_attr_all takes: buf, cap, class_id, ... */
101typedef struct
102{
103 uint8_t *buf;
104 size_t cap;
105 uint16_t class_id;
106 uint16_t instance_id;
107} CipBuildGetAttrAllArgs;
108
109/** @brief What build_set_attr_single takes: buf, cap, class_id, ... */
110typedef struct
111{
112 uint8_t *buf;
113 size_t cap;
114 uint16_t class_id;
115 uint16_t instance_id;
116 uint16_t attribute_id;
117 const uint8_t *value;
118 size_t value_len;
119} CipBuildSetAttrSingleArgs;
120
121/** @brief What parse_response takes: buf, len, out. */
122typedef struct
123{
124 const uint8_t *buf;
125 size_t len;
126 CipResponse *out;
127} CipParseResponseArgs;
128
129/**
130 * @brief CIP (Common Industrial Protocol) message codec (PROTOCORE_ENABLE_CIP) - zero-heap request builder + response
131 * parser for the message that rides inside an EtherNet/IP Unconnected Data item (services/fieldbus/enip).
132 *
133 * A caller sets the members a call takes, invokes it through ::Cip with the bytes it runs
134 * out of, and reads the outcome off the same handle.
135 *
136 * Cip.build_epath_args.buf = ...;
137 * Cip.build_epath_args.cap = ...;
138 * Cip.build_epath_args.class_id = ...;
139 * Cip.build_epath_args.instance_id = ...;
140 * Cip.build_epath_args.attribute_id = ...;
141 * Cip.build_epath_args.with_attribute = ...;
142 * Cip.build_epath(work);
143 * // Cip.n is what the call reports
144 *
145 * @var CipNs::build_epath_args what build_epath takes: buf, cap, class_id, instance_id,
146 * @var CipNs::build_request_args what build_request takes: buf, cap, service, epath,
147 * @var CipNs::build_get_attr_single_args what build_get_attr_single takes: buf, cap, class_id,
148 * @var CipNs::build_get_attr_all_args what build_get_attr_all takes: buf, cap, class_id,
149 * @var CipNs::build_set_attr_single_args what build_set_attr_single takes: buf, cap, class_id,
150 * @var CipNs::parse_response_args what parse_response takes: buf, len, out
151 * @var CipNs::ok a call's true/false outcome
152 * @var CipNs::n EPATH length in octets (always even / word-aligned), or 0 on ...
153 * @var CipNs::build_epath build a class/instance[/attribute] EPATH (logical segments) into buf
154 * @var CipNs::build_request build a CIP request: service + path size (words) + EPATH + service ...
155 * @var CipNs::build_get_attr_single build a Get_Attribute_Single request for class/instance/attribute
156 * @var CipNs::build_get_attr_all build a Get_Attributes_All request for class/instance: service 0x01 ...
157 * @var CipNs::build_set_attr_single build a Set_Attribute_Single request for class/instance/attribute ...
158 * @var CipNs::parse_response parse a CIP response (service + status + additional status + data)
159 *
160 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
161 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
162 * a caller drives every namespace the same way.
163 */
164typedef struct
165{
166 CipBuildEpathArgs build_epath_args;
167 CipBuildRequestArgs build_request_args;
168 CipBuildGetAttrSingleArgs build_get_attr_single_args;
169 CipBuildGetAttrAllArgs build_get_attr_all_args;
170 CipBuildSetAttrSingleArgs build_set_attr_single_args;
171 CipParseResponseArgs parse_response_args;
172 proto_bool ok;
173 size_t n;
174} CipVars;
175
176/** @brief The operands and the outcome. */
177extern CipVars CipV;
178
179/** @brief The entries. */
180typedef struct
181{
182 void (*const build_epath)(uint8_t *work);
183 void (*const build_request)(uint8_t *work);
184 void (*const build_get_attr_single)(uint8_t *work);
185 void (*const build_get_attr_all)(uint8_t *work);
186 void (*const build_set_attr_single)(uint8_t *work);
187 void (*const parse_response)(uint8_t *work);
188} CipNs;
189
190// What the table binds, defined once in the .c and taking one parameter each: everything
191// else an entry needs is an operand in CipV or a region of the borrow at a fixed offset.
192void protocore_cip_build_epath(uint8_t *work);
193void protocore_cip_build_request(uint8_t *work);
194void protocore_cip_build_get_attr_single(uint8_t *work);
195void protocore_cip_build_get_attr_all(uint8_t *work);
196void protocore_cip_build_set_attr_single(uint8_t *work);
197void protocore_cip_parse_response(uint8_t *work);
198
199// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
200// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
201// `Cip.build_epath(work)` resolves to a named function and becomes a DIRECT call. An extern table
202// leaves the call indirect and the symbol live at every level, -O2 -flto included.
203static const CipNs Cip __attribute__((unused)) = {
204 .build_epath = protocore_cip_build_epath,
205 .build_request = protocore_cip_build_request,
206 .build_get_attr_single = protocore_cip_build_get_attr_single,
207 .build_get_attr_all = protocore_cip_build_get_attr_all,
208 .build_set_attr_single = protocore_cip_build_set_attr_single,
209 .parse_response = protocore_cip_parse_response,
210};
211
213
214#endif // PROTOCORE_ENABLE_CIP
215
216#endif // PROTOCORE_CIP_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