ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ads.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 ads.h
6 * @brief Beckhoff ADS / AMS protocol codec (PROTOCORE_ENABLE_ADS) - zero-heap request builders +
7 * response parsers for TwinCAT PLCs over TCP 48898 (the PC-based-control protocol).
8 *
9 * ADS (Automation Device Specification) rides on AMS (Automation Message Specification). Every
10 * multi-octet field is LITTLE-endian. A frame is an AMS/TCP header (6 octets) + an AMS header
11 * (32 octets) + the command payload:
12 * @code
13 * AMS/TCP header (6)
14 * 00 00 reserved
15 * LL LL LL LL length of everything after this field (AMS header + payload)
16 * AMS header (32)
17 * target net id (6) e.g. 5.18....1.1 (AMSNetId, six octets in order)
18 * target port (2) e.g. 851 = the first TwinCAT 3 PLC runtime
19 * source net id (6)
20 * source port (2)
21 * cmd id (2) 1 ReadDeviceInfo 2 Read 3 Write 4 ReadState 5 WriteControl
22 * 6 AddNotification 7 DeleteNotification 8 Notification 9 ReadWrite
23 * state flags (2) 0x0004 request, 0x0005 response (bit0 = response, bit2 = ADS command)
24 * data length (4) cbData - octets of payload that follow the AMS header
25 * error code (4) AMS error (0 = success)
26 * invoke id (4) caller-chosen, echoed in the response to correlate it
27 * payload (cbData) command-specific (see the per-command builders/parsers below)
28 * @endcode
29 *
30 * AMS header field order (target-before-source), command ids, and state flags verified against
31 * the Beckhoff InfoSys AMS/ADS specification; payload layouts cross-checked with Beckhoff's own
32 * open-source ADS library, `pyads`, and Apache PLC4X. Pure codec, host-tested - the caller owns
33 * the TCP connection (`protocore_client_*`) and the AMS route registration on the target router.
34 *
35 * @author Douglas Quigg (dstroy0)
36 * @date 2026
37 */
38
39#ifndef PROTOCORE_ADS_H
40#define PROTOCORE_ADS_H
41
42#include "protocore_config.h" // the entry point: protocore_types.h for the widths
43
44#if PROTOCORE_ENABLE_ADS
45
47
48// This module holds nothing between calls, so it carves no borrow and states none. An entry
49// takes one all the same, and never reads it, so every namespace in the tree is invoked the
50// same way.
51
52#define ADS_TCP_PORT 48898 ///< AMS/TCP listening port (0xBF02)
53#define ADS_AMSTCP_HDR_LEN 6 ///< reserved(2) + length(4)
54#define ADS_AMS_HDR_LEN 32 ///< target/source net id + port, cmd, flags, cbData, error, invoke
55#define ADS_HDR_LEN 38 ///< ADS_AMSTCP_HDR_LEN + ADS_AMS_HDR_LEN (payload starts here)
56#define ADS_NET_ID_LEN 6 ///< an AMSNetId is six octets
57#define ADS_DEVICE_NAME_LEN 16 ///< ReadDeviceInfo device-name field width
58
59/// AMS header state-flag bits (octets 18-19). A TCP request is ADS_STATE_ADS_COMMAND; a response
60/// ORs in ADS_STATE_RESPONSE.
61#define ADS_STATE_RESPONSE 0x0001 ///< set on a response, clear on a request
62#define ADS_STATE_NO_RETURN 0x0002 ///< no response expected
63#define ADS_STATE_ADS_COMMAND 0x0004 ///< ADS command (set for TCP)
64#define ADS_STATE_SYS_COMMAND 0x0008 ///< system command
65#define ADS_STATE_HIGH_PRIO 0x0010 ///< high priority
66#define ADS_STATE_TIMESTAMP 0x0020 ///< a timestamp is appended
67#define ADS_STATE_UDP 0x0040 ///< carried over UDP
68#define ADS_STATE_INIT_COMMAND 0x0080
69#define ADS_STATE_BROADCAST 0x8000
70#define ADS_STATE_REQUEST ADS_STATE_ADS_COMMAND ///< 0x0004
71#define ADS_STATE_REPLY (ADS_STATE_ADS_COMMAND | ADS_STATE_RESPONSE) ///< 0x0005
72
73/// Well-known ADS index groups for symbol access (dedup of the magic constants).
74#define ADS_IGRP_SYM_HND_BY_NAME 0xF003 ///< ReadWrite name -> handle
75#define ADS_IGRP_SYM_VAL_BY_HANDLE 0xF005 ///< Read/Write value by handle
76#define ADS_IGRP_SYM_RELEASE_HANDLE 0xF006 ///< Write to release a handle
77#define ADS_IGRP_SYM_INFO_BY_NAME_EX 0xF009
78#define ADS_IGRP_SYM_UPLOAD 0xF00B
79#define ADS_IGRP_SYM_UPLOAD_INFO 0xF00F
80#define ADS_IGRP_IO_IMAGE_RW_IB 0xF020 ///< %I input image, bit offset
81#define ADS_IGRP_IO_IMAGE_RW_OB 0xF030 ///< %Q output image, bit offset
82#define ADS_IGRP_PLC_RW_M 0x4020 ///< %M flag memory, byte offset
83#define ADS_IGRP_PLC_RW_RB 0x4030 ///< retain memory
84
85typedef enum PROTO_ENUM_PACKED
86{
87 ADS_COMMAND_INVALID = 0x0000,
88 ADS_COMMAND_READ_DEVICE_INFO = 0x0001,
89 ADS_COMMAND_READ = 0x0002,
90 ADS_COMMAND_WRITE = 0x0003,
91 ADS_COMMAND_READ_STATE = 0x0004,
92 ADS_COMMAND_WRITE_CONTROL = 0x0005,
93 ADS_COMMAND_ADD_NOTIFICATION = 0x0006,
94 ADS_COMMAND_DEL_NOTIFICATION = 0x0007,
95 ADS_COMMAND_NOTIFICATION = 0x0008,
96 ADS_COMMAND_READ_WRITE = 0x0009,
97} AdsCommand;
98
99typedef enum PROTO_ENUM_PACKED
100{
101 ADS_STATE_INVALID = 0,
102 ADS_STATE_IDLE = 1,
103 ADS_STATE_RESET = 2,
104 ADS_STATE_INIT = 3,
105 ADS_STATE_START = 4,
106 ADS_STATE_RUN = 5,
107 ADS_STATE_STOP = 6,
108 ADS_STATE_SAVE_CONFIG = 7,
109 ADS_STATE_LOAD_CONFIG = 8,
110 ADS_STATE_POWER_FAILURE = 9,
111 ADS_STATE_POWER_GOOD = 10,
112 ADS_STATE_ERROR = 11,
113 ADS_STATE_SHUTDOWN = 12,
114 ADS_STATE_SUSPEND = 13,
115 ADS_STATE_RESUME = 14,
116 ADS_STATE_CONFIG = 15,
117 ADS_STATE_RECONFIG = 16,
118} AdsState;
119
120typedef enum PROTO_ENUM_PACKED
121{
122 ADS_TRANS_MODE_NO_TRANS = 0,
123 ADS_TRANS_MODE_CLIENT_CYCLE = 1,
124 ADS_TRANS_MODE_CLIENT_ON_CHANGE = 2,
125 ADS_TRANS_MODE_SERVER_CYCLE = 3, ///< server sends every CycleTime
126 ADS_TRANS_MODE_SERVER_ON_CHANGE = 4, ///< server sends when the value changes
127} AdsTransMode;
128
129typedef struct
130{
131 uint8_t net_id[ADS_NET_ID_LEN];
132 uint16_t port;
133} AdsAmsAddr;
134
135typedef struct
136{
137 AdsAmsAddr target;
138 AdsAmsAddr source;
139 uint32_t invoke_id;
140} AdsRequest;
141
142typedef struct
143{
144 AdsAmsAddr target;
145 AdsAmsAddr source;
146 AdsCommand cmd;
147 uint16_t state_flags;
148 uint32_t data_len; ///< cbData
149 uint32_t error_code; ///< AMS error (0 = success)
150 uint32_t invoke_id;
151 const uint8_t *data; ///< -> payload (into the caller's buffer)
152} AdsAmsHeader;
153
154typedef struct
155{
156 uint32_t result; ///< ADS error code (0 = success)
157 const uint8_t *data;
158 uint32_t len;
159} AdsReadResult;
160
161typedef struct
162{
163 uint32_t result;
164 uint16_t protocore_ads_state;
165 uint16_t device_state;
166} AdsReadStateResult;
167
168typedef struct
169{
170 uint32_t result;
171 uint8_t version_major;
172 uint8_t version_minor;
173 uint16_t version_build;
174 char device_name[ADS_DEVICE_NAME_LEN + 1]; ///< NUL-terminated copy of the 16-octet field
175} AdsDeviceInfo;
176
177typedef void (*AdsNotificationSampleFn)(uint32_t notification_handle, const uint8_t *sample, uint32_t sample_len,
178 uint64_t timestamp, void *user);
179
180/** @brief What build_read_device_info takes: buf, cap, r. */
181typedef struct
182{
183 uint8_t *buf;
184 size_t cap;
185 const AdsRequest *r;
186} AdsBuildReadDeviceInfoArgs;
187
188/** @brief What build_read_state takes: buf, cap, r. */
189typedef struct
190{
191 uint8_t *buf;
192 size_t cap;
193 const AdsRequest *r;
194} AdsBuildReadStateArgs;
195
196/** @brief What build_read takes: buf, cap, r, index_group, ... */
197typedef struct
198{
199 uint8_t *buf;
200 size_t cap;
201 const AdsRequest *r;
202 uint32_t index_group;
203 uint32_t index_offset;
204 uint32_t read_len;
205} AdsBuildReadArgs;
206
207/** @brief What build_write takes: buf, cap, r, index_group, ... */
208typedef struct
209{
210 uint8_t *buf;
211 size_t cap;
212 const AdsRequest *r;
213 uint32_t index_group;
214 uint32_t index_offset;
215 const uint8_t *data;
216 uint32_t len;
217} AdsBuildWriteArgs;
218
219/** @brief What build_read_write takes: buf, cap, r, index_group, ... */
220typedef struct
221{
222 uint8_t *buf;
223 size_t cap;
224 const AdsRequest *r;
225 uint32_t index_group;
226 uint32_t index_offset;
227 uint32_t read_len;
228 const uint8_t *write_data;
229 uint32_t write_len;
230} AdsBuildReadWriteArgs;
231
232/** @brief What build_write_control takes: buf, cap, r, ... */
233typedef struct
234{
235 uint8_t *buf;
236 size_t cap;
237 const AdsRequest *r;
238 uint16_t protocore_ads_state;
239 uint16_t device_state;
240 const uint8_t *data;
241 uint32_t len;
242} AdsBuildWriteControlArgs;
243
244/** @brief What build_add_notification takes: buf, cap, r, ... */
245typedef struct
246{
247 uint8_t *buf;
248 size_t cap;
249 const AdsRequest *r;
250 uint32_t index_group;
251 uint32_t index_offset;
252 uint32_t length;
253 AdsTransMode mode;
254 uint32_t max_delay;
255 uint32_t cycle_time;
256} AdsBuildAddNotificationArgs;
257
258/** @brief What build_del_notification takes: buf, cap, r, ... */
259typedef struct
260{
261 uint8_t *buf;
262 size_t cap;
263 const AdsRequest *r;
264 uint32_t notification_handle;
265} AdsBuildDelNotificationArgs;
266
267/** @brief What parse_ams_header takes: buf, len, out. */
268typedef struct
269{
270 const uint8_t *buf;
271 size_t len;
272 AdsAmsHeader *out;
273} AdsParseAmsHeaderArgs;
274
275/** @brief What parse_read takes: data, data_len, out. */
276typedef struct
277{
278 const uint8_t *data;
279 size_t data_len;
280 AdsReadResult *out;
281} AdsParseReadArgs;
282
283/** @brief What parse_result takes: data, data_len, result. */
284typedef struct
285{
286 const uint8_t *data;
287 size_t data_len;
288 uint32_t *result;
289} AdsParseResultArgs;
290
291/** @brief What parse_read_state takes: data, data_len, out. */
292typedef struct
293{
294 const uint8_t *data;
295 size_t data_len;
296 AdsReadStateResult *out;
297} AdsParseReadStateArgs;
298
299/** @brief What parse_read_device_info takes: data, data_len, out. */
300typedef struct
301{
302 const uint8_t *data;
303 size_t data_len;
304 AdsDeviceInfo *out;
305} AdsParseReadDeviceInfoArgs;
306
307/** @brief What parse_add_notification takes: data, data_len, result, ... */
308typedef struct
309{
310 const uint8_t *data;
311 size_t data_len;
312 uint32_t *result;
313 uint32_t *handle;
314} AdsParseAddNotificationArgs;
315
316/** @brief What parse_notification takes: data, data_len, on_sample, ... */
317typedef struct
318{
319 const uint8_t *data;
320 size_t data_len;
321 AdsNotificationSampleFn on_sample;
322 void *user;
323} AdsParseNotificationArgs;
324
325/**
326 * @brief Beckhoff ADS / AMS protocol codec (PROTOCORE_ENABLE_ADS) - zero-heap request builders + response parsers for
327 * TwinCAT PLCs over TCP 48898 (the PC-based-control protocol).
328 *
329 * A caller sets the members a call takes, invokes it through ::Ads with the bytes it runs
330 * out of, and reads the outcome off the same handle.
331 *
332 * Ads.build_read_device_info_args.buf = ...;
333 * Ads.build_read_device_info_args.cap = ...;
334 * Ads.build_read_device_info_args.r = ...;
335 * Ads.build_read_device_info(work);
336 * // Ads.n is what the call reports
337 *
338 * @var AdsNs::build_read_device_info_args what build_read_device_info takes: buf, cap, r
339 * @var AdsNs::build_read_state_args what build_read_state takes: buf, cap, r
340 * @var AdsNs::build_read_args what build_read takes: buf, cap, r, index_group,
341 * @var AdsNs::build_write_args what build_write takes: buf, cap, r, index_group,
342 * @var AdsNs::build_read_write_args what build_read_write takes: buf, cap, r, index_group,
343 * @var AdsNs::build_write_control_args what build_write_control takes: buf, cap, r,
344 * @var AdsNs::build_add_notification_args what build_add_notification takes: buf, cap, r,
345 * @var AdsNs::build_del_notification_args what build_del_notification takes: buf, cap, r,
346 * @var AdsNs::parse_ams_header_args what parse_ams_header takes: buf, len, out
347 * @var AdsNs::parse_read_args what parse_read takes: data, data_len, out
348 * @var AdsNs::parse_result_args what parse_result takes: data, data_len, result
349 * @var AdsNs::parse_read_state_args what parse_read_state takes: data, data_len, out
350 * @var AdsNs::parse_read_device_info_args what parse_read_device_info takes: data, data_len, out
351 * @var AdsNs::parse_add_notification_args what parse_add_notification takes: data, data_len, result,
352 * @var AdsNs::parse_notification_args what parse_notification takes: data, data_len, on_sample,
353 * @var AdsNs::ok a call's true/false outcome
354 * @var AdsNs::n the count a call reports
355 * @var AdsNs::build_read_device_info build_read_device_info
356 * @var AdsNs::build_read_state build_read_state
357 * @var AdsNs::build_read build_read
358 * @var AdsNs::build_write build_write
359 * @var AdsNs::build_read_write build_read_write
360 * @var AdsNs::build_write_control build_write_control
361 * @var AdsNs::build_add_notification build_add_notification
362 * @var AdsNs::build_del_notification build_del_notification
363 * @var AdsNs::parse_ams_header parse_ams_header
364 * @var AdsNs::parse_read parse_read
365 * @var AdsNs::parse_result parse_result
366 * @var AdsNs::parse_read_state parse_read_state
367 * @var AdsNs::parse_read_device_info parse_read_device_info
368 * @var AdsNs::parse_add_notification parse_add_notification
369 * @var AdsNs::parse_notification parse_notification
370 *
371 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
372 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
373 * a caller drives every namespace the same way.
374 */
375typedef struct
376{
377 AdsBuildReadDeviceInfoArgs build_read_device_info_args;
378 AdsBuildReadStateArgs build_read_state_args;
379 AdsBuildReadArgs build_read_args;
380 AdsBuildWriteArgs build_write_args;
381 AdsBuildReadWriteArgs build_read_write_args;
382 AdsBuildWriteControlArgs build_write_control_args;
383 AdsBuildAddNotificationArgs build_add_notification_args;
384 AdsBuildDelNotificationArgs build_del_notification_args;
385 AdsParseAmsHeaderArgs parse_ams_header_args;
386 AdsParseReadArgs parse_read_args;
387 AdsParseResultArgs parse_result_args;
388 AdsParseReadStateArgs parse_read_state_args;
389 AdsParseReadDeviceInfoArgs parse_read_device_info_args;
390 AdsParseAddNotificationArgs parse_add_notification_args;
391 AdsParseNotificationArgs parse_notification_args;
392 proto_bool ok;
393 size_t n;
394} AdsVars;
395
396/** @brief The operands and the outcome. */
397extern AdsVars AdsV;
398
399/** @brief The entries. */
400typedef struct
401{
402 void (*const build_read_device_info)(uint8_t *work);
403 void (*const build_read_state)(uint8_t *work);
404 void (*const build_read)(uint8_t *work);
405 void (*const build_write)(uint8_t *work);
406 void (*const build_read_write)(uint8_t *work);
407 void (*const build_write_control)(uint8_t *work);
408 void (*const build_add_notification)(uint8_t *work);
409 void (*const build_del_notification)(uint8_t *work);
410 void (*const parse_ams_header)(uint8_t *work);
411 void (*const parse_read)(uint8_t *work);
412 void (*const parse_result)(uint8_t *work);
413 void (*const parse_read_state)(uint8_t *work);
414 void (*const parse_read_device_info)(uint8_t *work);
415 void (*const parse_add_notification)(uint8_t *work);
416 void (*const parse_notification)(uint8_t *work);
417} AdsNs;
418
419// What the table binds, defined once in the .c and taking one parameter each: everything
420// else an entry needs is an operand in AdsV or a region of the borrow at a fixed offset.
421void protocore_ads_build_read_device_info(uint8_t *work);
422void protocore_ads_build_read_state(uint8_t *work);
423void protocore_ads_build_read(uint8_t *work);
424void protocore_ads_build_write(uint8_t *work);
425void protocore_ads_build_read_write(uint8_t *work);
426void protocore_ads_build_write_control(uint8_t *work);
427void protocore_ads_build_add_notification(uint8_t *work);
428void protocore_ads_build_del_notification(uint8_t *work);
429void protocore_ads_parse_ams_header(uint8_t *work);
430void protocore_ads_parse_read(uint8_t *work);
431void protocore_ads_parse_result(uint8_t *work);
432void protocore_ads_parse_read_state(uint8_t *work);
433void protocore_ads_parse_read_device_info(uint8_t *work);
434void protocore_ads_parse_add_notification(uint8_t *work);
435void protocore_ads_parse_notification(uint8_t *work);
436
437// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
438// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
439// `Ads.build_read_device_info(work)` resolves to a named function and becomes a DIRECT call. An extern table
440// leaves the call indirect and the symbol live at every level, -O2 -flto included.
441static const AdsNs Ads __attribute__((unused)) = {
442 .build_read_device_info = protocore_ads_build_read_device_info,
443 .build_read_state = protocore_ads_build_read_state,
444 .build_read = protocore_ads_build_read,
445 .build_write = protocore_ads_build_write,
446 .build_read_write = protocore_ads_build_read_write,
447 .build_write_control = protocore_ads_build_write_control,
448 .build_add_notification = protocore_ads_build_add_notification,
449 .build_del_notification = protocore_ads_build_del_notification,
450 .parse_ams_header = protocore_ads_parse_ams_header,
451 .parse_read = protocore_ads_parse_read,
452 .parse_result = protocore_ads_parse_result,
453 .parse_read_state = protocore_ads_parse_read_state,
454 .parse_read_device_info = protocore_ads_parse_read_device_info,
455 .parse_add_notification = protocore_ads_parse_add_notification,
456 .parse_notification = protocore_ads_parse_notification,
457};
458
460
461#endif // PROTOCORE_ENABLE_ADS
462
463#endif // PROTOCORE_ADS_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#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