DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ads.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 ads.h
6 * @brief Beckhoff ADS / AMS protocol codec (DETWS_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 (`det_client_*`) and the AMS route registration on the target router.
34 *
35 * @author Douglas Quigg (dstroy0)
36 * @date 2026
37 */
38
39#ifndef DETERMINISTICESPASYNCWEBSERVER_ADS_H
40#define DETERMINISTICESPASYNCWEBSERVER_ADS_H
41
42#include "ServerConfig.h"
43
44#if DETWS_ENABLE_ADS
45
46#include <stddef.h>
47#include <stdint.h>
48
49#define ADS_TCP_PORT 48898 ///< AMS/TCP listening port (0xBF02)
50#define ADS_AMSTCP_HDR_LEN 6 ///< reserved(2) + length(4)
51#define ADS_AMS_HDR_LEN 32 ///< target/source net id + port, cmd, flags, cbData, error, invoke
52#define ADS_HDR_LEN 38 ///< ADS_AMSTCP_HDR_LEN + ADS_AMS_HDR_LEN (payload starts here)
53#define ADS_NET_ID_LEN 6 ///< an AMSNetId is six octets
54#define ADS_DEVICE_NAME_LEN 16 ///< ReadDeviceInfo device-name field width
55
56/// ADS command ids (AMS header octets 16-17). Cast to/from the wire only at the byte boundary.
57enum class AdsCommand : uint16_t
58{
59 invalid = 0x0000,
60 read_device_info = 0x0001,
61 read = 0x0002,
62 write = 0x0003,
63 read_state = 0x0004,
64 write_control = 0x0005,
65 add_notification = 0x0006,
66 del_notification = 0x0007,
67 notification = 0x0008,
68 read_write = 0x0009,
69};
70
71/// AMS header state-flag bits (octets 18-19). A TCP request is `ads_command`; a response ORs in
72/// `response`. Grouped as constants (not an enum) because they are a bitmask.
73struct AdsStateFlags
74{
75 static constexpr uint16_t response = 0x0001; ///< set on a response, clear on a request
76 static constexpr uint16_t no_return = 0x0002; ///< no response expected
77 static constexpr uint16_t ads_command = 0x0004; ///< ADS command (set for TCP)
78 static constexpr uint16_t sys_command = 0x0008; ///< system command
79 static constexpr uint16_t high_prio = 0x0010; ///< high priority
80 static constexpr uint16_t timestamp = 0x0020; ///< a timestamp is appended
81 static constexpr uint16_t udp = 0x0040; ///< carried over UDP
82 static constexpr uint16_t init_command = 0x0080;
83 static constexpr uint16_t broadcast = 0x8000;
84
85 static constexpr uint16_t request = ads_command; ///< 0x0004
86 static constexpr uint16_t reply = ads_command | response; ///< 0x0005
87};
88
89/// ADS device state used by ReadState / WriteControl (a subset of ADSSTATE).
90enum class AdsState : uint16_t
91{
92 invalid = 0,
93 idle = 1,
94 reset = 2,
95 init = 3,
96 start = 4,
97 run = 5,
98 stop = 6,
99 save_config = 7,
100 load_config = 8,
101 power_failure = 9,
102 power_good = 10,
103 error = 11,
104 shutdown = 12,
105 suspend = 13,
106 resume = 14,
107 config = 15,
108 reconfig = 16,
109};
110
111/// AddDeviceNotification transmission modes (ADSTRANS).
112enum class AdsTransMode : uint32_t
113{
114 no_trans = 0,
115 client_cycle = 1,
116 client_on_change = 2,
117 server_cycle = 3, ///< server sends every CycleTime
118 server_on_change = 4, ///< server sends when the value changes
119};
120
121/// Well-known ADS index groups for symbol access (dedup of the magic constants).
122struct AdsIndexGroup
123{
124 static constexpr uint32_t sym_hnd_by_name = 0xF003; ///< ReadWrite name -> handle
125 static constexpr uint32_t sym_val_by_handle = 0xF005; ///< Read/Write value by handle
126 static constexpr uint32_t sym_release_handle = 0xF006; ///< Write to release a handle
127 static constexpr uint32_t sym_info_by_name_ex = 0xF009;
128 static constexpr uint32_t sym_upload = 0xF00B;
129 static constexpr uint32_t sym_upload_info = 0xF00F;
130 static constexpr uint32_t io_image_rw_ib = 0xF020; ///< %I input image, bit offset
131 static constexpr uint32_t io_image_rw_ob = 0xF030; ///< %Q output image, bit offset
132 static constexpr uint32_t plc_rw_m = 0x4020; ///< %M flag memory, byte offset
133 static constexpr uint32_t plc_rw_rb = 0x4030; ///< retain memory
134};
135
136/// A 6-octet AMSNetId + a 2-octet AMS port (one endpoint of the AMS route).
137struct AdsAmsAddr
138{
139 uint8_t net_id[ADS_NET_ID_LEN];
140 uint16_t port;
141};
142
143/// Target/source addressing + invoke id carried on every request from one client.
144struct AdsRequest
145{
146 AdsAmsAddr target;
147 AdsAmsAddr source;
148 uint32_t invoke_id;
149};
150
151/// A parsed AMS header; `data`/`data_len` point into the caller's buffer (no copy).
152struct AdsAmsHeader
153{
154 AdsAmsAddr target;
155 AdsAmsAddr source;
156 AdsCommand cmd;
157 uint16_t state_flags;
158 uint32_t data_len; ///< cbData
159 uint32_t error_code; ///< AMS error (0 = success)
160 uint32_t invoke_id;
161 const uint8_t *data; ///< -> payload (into the caller's buffer)
162};
163
164/// Parsed Read / ReadWrite response payload (Result + Length + Data).
165struct AdsReadResult
166{
167 uint32_t result; ///< ADS error code (0 = success)
168 const uint8_t *data;
169 uint32_t len;
170};
171
172/// Parsed ReadState response payload.
173struct AdsReadStateResult
174{
175 uint32_t result;
176 uint16_t ads_state;
177 uint16_t device_state;
178};
179
180/// Parsed ReadDeviceInfo response payload.
181struct AdsDeviceInfo
182{
183 uint32_t result;
184 uint8_t version_major;
185 uint8_t version_minor;
186 uint16_t version_build;
187 char device_name[ADS_DEVICE_NAME_LEN + 1]; ///< NUL-terminated copy of the 16-octet field
188};
189
190// ---------------------------------------------------------------------------------------------
191// Request builders. Each writes a complete on-wire frame (AMS/TCP + AMS header + payload) into
192// `buf` and returns the total octet count, or 0 if `buf`/`r` is null or `cap` is too small.
193// ---------------------------------------------------------------------------------------------
194
195/// ReadDeviceInfo (cmd 1): no payload. Response = AdsDeviceInfo.
196size_t ads_build_read_device_info(uint8_t *buf, size_t cap, const AdsRequest *r);
197
198/// ReadState (cmd 4): no payload. Response = AdsReadStateResult.
199size_t ads_build_read_state(uint8_t *buf, size_t cap, const AdsRequest *r);
200
201/// Read (cmd 2): IndexGroup + IndexOffset + Length. Response = AdsReadResult.
202size_t ads_build_read(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group, uint32_t index_offset,
203 uint32_t read_len);
204
205/// Write (cmd 3): IndexGroup + IndexOffset + Length + Data. Response = a single result u32.
206size_t ads_build_write(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group, uint32_t index_offset,
207 const uint8_t *data, uint32_t len);
208
209/// ReadWrite (cmd 9): IndexGroup + IndexOffset + ReadLen + WriteLen + WriteData. The workhorse
210/// for symbol-by-name (write the name to `sym_hnd_by_name`, read back the 4-octet handle).
211/// Response = AdsReadResult.
212size_t ads_build_read_write(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group, uint32_t index_offset,
213 uint32_t read_len, const uint8_t *write_data, uint32_t write_len);
214
215/// WriteControl (cmd 5): AdsState + DeviceState + Length + Data. Response = a single result u32.
216size_t ads_build_write_control(uint8_t *buf, size_t cap, const AdsRequest *r, uint16_t ads_state, uint16_t device_state,
217 const uint8_t *data, uint32_t len);
218
219/// AddDeviceNotification (cmd 6): subscribe to a symbol. Response = result u32 + handle u32
220/// (parse with ads_parse_add_notification). max_delay / cycle_time are in 100 ns units.
221size_t ads_build_add_notification(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group,
222 uint32_t index_offset, uint32_t length, AdsTransMode mode, uint32_t max_delay,
223 uint32_t cycle_time);
224
225/// DeleteDeviceNotification (cmd 7): NotificationHandle. Response = a single result u32.
226size_t ads_build_del_notification(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t notification_handle);
227
228// ---------------------------------------------------------------------------------------------
229// Response parsers. `ads_parse_ams_header` validates the framing and exposes the payload; the
230// per-command parsers then decode that payload. Each returns false on a short/garbled buffer.
231// ---------------------------------------------------------------------------------------------
232
233/// Validate the AMS/TCP + AMS framing and fill `out` (its `data` points into `buf`).
234bool ads_parse_ams_header(const uint8_t *buf, size_t len, AdsAmsHeader *out);
235
236/// Read / ReadWrite response payload: Result(4) + Length(4) + Data(Length).
237bool ads_parse_read(const uint8_t *data, size_t data_len, AdsReadResult *out);
238
239/// Write / WriteControl / DeleteNotification response payload: a single Result(4).
240bool ads_parse_result(const uint8_t *data, size_t data_len, uint32_t *result);
241
242/// ReadState response payload: Result(4) + AdsState(2) + DeviceState(2).
243bool ads_parse_read_state(const uint8_t *data, size_t data_len, AdsReadStateResult *out);
244
245/// ReadDeviceInfo response payload: Result(4) + Major(1) + Minor(1) + Build(2) + Name(16).
246bool ads_parse_read_device_info(const uint8_t *data, size_t data_len, AdsDeviceInfo *out);
247
248/// AddDeviceNotification response payload: Result(4) + NotificationHandle(4).
249bool ads_parse_add_notification(const uint8_t *data, size_t data_len, uint32_t *result, uint32_t *handle);
250
251/// Callback invoked once per sample while walking a DeviceNotification (cmd 8) payload.
252/// `timestamp` is the raw Windows FILETIME (100 ns ticks since 1601-01-01 UTC).
253using AdsNotificationSampleFn = void (*)(uint32_t notification_handle, const uint8_t *sample, uint32_t sample_len,
254 uint64_t timestamp, void *user);
255
256/// Walk a DeviceNotification payload (Length + Stamps, each stamp = Timestamp + Samples + the
257/// per-sample handle/size/data), calling `on_sample` for every sample. Returns false if the
258/// buffer is truncated or internally inconsistent.
259bool ads_parse_notification(const uint8_t *data, size_t data_len, AdsNotificationSampleFn on_sample, void *user);
260
261#endif // DETWS_ENABLE_ADS
262
263#endif // DETERMINISTICESPASYNCWEBSERVER_ADS_H
User-facing configuration for DeterministicESPAsyncWebServer.