ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
scpi.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 scpi.cpp
6 * @brief SCPI / IEEE 488.2 instrument-control codec (pure, host-tested).
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_SCPI
13
14#include <stdio.h> // snprintf (number formatting only; parsing is hand-rolled - no stdlib)
15#include <string.h>
16
17// ── small helpers ──────────────────────────────────────────────────────────────────────────────
18
19static char lower(char c)
20{
21 return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
22}
23
24// Case-insensitive equality of two byte ranges.
25static bool ieq(const char *a, size_t alen, const char *b, size_t blen)
26{
27 if (alen != blen)
28 {
29 return false;
30 }
31 for (size_t i = 0; i < alen; i++)
32 {
33 if (lower(a[i]) != lower(b[i]))
34 {
35 return false;
36 }
37 }
38 return true;
39}
40
41static bool is_digit(char c)
42{
43 return c >= '0' && c <= '9';
44}
45
46static bool is_alpha(char c)
47{
48 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
49}
50
51// ── common commands ────────────────────────────────────────────────────────────────────────────
52
53const char *pc_scpi_common(ScpiCommon c)
54{
55 switch (c)
56 {
57 case ScpiCommon::SCPI_CLS:
58 return "*CLS";
59 case ScpiCommon::SCPI_ESE:
60 return "*ESE";
61 case ScpiCommon::SCPI_ESE_Q:
62 return "*ESE?";
63 case ScpiCommon::SCPI_ESR_Q:
64 return "*ESR?";
65 case ScpiCommon::SCPI_IDN_Q:
66 return "*IDN?";
67 case ScpiCommon::SCPI_OPC:
68 return "*OPC";
69 case ScpiCommon::SCPI_OPC_Q:
70 return "*OPC?";
71 case ScpiCommon::SCPI_RST:
72 return "*RST";
73 case ScpiCommon::SCPI_SRE:
74 return "*SRE";
75 case ScpiCommon::SCPI_SRE_Q:
76 return "*SRE?";
77 case ScpiCommon::SCPI_STB_Q:
78 return "*STB?";
79 case ScpiCommon::SCPI_TST_Q:
80 return "*TST?";
81 case ScpiCommon::SCPI_WAI:
82 return "*WAI";
83 }
84 return "";
85}
86
87// ── command builder ────────────────────────────────────────────────────────────────────────────
88
89size_t pc_scpi_build(char *buf, size_t cap, const char *header, const char *const *args, size_t argc)
90{
91 if (!buf || !header || (argc && !args))
92 {
93 return 0;
94 }
95 size_t hlen = strnlen(header, cap);
96 if (hlen == 0 || hlen >= cap)
97 {
98 return 0;
99 }
100
101 size_t p = 0;
102 memcpy(buf, header, hlen);
103 p = hlen;
104 for (size_t i = 0; i < argc; i++)
105 {
106 if (!args[i])
107 {
108 return 0;
109 }
110 char sep = (i == 0) ? ' ' : ',';
111 size_t alen = strnlen(args[i], cap);
112 if (p + 1 + alen + 1 >= cap) // sep + arg + the trailing '\n' + NUL
113 {
114 return 0;
115 }
116 buf[p++] = sep;
117 memcpy(buf + p, args[i], alen);
118 p += alen;
119 }
120 if (p + 1 >= cap) // room for '\n' + NUL
121 {
122 return 0;
123 }
124 buf[p++] = '\n';
125 buf[p] = '\0';
126 return p;
127}
128
129size_t pc_scpi_fmt_real(char *buf, size_t cap, double v)
130{
131 if (!buf || cap == 0)
132 {
133 return 0;
134 }
135 // %g renders NR2 (fixed) or NR3 (scientific) and trims trailing zeros - exactly the SCPI forms.
136 pc_sb sb_buf = {buf, cap, 0, true};
137 pc_sb_g(&sb_buf, (double)(v), 10);
138 int n = (int)pc_sb_finish(&sb_buf);
139 // n<0 is unreachable: "%.10g" on a real double never hits a libc encoding error (no I/O, no wide
140 // conversion); the compound condition's branch data lands on this "if" line, not the continuation
141 // below, so the exclusion marker must sit here, not on the "cap)" line, to actually take effect.
142 if (n < 0 || // GCOVR_EXCL_BR_LINE
143 (size_t)n >= cap)
144 {
145 if (cap) // GCOVR_EXCL_BR_LINE cap==0 already returned above (see the guard at function entry)
146 {
147 buf[0] = '\0';
148 }
149 return 0;
150 }
151 return (size_t)n;
152}
153
154// ── response parsers ───────────────────────────────────────────────────────────────────────────
155
156// Fold the digit run at s[*i] into *acc, advancing *i past it. Returns how many digits were consumed.
157static int scan_digits(const char *s, size_t len, size_t *i, double *acc)
158{
159 int n = 0;
160 while (*i < len && is_digit(s[*i]))
161 {
162 *acc = *acc * 10.0 + (s[*i] - '0');
163 n++;
164 (*i)++;
165 }
166 return n;
167}
168
169// Optional E[+-]ddd suffix. Absent is not an error (*exp stays 0); false only for an E with no digits.
170static bool scan_exponent(const char *s, size_t len, size_t *i, int *exp)
171{
172 *exp = 0;
173 if (*i >= len || (s[*i] != 'e' && s[*i] != 'E'))
174 {
175 return true;
176 }
177 (*i)++;
178 bool neg = false;
179 if (*i < len && (s[*i] == '+' || s[*i] == '-'))
180 {
181 neg = (s[*i] == '-');
182 (*i)++;
183 }
184 int digits = 0;
185 while (*i < len && is_digit(s[*i]))
186 {
187 *exp = *exp * 10 + (s[*i] - '0');
188 digits++;
189 (*i)++;
190 }
191 if (digits == 0)
192 {
193 return false;
194 }
195 if (neg)
196 {
197 *exp = -*exp;
198 }
199 return true;
200}
201
202// Repeated multiply/divide rather than pow(): no libm dependency, and the digit counts here are small.
203static double apply_scale(double v, int scale)
204{
205 for (int k = 0; k < scale; k++)
206 {
207 v *= 10.0;
208 }
209 for (int k = 0; k < -scale; k++)
210 {
211 v /= 10.0;
212 }
213 return v;
214}
215
216bool pc_scpi_parse_number(const char *s, size_t len, double *out)
217{
218 if (!s || !out || len == 0)
219 {
220 return false;
221 }
222 size_t i = 0;
223 bool neg = false;
224 if (s[i] == '+' || s[i] == '-')
225 {
226 neg = (s[i] == '-');
227 i++;
228 }
229 double mant = 0.0;
230 int digits = scan_digits(s, len, &i, &mant);
231 int frac = 0; // number of fractional digits accumulated
232 if (i < len && s[i] == '.')
233 {
234 i++;
235 frac = scan_digits(s, len, &i, &mant);
236 digits += frac;
237 }
238 if (digits == 0) // no mantissa digit on either side of the point
239 {
240 return false;
241 }
242 int exp = 0;
243 if (!scan_exponent(s, len, &i, &exp))
244 {
245 return false;
246 }
247 if (i != len) // trailing junk -> not a clean numeric field
248 {
249 return false;
250 }
251
252 double val = apply_scale(mant, exp - frac); // decimal point and exponent applied together
253 *out = neg ? -val : val;
254 return true;
255}
256
257bool pc_scpi_parse_bool(const char *s, size_t len, bool *out)
258{
259 if (!s || !out || len == 0)
260 {
261 return false;
262 }
263 if (len == 1 && s[0] == '1')
264 {
265 *out = true;
266 return true;
267 }
268 if (len == 1 && s[0] == '0')
269 {
270 *out = false;
271 return true;
272 }
273 if (ieq(s, len, "ON", 2))
274 {
275 *out = true;
276 return true;
277 }
278 if (ieq(s, len, "OFF", 3))
279 {
280 *out = false;
281 return true;
282 }
283 return false;
284}
285
286size_t pc_scpi_parse_string(const char *s, size_t len, char *out, size_t cap)
287{
288 if (!s || !out || cap == 0 || len < 2)
289 {
290 return 0;
291 }
292 char q = s[0];
293 if ((q != '"' && q != '\'') || s[len - 1] != q)
294 {
295 return 0;
296 }
297 size_t o = 0;
298 size_t i = 1;
299 while (i + 1 < len)
300 {
301 size_t adv = 1;
302 if (s[i] == q)
303 {
304 // a doubled quote inside is one literal quote; anything else is a malformed close
305 if (i + 2 < len && s[i + 1] == q)
306 {
307 adv = 2; // step over the pair, emitting the single quote below
308 }
309 else
310 {
311 return 0;
312 }
313 }
314 if (o + 1 >= cap) // leave room for the NUL
315 {
316 return 0;
317 }
318 out[o++] = s[i];
319 i += adv;
320 }
321 out[o] = '\0';
322 return o;
323}
324
325bool pc_scpi_parse_block(const uint8_t *buf, size_t len, const uint8_t **data, size_t *data_len, size_t *consumed)
326{
327 if (!buf || !data || !data_len || !consumed || len < 2 || buf[0] != '#')
328 {
329 return false;
330 }
331 char n = (char)buf[1];
332 if (n == '0') // indefinite: #0<data><NL with EOI> - data runs to the final newline
333 {
334 if (len < 3 || buf[len - 1] != '\n')
335 {
336 return false;
337 }
338 *data = buf + 2;
339 *data_len = len - 3; // drop the leading "#0" and the trailing NL
340 *consumed = len;
341 return true;
342 }
343 if (n < '1' || n > '9') // definite: one nonzero digit giving the length-field width
344 {
345 return false;
346 }
347 size_t ndig = (size_t)(n - '0');
348 if (len < 2 + ndig)
349 {
350 return false;
351 }
352 size_t dlen = 0;
353 for (size_t i = 0; i < ndig; i++)
354 {
355 char c = (char)buf[2 + i];
356 if (!is_digit(c))
357 {
358 return false;
359 }
360 dlen = dlen * 10 + (size_t)(c - '0');
361 }
362 size_t start = 2 + ndig;
363 if (start + dlen > len)
364 {
365 return false;
366 }
367 *data = buf + start;
368 *data_len = dlen;
369 *consumed = start + dlen;
370 return true;
371}
372
373// ── IEEE 488.2 / SCPI status model ─────────────────────────────────────────────────────────────
374
375// The standard SCPI error/event messages (SCPI-99 Vol 2, "Error and Event Handling"). Only the
376// common standard numbers are tabled; an unknown number yields "" (the app supplies its own text).
377struct ScpiStdMsg
378{
379 int16_t number;
380 const char *msg;
381};
382static const ScpiStdMsg SCPI_STD[] = {
383 {0, "No error"},
384 // -1xx Command errors (ESR bit CME)
385 {-100, "Command error"},
386 {-101, "Invalid character"},
387 {-102, "Syntax error"},
388 {-103, "Invalid separator"},
389 {-104, "Data type error"},
390 {-108, "Parameter not allowed"},
391 {-109, "Missing parameter"},
392 {-110, "Command header error"},
393 {-111, "Header separator error"},
394 {-112, "Program mnemonic too long"},
395 {-113, "Undefined header"},
396 {-114, "Header suffix out of range"},
397 {-120, "Numeric data error"},
398 {-128, "Numeric data not allowed"},
399 {-148, "Character data not allowed"},
400 {-150, "String data error"},
401 {-158, "String data not allowed"},
402 {-168, "Block data not allowed"},
403 // -2xx Execution errors (ESR bit EXE)
404 {-200, "Execution error"},
405 {-220, "Parameter error"},
406 {-221, "Settings conflict"},
407 {-222, "Data out of range"},
408 {-223, "Too much data"},
409 {-224, "Illegal parameter value"},
410 {-230, "Data corrupt or stale"},
411 {-240, "Hardware error"},
412 {-241, "Hardware missing"},
413 // -3xx Device-specific errors (ESR bit DDE)
414 {-300, "Device-specific error"},
415 {-310, "System error"},
416 {-311, "Memory error"},
417 {-313, "Calibration memory lost"},
418 {-314, "Save/recall memory lost"},
419 {-315, "Configuration memory lost"},
420 {-321, "Out of memory"},
421 {-330, "Self-test failed"},
422 {-350, "Queue overflow"},
423 {-360, "Communication error"},
424 {-363, "Input buffer overrun"},
425 // -4xx Query errors (ESR bit QYE)
426 {-400, "Query error"},
427 {-410, "Query INTERRUPTED"},
428 {-420, "Query UNTERMINATED"},
429 {-430, "Query DEADLOCKED"},
430 {-440, "Query UNTERMINATED after indefinite response"},
431};
432
433const char *pc_scpi_std_error(int16_t number)
434{
435 for (size_t i = 0; i < sizeof(SCPI_STD) / sizeof(SCPI_STD[0]); i++)
436 {
437 if (SCPI_STD[i].number == number)
438 {
439 return SCPI_STD[i].msg;
440 }
441 }
442 return "";
443}
444
445void pc_scpi_status_init(ScpiStatus *s)
446{
447 if (!s)
448 {
449 return;
450 }
451 memset(s, 0, sizeof(*s));
452}
453
454void pc_scpi_event(ScpiStatus *s, uint8_t esr_bits)
455{
456 if (s)
457 {
458 s->esr |= esr_bits;
459 }
460}
461
462// Map an error/event number to the ESR bit it latches, per SCPI-99 Vol 2 §21.8 class ranges:
463// -1xx CME, -2xx EXE, -3xx (and positive device-specific) DDE, -4xx QYE, -5xx PON, -6xx URQ,
464// -7xx RQC, -8xx OPC. 0 = No error latches nothing.
465static uint8_t esr_bit_for(int16_t number)
466{
467 if (number >= 0)
468 {
469 return number == 0 ? 0 : SCPI_ESR_DDE; // GCOVR_EXCL_BR_LINE ==0 arm unreachable: pc_scpi_push_error (only
470 // caller) already returns on number==0 before calling this
471 }
472 if (number > -200)
473 {
474 return SCPI_ESR_CME;
475 }
476 if (number > -300)
477 {
478 return SCPI_ESR_EXE;
479 }
480 if (number > -400)
481 {
482 return SCPI_ESR_DDE;
483 }
484 if (number > -500)
485 {
486 return SCPI_ESR_QYE;
487 }
488 if (number > -600)
489 {
490 return SCPI_ESR_PON;
491 }
492 if (number > -700)
493 {
494 return SCPI_ESR_URQ;
495 }
496 if (number > -800)
497 {
498 return SCPI_ESR_RQC;
499 }
500 if (number > -900)
501 {
502 return SCPI_ESR_OPC;
503 }
504 return 0;
505}
506
507void pc_scpi_push_error(ScpiStatus *s, int16_t number, const char *msg)
508{
509 if (!s || number == 0)
510 {
511 return;
512 }
513 if (!msg)
514 {
515 msg = pc_scpi_std_error(number);
516 }
517 s->esr |= esr_bit_for(number);
518 if (s->count >= PC_SCPI_ERR_QUEUE)
519 {
520 // Overflow: the most recent entry becomes -350 "Queue overflow" (SCPI rule); latch DDE.
521 uint8_t tail = (uint8_t)((s->head + s->count - 1) % PC_SCPI_ERR_QUEUE);
522 s->queue[tail].number = -350;
523 s->queue[tail].msg = pc_scpi_std_error(-350);
524 s->esr |= SCPI_ESR_DDE;
525 return;
526 }
527 uint8_t slot = (uint8_t)((s->head + s->count) % PC_SCPI_ERR_QUEUE);
528 s->queue[slot].number = number;
529 s->queue[slot].msg = msg;
530 s->count++;
531}
532
533bool pc_scpi_pop_error(ScpiStatus *s, ScpiError *out)
534{
535 if (!out)
536 {
537 return false;
538 }
539 if (!s || s->count == 0)
540 {
541 out->number = 0;
542 out->msg = "No error";
543 return false;
544 }
545 *out = s->queue[s->head];
546 s->head = (uint8_t)((s->head + 1) % PC_SCPI_ERR_QUEUE);
547 s->count--;
548 return true;
549}
550
551uint8_t pc_scpi_stb(const ScpiStatus *s)
552{
553 if (!s)
554 {
555 return 0;
556 }
557 uint8_t stb = (uint8_t)(s->summary & (SCPI_STB_QSB | SCPI_STB_MAV | SCPI_STB_OSB));
558 if (s->count)
559 {
560 stb |= SCPI_STB_EAV;
561 }
562 if (s->esr & s->ese)
563 {
564 stb |= SCPI_STB_ESB;
565 }
566 // MSS = OR of (STB & SRE) over every bit except bit 6 (it cannot summarize itself).
567 if (stb & s->sre & (uint8_t)~SCPI_STB_MSS)
568 {
569 stb |= SCPI_STB_MSS;
570 }
571 return stb;
572}
573
574void pc_scpi_cls(ScpiStatus *s)
575{
576 if (!s)
577 {
578 return;
579 }
580 s->esr = 0;
581 s->head = 0;
582 s->count = 0;
583}
584
585// ── SCPI short/long-form header matcher ────────────────────────────────────────────────────────
586
587// Parse a decimal numeric suffix; empty defaults to 1 (SCPI numeric-suffix rule). -1 on a non-digit.
588static int suffix_val(const char *s, size_t len)
589{
590 if (len == 0)
591 {
592 return 1;
593 }
594 int v = 0;
595 for (size_t i = 0; i < len; i++)
596 {
597 if (!is_digit(s[i]))
598 {
599 return -1;
600 }
601 v = v * 10 + (s[i] - '0');
602 }
603 return v;
604}
605
606// Match one header node: input [i,ilen) vs pattern [p,plen) (uppercase run = short form).
607static bool match_node(const char *i, size_t ilen, const char *p, size_t plen)
608{
609 if (ilen == 0 || plen == 0)
610 {
611 return false;
612 }
613 // pattern alpha length + short-form (uppercase-prefix) length
614 size_t palpha = 0;
615 while (palpha < plen && is_alpha(p[palpha]))
616 {
617 palpha++;
618 }
619 size_t pshort = 0;
620 // ">= 'A'" false is unreachable: every byte here already passed is_alpha() in the palpha loop above,
621 // and both alpha ranges ('A'-'Z', 'a'-'z') satisfy >= 'A'; the branch data for a multi-line "&&" chain
622 // lands on the line each sub-test's operator sits on, so the marker must sit on this line, not the
623 // "<= 'Z'" continuation below, to actually take effect.
624 while (pshort < palpha && p[pshort] >= 'A' && // GCOVR_EXCL_BR_LINE
625 p[pshort] <= 'Z')
626 {
627 pshort++;
628 }
629 // input alpha length
630 size_t ialpha = 0;
631 while (ialpha < ilen && is_alpha(i[ialpha]))
632 {
633 ialpha++;
634 }
635 // the input alpha must equal the short form OR the whole long form (case-insensitive)
636 if (!ieq(i, ialpha, p, pshort) && !ieq(i, ialpha, p, palpha))
637 {
638 return false;
639 }
640 // numeric suffix (rest of each node) - omitted defaults to 1
641 int pv = suffix_val(p + palpha, plen - palpha);
642 int iv = suffix_val(i + ialpha, ilen - ialpha);
643 return pv >= 0 && iv >= 0 && pv == iv;
644}
645
646bool pc_scpi_match(const char *input, size_t input_len, const char *pattern)
647{
648 if (!input || !pattern)
649 {
650 return false;
651 }
652 // clip the input to its header (everything before the first space)
653 size_t hlen = 0;
654 while (hlen < input_len && input[hlen] != ' ')
655 {
656 hlen++;
657 }
658 const char *ip = input;
659 size_t irem = hlen;
660
661 // common command: match the whole token case-insensitively
662 if (pattern[0] == '*')
663 {
664 return ieq(ip, irem, pattern, strnlen(pattern, 64));
665 }
666
667 // a leading ':' on the input is an absolute-root anchor - skip it
668 if (irem && ip[0] == ':')
669 {
670 ip++;
671 irem--;
672 }
673
674 size_t prem = strnlen(pattern, 256);
675 const char *pp = pattern;
676
677 // reconcile the query '?' suffix: both must have it or neither
678 bool pq = prem && pp[prem - 1] == '?';
679 bool iq = irem && ip[irem - 1] == '?';
680 if (pq != iq)
681 {
682 return false;
683 }
684 if (pq)
685 {
686 prem--;
687 }
688 if (iq)
689 {
690 irem--;
691 }
692
693 // walk the ':'-separated nodes in lockstep
694 while (true)
695 {
696 size_t pn = 0;
697 while (pn < prem && pp[pn] != ':')
698 {
699 pn++;
700 }
701 size_t in = 0;
702 while (in < irem && ip[in] != ':')
703 {
704 in++;
705 }
706 if (!match_node(ip, in, pp, pn))
707 {
708 return false;
709 }
710 // advance past this node (and its ':' if present)
711 bool p_more = pn < prem;
712 bool i_more = in < irem;
713 if (p_more != i_more) // different depth
714 {
715 return false;
716 }
717 if (!p_more) // both exhausted -> full match
718 {
719 return true;
720 }
721 pp += pn + 1;
722 prem -= pn + 1;
723 ip += in + 1;
724 irem -= in + 1;
725 }
726}
727
728#endif // PC_ENABLE_SCPI
#define PC_SCPI_ERR_QUEUE
SCPI error/event queue depth (entries). The SCPI status model requires a queue; when it overflows the...
SCPI / IEEE 488.2 instrument-control codec (PC_ENABLE_SCPI) - a zero-heap codec for the text command ...
Bounded no-heap string builder that fails closed on overflow (one shared copy).
void pc_sb_g(pc_sb *b, double v, unsigned sig)
Append v with sig significant digits, choosing fixed or scientific form - the printf "%....
Definition strbuf.h:437
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30