DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
mtconnect.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 mtconnect.cpp
6 * @brief MTConnect agent response codec (see mtconnect.h).
7 */
8
10
11#if DETWS_ENABLE_MTCONNECT
12
13#include <string.h>
14
15namespace
16{
17void put(DetwsMtcStreams *s, const char *text)
18{
19 if (!s->ok || !text) // null text is a no-op: harden the helper itself, not just every call site
20 return;
21 size_t tl = strnlen(text, s->cap + 1);
22 if (s->len + tl >= s->cap)
23 {
24 s->ok = false;
25 return;
26 }
27 memcpy(s->buf + s->len, text, tl);
28 s->len += tl;
29}
30
31void put_escaped(DetwsMtcStreams *s, const char *text)
32{
33 if (!s->ok || !text) // null text is a no-op: harden the helper itself, not just every call site
34 return;
35 for (const char *p = text; *p; p++)
36 {
37 const char *rep = nullptr;
38 switch (*p)
39 {
40 case '&':
41 rep = "&amp;";
42 break;
43 case '<':
44 rep = "&lt;";
45 break;
46 case '>':
47 rep = "&gt;";
48 break;
49 case '"':
50 rep = "&quot;";
51 break;
52 default:
53 break;
54 }
55 if (rep)
56 put(s, rep);
57 else
58 {
59 if (s->len + 1 >= s->cap)
60 {
61 s->ok = false;
62 return;
63 }
64 s->buf[s->len++] = *p;
65 }
66 }
67}
68
69// A minimal unsigned -> decimal directly into the stream.
70void put_u64(DetwsMtcStreams *s, uint64_t v)
71{
72 char tmp[20];
73 int n = 0;
74 do
75 {
76 tmp[n++] = (char)('0' + (int)(v % 10));
77 v /= 10;
78 } while (v);
79 char out[21];
80 for (int i = 0; i < n; i++)
81 out[i] = tmp[n - 1 - i];
82 out[n] = '\0';
83 put(s, out);
84}
85
86const char *mtc_cat_str(DetwsMtcCategory cat)
87{
88 if (cat == DetwsMtcCategory::DETWS_MTC_SAMPLE)
89 return "SAMPLE";
90 if (cat == DetwsMtcCategory::DETWS_MTC_EVENT)
91 return "EVENT";
92 return "CONDITION";
93}
94} // namespace
95
96void detws_mtc_streams_begin(DetwsMtcStreams *s, char *buf, size_t cap, uint64_t instance_id, uint64_t next_seq,
97 const char *device_name)
98{
99 s->buf = buf;
100 s->cap = cap;
101 s->len = 0;
102 s->ok = (buf != nullptr && cap > 0);
103 s->in_comp = false;
104 put(s, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
105 put(s, "<MTConnectStreams xmlns=\"urn:mtconnect.org:MTConnectStreams:1.4\">");
106 put(s, "<Header instanceId=\"");
107 put_u64(s, instance_id);
108 put(s, "\" version=\"1.4\" nextSequence=\"");
109 put_u64(s, next_seq);
110 put(s, "\"/>");
111 put(s, "<Streams><DeviceStream name=\"");
112 put_escaped(s, device_name ? device_name : "");
113 put(s, "\">");
114}
115
116void detws_mtc_streams_add(DetwsMtcStreams *s, DetwsMtcCategory cat, const char *type, const char *data_id,
117 uint64_t seq, const char *timestamp, const char *value)
118{
119 if (!s->ok)
120 return;
121 if (!s->in_comp)
122 {
123 put(s, "<ComponentStream component=\"Device\">");
124 s->in_comp = true;
125 }
126 const char *wrap = "Condition";
127 if (cat == DetwsMtcCategory::DETWS_MTC_SAMPLE)
128 wrap = "Samples";
129 else if (cat == DetwsMtcCategory::DETWS_MTC_EVENT)
130 wrap = "Events";
131 put(s, "<");
132 put(s, wrap);
133 put(s, ">");
134 if (cat == DetwsMtcCategory::DETWS_MTC_CONDITION)
135 {
136 // <Condition><Normal type="TYPE" dataItemId="ID" sequence="SEQ" timestamp="TS"/></Condition>
137 const char *sub = value ? value : "Normal"; // Normal / Warning / Fault / Unavailable
138 put(s, "<");
139 put(s, sub);
140 put(s, " type=\"");
141 put_escaped(s, type ? type : "");
142 put(s, "\" dataItemId=\"");
143 put_escaped(s, data_id ? data_id : "");
144 put(s, "\" sequence=\"");
145 put_u64(s, seq);
146 put(s, "\" timestamp=\"");
147 put_escaped(s, timestamp ? timestamp : "");
148 put(s, "\"/>");
149 }
150 else
151 {
152 // <Type dataItemId="ID" sequence="SEQ" timestamp="TS">VALUE</Type>
153 put(s, "<");
154 put(s, type ? type : "");
155 put(s, " dataItemId=\"");
156 put_escaped(s, data_id ? data_id : "");
157 put(s, "\" sequence=\"");
158 put_u64(s, seq);
159 put(s, "\" timestamp=\"");
160 put_escaped(s, timestamp ? timestamp : "");
161 put(s, "\">");
162 put_escaped(s, value ? value : "");
163 put(s, "</");
164 put(s, type ? type : "");
165 put(s, ">");
166 }
167 put(s, "</");
168 put(s, wrap);
169 put(s, ">");
170}
171
172size_t detws_mtc_streams_end(DetwsMtcStreams *s)
173{
174 if (s->in_comp)
175 put(s, "</ComponentStream>");
176 put(s, "</DeviceStream></Streams></MTConnectStreams>");
177 if (!s->ok)
178 return 0;
179 s->buf[s->len] = '\0';
180 return s->len;
181}
182
183size_t detws_mtc_error(uint64_t instance_id, const char *error_code, const char *message, char *out, size_t cap)
184{
185 DetwsMtcStreams s;
186 s.buf = out;
187 s.cap = cap;
188 s.len = 0;
189 s.ok = (out != nullptr && cap > 0);
190 s.in_comp = false;
191 put(&s, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
192 put(&s, "<MTConnectError xmlns=\"urn:mtconnect.org:MTConnectError:1.4\">");
193 put(&s, "<Header instanceId=\"");
194 put_u64(&s, instance_id);
195 put(&s, "\" version=\"1.4\"/>");
196 put(&s, "<Errors><Error errorCode=\"");
197 put_escaped(&s, error_code ? error_code : "");
198 put(&s, "\">");
199 put_escaped(&s, message ? message : "");
200 put(&s, "</Error></Errors></MTConnectError>");
201 // Null-check `out` directly (not just s.ok, which already implies it) so the NUL terminator
202 // write is provably safe to the analyzer as well as at runtime.
203 if (!s.ok || out == nullptr)
204 return 0;
205 out[s.len] = '\0';
206 return s.len;
207}
208
209// --- probe (MTConnectDevices): the device model a client discovers before streaming ---
210
211void detws_mtc_devices_begin(DetwsMtcStreams *s, char *buf, size_t cap, uint64_t instance_id, const char *device_id,
212 const char *device_name, const char *uuid)
213{
214 s->buf = buf;
215 s->cap = cap;
216 s->len = 0;
217 s->ok = (buf != nullptr && cap > 0);
218 s->in_comp = false;
219 put(s, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
220 put(s, "<MTConnectDevices xmlns=\"urn:mtconnect.org:MTConnectDevices:1.4\">");
221 put(s, "<Header instanceId=\"");
222 put_u64(s, instance_id);
223 put(s, "\" version=\"1.4\"/>");
224 put(s, "<Devices><Device id=\"");
225 put_escaped(s, device_id ? device_id : "");
226 put(s, "\" name=\"");
227 put_escaped(s, device_name ? device_name : "");
228 put(s, "\" uuid=\"");
229 put_escaped(s, uuid ? uuid : "");
230 put(s, "\"><DataItems>");
231}
232
233void detws_mtc_devices_add_item(DetwsMtcStreams *s, DetwsMtcCategory cat, const char *id, const char *type,
234 const char *name, const char *units)
235{
236 if (!s->ok)
237 return;
238 put(s, "<DataItem category=\"");
239 put(s, mtc_cat_str(cat));
240 put(s, "\" id=\"");
241 put_escaped(s, id ? id : "");
242 put(s, "\" type=\"");
243 put_escaped(s, type ? type : "");
244 put(s, "\"");
245 if (name && name[0])
246 {
247 put(s, " name=\"");
248 put_escaped(s, name);
249 put(s, "\"");
250 }
251 if (units && units[0])
252 {
253 put(s, " units=\"");
254 put_escaped(s, units);
255 put(s, "\"");
256 }
257 put(s, "/>");
258}
259
260size_t detws_mtc_devices_end(DetwsMtcStreams *s)
261{
262 put(s, "</DataItems></Device></Devices></MTConnectDevices>");
263 if (!s->ok)
264 return 0;
265 s->buf[s->len] = '\0';
266 return s->len;
267}
268
269// --- asset (MTConnectAssets): the tool/fixture inventory a client reads by GET /asset ---
270
271void detws_mtc_assets_begin(DetwsMtcStreams *s, char *buf, size_t cap, uint64_t instance_id, uint32_t asset_count,
272 uint32_t asset_buffer_size)
273{
274 s->buf = buf;
275 s->cap = cap;
276 s->len = 0;
277 s->ok = (buf != nullptr && cap > 0);
278 s->in_comp = false;
279 put(s, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
280 put(s, "<MTConnectAssets xmlns=\"urn:mtconnect.org:MTConnectAssets:1.4\">");
281 put(s, "<Header instanceId=\"");
282 put_u64(s, instance_id);
283 put(s, "\" version=\"1.4\" assetBufferSize=\"");
284 put_u64(s, asset_buffer_size);
285 put(s, "\" assetCount=\"");
286 put_u64(s, asset_count);
287 put(s, "\"/>");
288 put(s, "<Assets>");
289}
290
291void detws_mtc_assets_cutting_tool_begin(DetwsMtcStreams *s, const char *asset_id, const char *serial_number,
292 const char *tool_id, const char *device_uuid, const char *timestamp)
293{
294 put(s, "<CuttingTool assetId=\"");
295 put_escaped(s, asset_id ? asset_id : "");
296 put(s, "\"");
297 if (serial_number && serial_number[0])
298 {
299 put(s, " serialNumber=\"");
300 put_escaped(s, serial_number);
301 put(s, "\"");
302 }
303 if (tool_id && tool_id[0])
304 {
305 put(s, " toolId=\"");
306 put_escaped(s, tool_id);
307 put(s, "\"");
308 }
309 if (device_uuid && device_uuid[0])
310 {
311 put(s, " deviceUuid=\"");
312 put_escaped(s, device_uuid);
313 put(s, "\"");
314 }
315 if (timestamp && timestamp[0])
316 {
317 put(s, " timestamp=\"");
318 put_escaped(s, timestamp);
319 put(s, "\"");
320 }
321 put(s, "><CuttingToolLifeCycle>");
322}
323
324void detws_mtc_assets_tool_life(DetwsMtcStreams *s, const char *type, const char *count_direction, const char *limit,
325 const char *value)
326{
327 // <ToolLife type="MINUTES" countDirection="UP" limit="100">42</ToolLife>
328 put(s, "<ToolLife type=\"");
329 put_escaped(s, type ? type : "");
330 put(s, "\" countDirection=\"");
331 put_escaped(s, count_direction ? count_direction : "");
332 put(s, "\"");
333 if (limit && limit[0])
334 {
335 put(s, " limit=\"");
336 put_escaped(s, limit);
337 put(s, "\"");
338 }
339 put(s, ">");
340 put_escaped(s, value ? value : "");
341 put(s, "</ToolLife>");
342}
343
344void detws_mtc_assets_cutting_tool_end(DetwsMtcStreams *s)
345{
346 put(s, "</CuttingToolLifeCycle></CuttingTool>");
347}
348
349size_t detws_mtc_assets_end(DetwsMtcStreams *s)
350{
351 put(s, "</Assets></MTConnectAssets>");
352 if (!s->ok)
353 return 0;
354 s->buf[s->len] = '\0';
355 return s->len;
356}
357
358// --- sample sequence cursor: a rolling observation buffer for the `sample` from/count long-poll ---
359
360namespace
361{
362// Bounded, always-NUL-terminated copy into a fixed field.
363void mtc_copy_str(char *dst, size_t cap, const char *src)
364{
365 if (cap == 0)
366 return;
367 size_t i = 0;
368 if (src)
369 for (; i < cap - 1 && src[i] != '\0'; i++) // range check first (short-circuits the src read)
370 dst[i] = src[i];
371 dst[i] = '\0';
372}
373
374// Open an MTConnectStreams document with the full sample-cursor header (buffer/first/last/next seq).
375void mtc_streams_begin_windowed(DetwsMtcStreams *s, char *buf, size_t cap, uint64_t instance_id, uint64_t first_seq,
376 uint64_t last_seq, uint64_t next_seq, uint32_t buffer_size, const char *device_name)
377{
378 s->buf = buf;
379 s->cap = cap;
380 s->len = 0;
381 s->ok = (buf != nullptr && cap > 0);
382 s->in_comp = false;
383 put(s, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
384 put(s, "<MTConnectStreams xmlns=\"urn:mtconnect.org:MTConnectStreams:1.4\">");
385 put(s, "<Header instanceId=\"");
386 put_u64(s, instance_id);
387 put(s, "\" version=\"1.4\" bufferSize=\"");
388 put_u64(s, buffer_size);
389 put(s, "\" firstSequence=\"");
390 put_u64(s, first_seq);
391 put(s, "\" lastSequence=\"");
392 put_u64(s, last_seq);
393 put(s, "\" nextSequence=\"");
394 put_u64(s, next_seq);
395 put(s, "\"/>");
396 put(s, "<Streams><DeviceStream name=\"");
397 put_escaped(s, device_name ? device_name : "");
398 put(s, "\">");
399}
400} // namespace
401
402void detws_mtc_sample_buffer_init(DetwsMtcSampleBuffer *b, uint64_t start_seq)
403{
404 b->count = 0;
405 b->head = 0;
406 b->next_seq = start_seq ? start_seq : 1;
407 b->first_seq = b->next_seq; // empty: first == next (lastSequence = next-1 sits just below first)
408}
409
410uint64_t detws_mtc_sample_buffer_add(DetwsMtcSampleBuffer *b, DetwsMtcCategory cat, const char *type,
411 const char *data_id, const char *timestamp, const char *value)
412{
413 DetwsMtcObservation *o = &b->obs[b->head];
414 o->cat = cat;
415 o->seq = b->next_seq;
416 mtc_copy_str(o->type, sizeof(o->type), type);
417 mtc_copy_str(o->data_id, sizeof(o->data_id), data_id);
418 mtc_copy_str(o->timestamp, sizeof(o->timestamp), timestamp);
419 mtc_copy_str(o->value, sizeof(o->value), value);
420 b->head = (b->head + 1) % DETWS_MTC_SAMPLE_BUFFER;
421 if (b->count < DETWS_MTC_SAMPLE_BUFFER)
422 b->count++;
423 else
424 b->first_seq++; // ring full: the oldest was overwritten, so the window slides forward
425 return b->next_seq++;
426}
427
428size_t detws_mtc_sample_query(DetwsMtcSampleBuffer *b, char *buf, size_t cap, uint64_t instance_id,
429 const char *device_name, uint64_t from, uint32_t count)
430{
431 uint64_t first = b->first_seq;
432 uint64_t next = b->next_seq; // one past the newest retained observation
433 uint64_t last = next - 1; // newest sequence (next-1); when empty this is first-1
434 uint64_t start = from < first ? first : from; // a stale `from` catches up from the oldest kept
435
436 uint32_t avail = (start < next) ? (uint32_t)(next - start) : 0;
437 uint32_t to_emit = (count < avail) ? count : avail;
438 // Resume point: past the last one returned, or the buffer's nextSequence when nothing was in range.
439 uint64_t next_report = (start >= next) ? next : start + to_emit;
440
441 DetwsMtcStreams s;
442 mtc_streams_begin_windowed(&s, buf, cap, instance_id, first, last, next_report, DETWS_MTC_SAMPLE_BUFFER,
443 device_name);
444
445 // The oldest retained observation sits `count` slots behind head; observation `first + k` is at
446 // (oldest_idx + k) around the ring.
447 uint32_t oldest_idx = (b->head + DETWS_MTC_SAMPLE_BUFFER - b->count) % DETWS_MTC_SAMPLE_BUFFER;
448 for (uint32_t e = 0; e < to_emit; e++)
449 {
450 uint32_t k = (uint32_t)((start - first) + e);
451 DetwsMtcObservation *o = &b->obs[(oldest_idx + k) % DETWS_MTC_SAMPLE_BUFFER];
452 detws_mtc_streams_add(&s, o->cat, o->type, o->data_id, o->seq, o->timestamp, o->value);
453 }
454 return detws_mtc_streams_end(&s);
455}
456
457#endif // DETWS_ENABLE_MTCONNECT
#define DETWS_MTC_SAMPLE_BUFFER
MTConnect rolling sample buffer sizing (DETWS_ENABLE_MTCONNECT).
MTConnect agent response codec (DETWS_ENABLE_MTCONNECT).