DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
ads.cpp
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.cpp
6 * @brief Beckhoff ADS / AMS builder + parser (pure, host-tested). All fields little-endian.
7 */
8
9#include "services/ads/ads.h"
10
11#if DETWS_ENABLE_ADS
12
13#include <string.h>
14
15// AMS is little-endian throughout.
16static size_t put16le(uint8_t *p, uint16_t v)
17{
18 p[0] = (uint8_t)(v & 0xFF);
19 p[1] = (uint8_t)(v >> 8);
20 return 2;
21}
22
23static size_t put32le(uint8_t *p, uint32_t v)
24{
25 p[0] = (uint8_t)(v & 0xFF);
26 p[1] = (uint8_t)((v >> 8) & 0xFF);
27 p[2] = (uint8_t)((v >> 16) & 0xFF);
28 p[3] = (uint8_t)((v >> 24) & 0xFF);
29 return 4;
30}
31
32static uint16_t get16le(const uint8_t *p)
33{
34 return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
35}
36
37static uint32_t get32le(const uint8_t *p)
38{
39 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
40}
41
42static uint64_t get64le(const uint8_t *p)
43{
44 return (uint64_t)get32le(p) | ((uint64_t)get32le(p + 4) << 32);
45}
46
47// Write the AMS/TCP header (with the final total length) + the 32-octet AMS header. The payload
48// is appended by the caller; `payload_len` is cbData. Returns ADS_HDR_LEN, or 0 if too small.
49static size_t write_header(uint8_t *buf, size_t cap, const AdsRequest *r, AdsCommand cmd, uint32_t payload_len)
50{
51 if (!buf || !r || cap < (size_t)ADS_HDR_LEN + payload_len)
52 return 0;
53 size_t p = 0;
54 // AMS/TCP header: reserved(2) + length(4). length covers the AMS header + payload.
55 buf[p++] = 0x00;
56 buf[p++] = 0x00;
57 p += put32le(buf + p, (uint32_t)ADS_AMS_HDR_LEN + payload_len);
58 // AMS header.
59 memcpy(buf + p, r->target.net_id, ADS_NET_ID_LEN);
60 p += ADS_NET_ID_LEN;
61 p += put16le(buf + p, r->target.port);
62 memcpy(buf + p, r->source.net_id, ADS_NET_ID_LEN);
63 p += ADS_NET_ID_LEN;
64 p += put16le(buf + p, r->source.port);
65 p += put16le(buf + p, (uint16_t)cmd); // wire byte in
66 p += put16le(buf + p, AdsStateFlags::request);
67 p += put32le(buf + p, payload_len); // cbData
68 p += put32le(buf + p, 0); // error code (0 on a request)
69 p += put32le(buf + p, r->invoke_id);
70 return p; // == ADS_HDR_LEN
71}
72
73size_t ads_build_read_device_info(uint8_t *buf, size_t cap, const AdsRequest *r)
74{
75 return write_header(buf, cap, r, AdsCommand::read_device_info, 0);
76}
77
78size_t ads_build_read_state(uint8_t *buf, size_t cap, const AdsRequest *r)
79{
80 return write_header(buf, cap, r, AdsCommand::read_state, 0);
81}
82
83size_t ads_build_read(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group, uint32_t index_offset,
84 uint32_t read_len)
85{
86 size_t p = write_header(buf, cap, r, AdsCommand::read, 12);
87 if (!p)
88 return 0;
89 p += put32le(buf + p, index_group);
90 p += put32le(buf + p, index_offset);
91 p += put32le(buf + p, read_len);
92 return p;
93}
94
95size_t ads_build_write(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group, uint32_t index_offset,
96 const uint8_t *data, uint32_t len)
97{
98 if (len && !data)
99 return 0;
100 size_t p = write_header(buf, cap, r, AdsCommand::write, 12 + len);
101 if (!p)
102 return 0;
103 p += put32le(buf + p, index_group);
104 p += put32le(buf + p, index_offset);
105 p += put32le(buf + p, len);
106 if (len)
107 {
108 memcpy(buf + p, data, len);
109 p += len;
110 }
111 return p;
112}
113
114size_t ads_build_read_write(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group, uint32_t index_offset,
115 uint32_t read_len, const uint8_t *write_data, uint32_t write_len)
116{
117 if (write_len && !write_data)
118 return 0;
119 size_t p = write_header(buf, cap, r, AdsCommand::read_write, 16 + write_len);
120 if (!p)
121 return 0;
122 p += put32le(buf + p, index_group);
123 p += put32le(buf + p, index_offset);
124 p += put32le(buf + p, read_len);
125 p += put32le(buf + p, write_len);
126 if (write_len)
127 {
128 memcpy(buf + p, write_data, write_len);
129 p += write_len;
130 }
131 return p;
132}
133
134size_t ads_build_write_control(uint8_t *buf, size_t cap, const AdsRequest *r, uint16_t ads_state, uint16_t device_state,
135 const uint8_t *data, uint32_t len)
136{
137 if (len && !data)
138 return 0;
139 size_t p = write_header(buf, cap, r, AdsCommand::write_control, 8 + len);
140 if (!p)
141 return 0;
142 p += put16le(buf + p, ads_state);
143 p += put16le(buf + p, device_state);
144 p += put32le(buf + p, len);
145 if (len)
146 {
147 memcpy(buf + p, data, len);
148 p += len;
149 }
150 return p;
151}
152
153size_t ads_build_add_notification(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t index_group,
154 uint32_t index_offset, uint32_t length, AdsTransMode mode, uint32_t max_delay,
155 uint32_t cycle_time)
156{
157 // IndexGroup + IndexOffset + Length + TransMode + MaxDelay + CycleTime + Reserved(16) = 40.
158 size_t p = write_header(buf, cap, r, AdsCommand::add_notification, 40);
159 if (!p)
160 return 0;
161 p += put32le(buf + p, index_group);
162 p += put32le(buf + p, index_offset);
163 p += put32le(buf + p, length);
164 p += put32le(buf + p, (uint32_t)mode); // wire byte in
165 p += put32le(buf + p, max_delay);
166 p += put32le(buf + p, cycle_time);
167 memset(buf + p, 0, 16); // reserved
168 p += 16;
169 return p;
170}
171
172size_t ads_build_del_notification(uint8_t *buf, size_t cap, const AdsRequest *r, uint32_t notification_handle)
173{
174 size_t p = write_header(buf, cap, r, AdsCommand::del_notification, 4);
175 if (!p)
176 return 0;
177 p += put32le(buf + p, notification_handle);
178 return p;
179}
180
181bool ads_parse_ams_header(const uint8_t *buf, size_t len, AdsAmsHeader *out)
182{
183 if (!buf || !out || len < (size_t)ADS_HDR_LEN)
184 return false;
185 if (buf[0] != 0x00 || buf[1] != 0x00) // AMS/TCP reserved
186 return false;
187 uint32_t frame_len = get32le(buf + 2); // AMS header + payload
188 if (frame_len < (uint32_t)ADS_AMS_HDR_LEN)
189 return false;
190 if ((size_t)ADS_AMSTCP_HDR_LEN + frame_len > len)
191 return false;
192 const uint8_t *a = buf + ADS_AMSTCP_HDR_LEN;
193 memcpy(out->target.net_id, a, ADS_NET_ID_LEN);
194 out->target.port = get16le(a + 6);
195 memcpy(out->source.net_id, a + 8, ADS_NET_ID_LEN);
196 out->source.port = get16le(a + 14);
197 out->cmd = (AdsCommand)get16le(a + 16); // wire byte out
198 out->state_flags = get16le(a + 18);
199 out->data_len = get32le(a + 20);
200 out->error_code = get32le(a + 24);
201 out->invoke_id = get32le(a + 28);
202 // cbData must fit inside the frame the AMS/TCP length promised.
203 if ((uint32_t)ADS_AMS_HDR_LEN + out->data_len > frame_len)
204 return false;
205 out->data = a + ADS_AMS_HDR_LEN;
206 return true;
207}
208
209bool ads_parse_read(const uint8_t *data, size_t data_len, AdsReadResult *out)
210{
211 if (!data || !out || data_len < 8)
212 return false;
213 out->result = get32le(data);
214 out->len = get32le(data + 4);
215 if (8 + (size_t)out->len > data_len)
216 return false;
217 out->data = data + 8;
218 return true;
219}
220
221bool ads_parse_result(const uint8_t *data, size_t data_len, uint32_t *result)
222{
223 if (!data || !result || data_len < 4)
224 return false;
225 *result = get32le(data);
226 return true;
227}
228
229bool ads_parse_read_state(const uint8_t *data, size_t data_len, AdsReadStateResult *out)
230{
231 if (!data || !out || data_len < 8)
232 return false;
233 out->result = get32le(data);
234 out->ads_state = get16le(data + 4);
235 out->device_state = get16le(data + 6);
236 return true;
237}
238
239bool ads_parse_read_device_info(const uint8_t *data, size_t data_len, AdsDeviceInfo *out)
240{
241 if (!data || !out || data_len < 4 + 4 + ADS_DEVICE_NAME_LEN)
242 return false;
243 out->result = get32le(data);
244 out->version_major = data[4];
245 out->version_minor = data[5];
246 out->version_build = get16le(data + 6);
247 memcpy(out->device_name, data + 8, ADS_DEVICE_NAME_LEN);
248 out->device_name[ADS_DEVICE_NAME_LEN] = '\0'; // the field is not guaranteed NUL-terminated
249 return true;
250}
251
252bool ads_parse_add_notification(const uint8_t *data, size_t data_len, uint32_t *result, uint32_t *handle)
253{
254 if (!data || !result || !handle || data_len < 8)
255 return false;
256 *result = get32le(data);
257 *handle = get32le(data + 4);
258 return true;
259}
260
261bool ads_parse_notification(const uint8_t *data, size_t data_len, AdsNotificationSampleFn on_sample, void *user)
262{
263 // Length(4) + Stamps(4), then per stamp: Timestamp(8) + Samples(4) + samples.
264 if (!data || !on_sample || data_len < 8)
265 return false;
266 uint32_t length = get32le(data); // octets after this field
267 uint32_t stamps = get32le(data + 4);
268 if (4 + (size_t)length > data_len)
269 return false;
270 size_t p = 8;
271 for (uint32_t s = 0; s < stamps; s++)
272 {
273 if (p + 12 > data_len) // timestamp(8) + samples(4)
274 return false;
275 uint64_t timestamp = get64le(data + p);
276 uint32_t samples = get32le(data + p + 8);
277 p += 12;
278 for (uint32_t i = 0; i < samples; i++)
279 {
280 if (p + 8 > data_len) // handle(4) + size(4)
281 return false;
282 uint32_t handle = get32le(data + p);
283 uint32_t size = get32le(data + p + 4);
284 p += 8;
285 if (p + (size_t)size > data_len)
286 return false;
287 on_sample(handle, data + p, size, timestamp, user);
288 p += size;
289 }
290 }
291 return true;
292}
293
294#endif // DETWS_ENABLE_ADS
Beckhoff ADS / AMS protocol codec (DETWS_ENABLE_ADS) - zero-heap request builders + response parsers ...