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