RF-Positioning-System V0.1
by Max Heinekamp
Loading...
Searching...
No Matches
scpi.cpp
Go to the documentation of this file.
1
11
12#include "scpi.h"
13#include "scpi_commands.h"
14#include "flash.h"
15#include "esp_system.h"
16#include <utility/w5100.h> // SnSR:: raw TCP socket-status constants, for diagnostics
17
18int brightness = 0;
19extern FLASH flash_memory;
20
21// Defined in main.cpp — reused here so DIAGnostic:SYStem? doesn't duplicate the reset-reason
22// mapping, and so a repeated involuntary reboot mid-session is visible across soft resets.
23extern const char* resetReasonToString(esp_reset_reason_t reason);
24extern RTC_DATA_ATTR uint32_t bootCount;
25
26static const char* socketStatusName(uint8_t status) {
27 switch (status) {
28 case SnSR::CLOSED: return "CLOSED";
29 case SnSR::LISTEN: return "LISTEN";
30 case SnSR::SYNSENT: return "SYNSENT";
31 case SnSR::SYNRECV: return "SYNRECV";
32 case SnSR::ESTABLISHED: return "ESTABLISHED";
33 case SnSR::FIN_WAIT: return "FIN_WAIT";
34 case SnSR::CLOSING: return "CLOSING";
35 case SnSR::TIME_WAIT: return "TIME_WAIT";
36 case SnSR::CLOSE_WAIT: return "CLOSE_WAIT";
37 case SnSR::LAST_ACK: return "LAST_ACK";
38 case SnSR::UDP: return "UDP";
39 default: return "UNKNOWN";
40 }
41}
42
44}
45
46// Single source of truth for every registered SCPI command: full absolute path, handler, and a
47// one-line description used by SYSTem:HELP:HEADers?/SYSTem:HELP:SYNTax?. Each command is
48// registered by its complete path rather than via SetCommandTreeBase — this makes registration
49// hash exactly the way a fully-typed user command hashes at runtime, with no tree-base shortcut
50// left to accidentally collide (this project has hit that bug class twice: CONFigure:MOTor
51// hashing to the parser's "no tree set" sentinel, and a SYStem/SYSTem capitalization collision).
53 const char* path; // full absolute SCPI path, e.g. "CONTRol:ROTation:DIRection#?"
54 SCPI_caller_t handler;
55 const char* description; // shown by SYSTem:HELP:SYNTax?
56};
57
58static const CommandInfo kCommands[] = {
59 {"*IDN?", &Identify,
60 "Returns device identification string.\n"
61 "Returns: \"<manufacturer>, <name>, V<version>, #<serial>\", comma-separated."},
62
63 {"*RST", &Reset,
64 "Performs a full device reset (reboot).\n"
65 "Parameter: none.\n"
66 "Affects: restarts the ESP32 immediately; stored settings in flash are unchanged. Does not "
67 "send a response — the connection drops as the device reboots."},
68
69 {"*OPC?", &OPC,
70 "Signals that all previously queued commands have completed.\n"
71 "Returns: \"1\", always, immediately.\n"
72 "Note: this device processes commands synchronously on a single thread, so reaching this "
73 "command already implies prior commands finished; it does not track any asynchronous operation."},
74
75 {"SYSTem:ERRor?", &GetError,
76 "Returns and dequeues the oldest error from the error queue.\n"
77 "Returns: \"<code>, \\\"<message>\\\"\", or \"0, \\\"No error\\\"\" if the queue is empty.\n"
78 "Note: the queue holds up to 10 entries; if it overflows, newer errors are discarded and a "
79 "queue-overflow error is reported once."},
80
81 {"SYSTem:HELP:HEADers?", &GetHelp,
82 "Lists all available SCPI command headers.\n"
83 "Parameter: none.\n"
84 "Returns: \"#HEADERS <n>\" followed by one header per line."},
85
86 {"SYSTem:HELP:SYNTax?", &GetHelpSyntax,
87 "Returns detailed syntax help for a single command header.\n"
88 "Parameter: string, the exact header as printed by SYSTem:HELP:HEADers? (e.g. \"*IDN?\"). "
89 "Case-insensitive; surrounding quotes are stripped if present.\n"
90 "Returns: a multi-line description of that command, or \"UNKNOWN\" plus a queued error if the "
91 "header isn't recognized."},
92
93 {"SYSTem:VERSion?", &GetVersion,
94 "Returns the device firmware version.\n"
95 "Returns: \"V<version>\", e.g. \"V1.0.0\"."},
96
97 {"SYSTem:DEBUG?", &GetDebug,
98 "Returns internal SCPI parser debug info.\n"
99 "Returns: registered token/command counts and message buffer usage, for firmware development use."},
100
101 {"SYSTem:PRESet", &Preset,
102 "Performs a factory reset.\n"
103 "Parameter: none.\n"
104 "Affects: clears ALL stored preferences in flash (IP, MAC, port, motor current, speed, "
105 "acceleration, calibration, etc. — everything reverts to config.h defaults) and reboots immediately."},
106
107 {"SYSTem:COMMunicate:ETHernet:PORT", &SetPort,
108 "Sets the SCPI TCP listen port.\n"
109 "Parameter: integer, 0-65535.\n"
110 "Affects: stored to flash immediately; takes effect on the next reboot, not the current session."},
111
112 {"SYSTem:COMMunicate:ETHernet:PORT?", &GetPort,
113 "Returns the configured SCPI TCP listen port.\n"
114 "Returns: integer, the port currently in use (as loaded at boot)."},
115
116 {"SYSTem:COMMunicate:ETHernet:ADDRess", &SetIP,
117 "Sets the device's static IP address.\n"
118 "Parameter: string, dotted-quad IPv4 format, e.g. \"192.168.0.55\".\n"
119 "Affects: stored to flash; takes effect on the next Ethernet re-init "
120 "(SYSTem:COMMunicate:ETHernet:RESET) or reboot."},
121
122 {"SYSTem:COMMunicate:ETHernet:ADDRess?", &GetIP,
123 "Returns the device's current live IP address.\n"
124 "Returns: dotted-quad IPv4 string, as currently assigned to the Ethernet interface."},
125
126 {"SYSTem:COMMunicate:ETHernet:MAC", &SetMAC,
127 "Sets the device's MAC address.\n"
128 "Parameter: string, colon-separated hex octets, e.g. \"DE:AD:BE:EF:FE:ED\".\n"
129 "Affects: stored to flash; takes effect on the next Ethernet re-init or reboot."},
130
131 {"SYSTem:COMMunicate:ETHernet:MAC?", &GetMAC,
132 "Returns the device's current MAC address.\n"
133 "Returns: colon-separated hex octets, e.g. \"DE:AD:BE:EF:FE:ED\"."},
134
135 {"SYSTem:COMMunicate:ETHernet:DGATeway?", &GetGW,
136 "Returns the configured default gateway IP address.\n"
137 "Returns: dotted-quad IPv4 string."},
138
139 {"SYSTem:COMMunicate:ETHernet:RESET", &ResetEthernet,
140 "Resets the W5500 Ethernet module and re-initializes the network stack.\n"
141 "Parameter: none.\n"
142 "Affects: toggles the module's hardware reset pin, re-reads IP/MAC from flash, restarts the "
143 "SCPI listener, and drops all currently tracked SCPI sessions (including the one issuing this command)."},
144
145 {"CONTRol:ROTation:STEP#", &Step,
146 "Jogs axis <n> exactly one full mechanical revolution at fixed speed, with no acceleration ramp.\n"
147 "Parameter: <n> is 1 or 2 (axis 1 or 2); no other parameters.\n"
148 "Affects: physically moves the axis; does not update the axis's tracked absolute position. "
149 "Test/development use only, not intended for normal operation."},
150
151 {"CONTRol:ROTation:DIRection#", &SetDirection,
152 "Sets rotation direction of axis <n>.\n"
153 "Parameter: <n> is 1 or 2. Value: \"CW\" or \"CCW\".\n"
154 "Affects: axis <n>'s direction for subsequent moves. On axis 2, an actual direction reversal "
155 "also triggers an automatic backlash-compensation move before the next command; repeating the "
156 "same direction is a no-op."},
157
158 {"CONTRol:ROTation:DIRection#?", &GetDirection,
159 "Queries the current rotation direction of axis <n>.\n"
160 "Parameter: <n> is 1 or 2.\n"
161 "Returns: \"CW\" or \"CCW\"."},
162
163 {"CONTRol:ROTation:RELative#", &SetRelativePosition,
164 "Moves axis <n> by a relative angle.\n"
165 "Parameter: <n> is 1 or 2. Value: integer degrees, 0-360.\n"
166 "Affects: physically moves the axis using the currently configured SPEED and ACCELeration."},
167
168 {"CONTRol:ROTation:ABSolute#", &SetAbsolutePosition,
169 "Moves axis <n> to an absolute angle.\n"
170 "Parameter: <n> is 1 or 2. Value: integer degrees, 0-360.\n"
171 "Affects: physically moves the axis to the target angle using the currently configured SPEED "
172 "and ACCELeration; updates the axis's tracked absolute position."},
173
174 {"CONTRol:ROTation:ABSolute#?", &GetAbsolutePosition,
175 "Queries the current absolute position of axis <n>.\n"
176 "Parameter: <n> is 1 or 2.\n"
177 "Returns: float, degrees — the device's internally tracked position (dead-reckoned from "
178 "commanded steps, not measured by any sensor)."},
179
180 {"CONTRol:ROTation:PERPetual#", &Perpetual,
181 "Runs axis <n> continuously at fixed speed.\n"
182 "Parameter: <n> is 1 or 2; no other parameters.\n"
183 "Affects: physically moves the axis indefinitely. Does not return a response and blocks all "
184 "further command processing (Serial and Ethernet) until the device is reset or powered off. "
185 "Test/development use only, not intended for normal operation."},
186
187 {"CONFigure:MOTor:RMScurrent", &SetRMSCurrent,
188 "Sets the stepper driver RMS run current.\n"
189 "Parameter: integer, 0-2400 (mA).\n"
190 "Affects: both axes simultaneously — there is no per-axis current setting."},
191
192 {"CONFigure:MOTor:RMScurrent?", &GetRMSCurrent,
193 "Queries the actual RMS run current applied by the driver.\n"
194 "Returns: integer, mA — read back from the driver hardware, not just the last requested "
195 "value, so it reflects the driver's real current-scale quantization."},
196
197 {"CONFigure:MOTor:HOLDcurrent", &SetHoldCurrent,
198 "Sets the stepper driver hold current.\n"
199 "Parameter: integer, 0-32 — a raw IHOLD driver scale code (not mA), roughly a fraction /32 of "
200 "the run current.\n"
201 "Affects: both axes simultaneously."},
202
203 {"CONFigure:MOTor:HOLDcurrent?", &GetHoldCurrent,
204 "Queries the actual hold current applied by the driver.\n"
205 "Returns: integer, 0-32 — the raw IHOLD driver scale code currently applied."},
206
207 {"CONFigure:MOTor:MICROsteps", &SetMicrosteps,
208 "Sets the stepper driver microstepping resolution.\n"
209 "Parameter: integer, 1-256 (typically a power of two, e.g. 16).\n"
210 "Affects: both axes simultaneously."},
211
212 {"CONFigure:MOTor:MICROsteps?", &GetMicrosteps,
213 "Queries the configured microstepping resolution.\n"
214 "Returns: integer, microsteps per full step."},
215
216 {"CONFigure:MOTor:SPEED", &SetSpeed,
217 "Sets the target motion speed used by subsequent moves.\n"
218 "Parameter: integer, 0-100000 (steps/s).\n"
219 "Affects: both axes; applied to the next RELative#/ABSolute# move issued after this command, "
220 "not to any move already in progress."},
221
222 {"CONFigure:MOTor:SPEED?", &GetSpeed,
223 "Queries the configured target motion speed.\n"
224 "Returns: integer, steps/s."},
225
226 {"CONFigure:MOTor:ACCELeration", &SetAcceleration,
227 "Sets the motion acceleration used by subsequent moves.\n"
228 "Parameter: integer, 0-10000 (steps/s^2).\n"
229 "Affects: both axes; applied to the next RELative#/ABSolute# move issued after this command."},
230
231 {"CONFigure:MOTor:ACCELeration?", &GetAcceleration,
232 "Queries the configured motion acceleration.\n"
233 "Returns: integer, steps/s^2."},
234
235 // Named "STATus?" rather than "Debug?" deliberately: "Debug" (only the first letter
236 // capitalized) is case-insensitively identical to the unrelated "SYSTem:DEBUG?" token (which is
237 // fully capitalized, so its short and long forms are both "DEBUG"). The parser's long-form
238 // matching is case-insensitive but its token table is case-sensitive, so this command's short
239 // form ("MOT:D?") would silently resolve through SYSTem:DEBUG?'s token instead of its own,
240 // never actually matching what got registered — confirmed via the hash simulation script (same
241 // bug class as the earlier SYStem/SYSTem collision). "STATus?" is also the more conventional
242 // SCPI name for a raw status query.
243 {"CONFigure:MOTor:STATus?", &GetMotorDebug,
244 "Returns raw TMC2209 driver status registers for both axes.\n"
245 "Returns: two lines, \"Motor 1 Status: <binary>\" and \"Motor 2 Status: <binary>\" — raw "
246 "DRV_STATUS-style bits, for diagnostic use."},
247
248 {"DIAGnostic:SESSion:LIST?", &GetSessionList,
249 "Lists live SCPI Ethernet sessions.\n"
250 "Returns: \"#SESSIONS <n>\" then one CSV line per active slot — index, classification "
251 "(SCPI/PING), TCP state, remote IP, remote port, local port, age_ms, idle_ms, rx bytes, tx bytes."},
252
253 {"DIAGnostic:SESSion:LOG?", &GetSessionLog,
254 "Lists recent SCPI session closures.\n"
255 "Returns: \"#CLOSES <n>\" then one CSV line per closure (oldest first, up to the last 16) — "
256 "time since closed, remote IP:port, reason code (EVICTED_BY_NEW/CLIENT_FIN/RST/OTHER), session "
257 "duration, idle time at close, rx/tx bytes."},
258
259 {"DIAGnostic:SOCKet:LIST?", &GetSocketList,
260 "Lists the raw state of all 8 W5500 hardware sockets.\n"
261 "Returns: \"#SOCKETS 8\" then one CSV line per socket — index, TCP state, and whether it's "
262 "currently tracked as an SCPI session (SCPI-TRACKED) or not (UNTRACKED)."},
263
264 {"DIAGnostic:SYSTem?", &GetDiagSystem,
265 "Returns system and reset health diagnostics.\n"
266 "Returns: comma-separated key=value pairs — uptime_s, free_heap, min_free_heap, reset_reason, "
267 "boot_count, link_flaps, eth_reinits."},
268};
269static const int kCommandCount = sizeof(kCommands) / sizeof(kCommands[0]);
270
271// Registers all the SCPI commands that are available for the instrument and assigns them to
272// the corresponding functions. (Functions are located in scpi_commands.h/cpp)
273
275 for (int i = 0; i < kCommandCount; i++) {
276 my_instrument.RegisterCommand(kCommands[i].path, kCommands[i].handler);
277 }
278}
279
281 //Retrieve the IP address from flash memory
282 IPAddress ip;
283 String ipString = flash_memory.preferences.getString("IP-ADDRESS", IP_ADDRESS);
284 ip.fromString(ipString);
285
286 // Retrieve the MAC address from flash memory
287 byte mac[6];
288 flash_memory.preferences.getBytes("MAC-ADDRESS", mac, sizeof(mac));
289
290 // Retrieve the port number from flash memory
291 port = flash_memory.preferences.getInt("SCPI-PORT", SCPI_PORT);
292
293 // Dynamically initialize scpi_server
294 if (scpi_server == nullptr) {
295 scpi_server = new EthernetServer(port);
296 }
297
299 Ethernet.init(33);
300 Ethernet.begin((uint8_t*)mac, ip);
301 scpi_server->begin();
302
303}
304
306 EthernetLinkStatus linkStatus = Ethernet.linkStatus();
307 if (lastLinkStatus == LinkON && linkStatus != LinkON) {
308 linkFlapCount++;
309 }
310 lastLinkStatus = linkStatus;
311
312 if (linkStatus == LinkON) {
313 if (!server_started) {
314 scpi_server->begin();
315 server_started = true;
316 }
317
318 // Always drain the listener every loop, so a probing/second connection is either adopted
319 // into a free session slot or immediately rejected — never left stranding a hardware socket.
320 //
321 // Note: sessions are NOT matched/evicted by remote IP. Behind a router/VPN concentrator that
322 // NATs all client traffic to one apparent source IP, every new connection (a real reconnect or
323 // a health-check probe) would otherwise match and evict the active session by IP alone. A new
324 // TCP connection always gets a fresh ephemeral source port regardless of source IP, so an
325 // IP-based (or even IP+port-based) match against an already-tracked session essentially never
326 // legitimately occurs — stale sessions are instead reclaimed below once .connected() goes false.
327 //
328 // IMPORTANT: must use accept(), not available(). EthernetServer::available() scans every
329 // socket ever bound to this port — including ones already accepted and tracked below — and
330 // returns any of them that currently has unread data, with no regard for whether we already
331 // own it. That silently re-hands back an already-active session as if it were a brand-new
332 // connection any time it has pending bytes, corrupting the slot table (confirmed via field
333 // data: see docs/code-review-scpi-ethernet-sessions.md). accept() marks a socket as claimed
334 // (EthernetServer.cpp: "server_port[i] = 0; // only return the client once") so it is never
335 // handed back again — the correct way to detect only genuinely new connections.
336 EthernetClient newClient = scpi_server->accept();
337 if (newClient) {
338 int freeSlot = -1;
339 for (int i = 0; i < MAX_SCPI_CLIENTS; i++) {
340 if (!scpi_clients[i].active) {
341 freeSlot = i;
342 break;
343 }
344 }
345
346 if (freeSlot == -1) {
347 // At capacity — evict the oldest session to make room for the new connection.
348 freeSlot = 0;
349 for (int i = 1; i < MAX_SCPI_CLIENTS; i++) {
350 if (scpi_clients[i].connectedAt < scpi_clients[freeSlot].connectedAt) {
351 freeSlot = i;
352 }
353 }
354 logSessionClose(scpi_clients[freeSlot], "EVICTED_BY_NEW");
355 scpi_clients[freeSlot].client.stop();
356 }
357
358 scpi_clients[freeSlot].client = newClient;
359 scpi_clients[freeSlot].active = true;
360 scpi_clients[freeSlot].connectedAt = millis();
361 scpi_clients[freeSlot].lastActivity = millis();
362 scpi_clients[freeSlot].rxBytes = 0;
363 scpi_clients[freeSlot].txBytes = 0;
364 scpi_clients[freeSlot].remoteIP = newClient.remoteIP();
365 scpi_clients[freeSlot].remotePort = newClient.remotePort();
366 }
367
368 // Service every active session.
369 for (int i = 0; i < MAX_SCPI_CLIENTS; i++) {
370 if (scpi_clients[i].active) {
371 if (scpi_clients[i].client.connected()) {
373 SessionStream stream(scpi_clients[i]);
374 my_instrument.ProcessInput(stream, "\r\n");
375 } else {
376 // Capture the raw socket state before .stop() forces it to CLOSED, so a clean FIN
377 // (CLOSE_WAIT) can be told apart from an abrupt RST (straight to CLOSED).
378 uint8_t stat = scpi_clients[i].client.status();
379 const char* reason = (stat == SnSR::CLOSE_WAIT) ? "CLIENT_FIN"
380 : (stat == SnSR::CLOSED) ? "RST"
381 : "OTHER";
382 logSessionClose(scpi_clients[i], reason);
383 scpi_clients[i].client.stop();
384 scpi_clients[i].active = false;
385 }
386 }
387 }
388 }
389}
390
391void SCPI::logSessionClose(const SCPIClient& slot, const char* reason) {
392 SessionCloseLog& entry = sessionLog[sessionLogHead];
393 entry.closedAt = millis();
394 entry.remoteIP = slot.remoteIP;
395 entry.remotePort = slot.remotePort;
396 strncpy(entry.reason, reason, sizeof(entry.reason) - 1);
397 entry.reason[sizeof(entry.reason) - 1] = '\0';
398 entry.durationMs = millis() - slot.connectedAt;
399 entry.idleMsAtClose = millis() - slot.lastActivity;
400 entry.rxBytes = slot.rxBytes;
401 entry.txBytes = slot.txBytes;
402 sessionLogHead = (sessionLogHead + 1) % SESSION_LOG_SIZE;
403 if (sessionLogCount < SESSION_LOG_SIZE) sessionLogCount++;
404}
405
407 reinitCount++;
408
409 IPAddress ip;
410 String ipString = flash_memory.preferences.getString("IP-ADDRESS", IP_ADDRESS);
411 ip.fromString(ipString);
412
413 byte mac[6];
414 flash_memory.preferences.getBytes("MAC-ADDRESS", mac, sizeof(mac));
415
416 for (int i = 0; i < MAX_SCPI_CLIENTS; i++) {
417 if (scpi_clients[i].active) {
418 scpi_clients[i].client.stop();
419 scpi_clients[i].active = false;
420 }
421 }
422
423 Ethernet.begin((uint8_t*)mac, ip);
424 server_started = false; // let processEthernet() re-arm the listener next loop
425}
426
428 my_instrument.ProcessInput(Serial, "\n");
429}
430
431void SCPI::PrintDebugInfo(Stream& interface) {
432 my_instrument.PrintDebugInfo(interface);
433}
434
435void SCPI::logError(int code, const char* message) {
436 if (errorCount < ERROR_QUEUE_SIZE) {
437 // Normal case: Add error and move head
438 errorQueue[errorHead].code = code;
439 strncpy(errorQueue[errorHead].message, message, sizeof(errorQueue[errorHead].message) - 1);
440 errorQueue[errorHead].message[sizeof(errorQueue[errorHead].message) - 1] = '\0';
441 errorHead = (errorHead + 1) % ERROR_QUEUE_SIZE;
442 errorCount++;
443 } else {
444 // Overflow handling: Keep the oldest errors, discard the newest
445 queueOverflow = true;
446 }
447}
448
449const char* SCPI::getError() {
450 static char response[100]; // Buffer for formatted output
451
452 if (errorCount == 0) {
453 queueOverflow = false; // Reset overflow flag after queue is cleared
454 return "0, \"No error\"";
455 }
456
457 // If overflow occurred, return the queue overflow error
458 if (queueOverflow) {
459 queueOverflow = false; // Only return overflow once
460 return "-350, \"Queue overflow\"";
461 }
462
463 // Get and remove the oldest error
464 snprintf(response, sizeof(response), "%d, \"%s\"", errorQueue[errorTail].code, errorQueue[errorTail].message);
465 errorTail = (errorTail + 1) % ERROR_QUEUE_SIZE;
466 errorCount--;
467
468 return response;
469}
470
472 return port;
473}
474
475void SCPI::printSessionList(Stream& interface) {
476 int count = 0;
477 for (int i = 0; i < MAX_SCPI_CLIENTS; i++) {
478 if (scpi_clients[i].active) count++;
479 }
480 interface.print(F("#SESSIONS "));
481 interface.print(count);
482 interface.write('\n');
483
484 unsigned long now = millis();
485 for (int i = 0; i < MAX_SCPI_CLIENTS; i++) {
486 if (!scpi_clients[i].active) continue;
487 SCPIClient& s = scpi_clients[i];
488 // Classification heuristic: a slot that has never received a byte is presumed to be a
489 // non-SCPI probe (e.g. a router health check) rather than a real client.
490 const char* classification = (s.rxBytes > 0) ? "SCPI" : "PING";
491 interface.print(i); interface.print(',');
492 interface.print(classification); interface.print(',');
493 interface.print(socketStatusName(s.client.status())); interface.print(',');
494 interface.print(s.remoteIP); interface.print(',');
495 interface.print(s.remotePort); interface.print(',');
496 interface.print(port); interface.print(',');
497 interface.print(F("age=")); interface.print(now - s.connectedAt); interface.print(',');
498 interface.print(F("idle=")); interface.print(now - s.lastActivity); interface.print(',');
499 interface.print(F("rx=")); interface.print(s.rxBytes); interface.print(',');
500 interface.print(F("tx=")); interface.print(s.txBytes);
501 interface.write('\n');
502 }
503}
504
505void SCPI::printSessionLog(Stream& interface) {
506 interface.print(F("#CLOSES "));
507 interface.print(sessionLogCount);
508 interface.write('\n');
509
510 unsigned long now = millis();
511 int start = (sessionLogHead - sessionLogCount + SESSION_LOG_SIZE) % SESSION_LOG_SIZE;
512 for (int n = 0; n < sessionLogCount; n++) {
513 SessionCloseLog& e = sessionLog[(start + n) % SESSION_LOG_SIZE];
514 interface.print(F("ago_ms=")); interface.print(now - e.closedAt); interface.print(',');
515 interface.print(F("remote=")); interface.print(e.remoteIP); interface.print(':'); interface.print(e.remotePort); interface.print(',');
516 interface.print(F("reason=")); interface.print(e.reason); interface.print(',');
517 interface.print(F("duration_ms=")); interface.print(e.durationMs); interface.print(',');
518 interface.print(F("idle_ms=")); interface.print(e.idleMsAtClose); interface.print(',');
519 interface.print(F("rx=")); interface.print(e.rxBytes); interface.print(',');
520 interface.print(F("tx=")); interface.print(e.txBytes);
521 interface.write('\n');
522 }
523}
524
525void SCPI::printSocketList(Stream& interface) {
526 interface.print(F("#SOCKETS "));
527 interface.print(MAX_SOCK_NUM);
528 interface.write('\n');
529
530 for (uint8_t i = 0; i < MAX_SOCK_NUM; i++) {
531 EthernetClient probe(i); // wraps an existing hardware socket index for a read-only status query
532 bool tracked = false;
533 for (int j = 0; j < MAX_SCPI_CLIENTS; j++) {
534 if (scpi_clients[j].active && scpi_clients[j].client.getSocketNumber() == i) {
535 tracked = true;
536 break;
537 }
538 }
539 interface.print(i); interface.print(',');
540 interface.print(socketStatusName(probe.status())); interface.print(',');
541 interface.print(tracked ? F("SCPI-TRACKED") : F("UNTRACKED"));
542 interface.write('\n');
543 }
544}
545
546void SCPI::printSystemDiagnostics(Stream& interface) {
547 interface.print(F("uptime_s=")); interface.print(millis() / 1000);
548 interface.print(F(",free_heap=")); interface.print(ESP.getFreeHeap());
549 interface.print(F(",min_free_heap=")); interface.print(esp_get_minimum_free_heap_size());
550 interface.print(F(",reset_reason=")); interface.print(resetReasonToString(esp_reset_reason()));
551 interface.print(F(",boot_count=")); interface.print(bootCount);
552 interface.print(F(",link_flaps=")); interface.print(linkFlapCount);
553 interface.print(F(",eth_reinits=")); interface.print(reinitCount);
554 interface.write('\n');
555}
556
557void SCPI::printAllHeaders(Stream& interface) {
558 interface.print(F("#HEADERS "));
559 interface.print(kCommandCount);
560 interface.write('\n');
561 for (int i = 0; i < kCommandCount; i++) {
562 interface.print(kCommands[i].path);
563 interface.write('\n');
564 }
565}
566
567void SCPI::printHeaderSyntax(Stream& interface, const char* header) {
568 // Defensively strip a single pair of surrounding double quotes, since some SCPI clients quote
569 // string parameters and the underlying parser doesn't strip them itself.
570 String query(header);
571 query.trim();
572 if (query.length() >= 2 && query[0] == '"' && query[query.length() - 1] == '"') {
573 query = query.substring(1, query.length() - 1);
574 }
575
576 for (int i = 0; i < kCommandCount; i++) {
577 if (query.equalsIgnoreCase(kCommands[i].path)) {
578 interface.print(kCommands[i].description);
579 interface.write('\n');
580 return;
581 }
582 }
583
584 // A query must always produce a response — never leave the client waiting.
585 logError(224, "Illegal parameter value");
586 interface.print(F("UNKNOWN"));
587 interface.write('\n');
588}
This class handles access to the flash memory of the ESP32. It is also used to initialize the flash m...
Definition flash.h:19
Stream wrapper around a tracked SCPIClient's EthernetClient that transparently counts bytes transferr...
Definition scpi.h:163
int errorCount
Tracks number of stored errors.
Definition scpi.h:233
int getPort()
Get the port number of the SCPI server.
Definition scpi.cpp:471
void registerCommands()
Register SCPI commands from scpi_commands.h/cpp.
Definition scpi.cpp:274
void printHeaderSyntax(Stream &interface, const char *header)
Print syntax help for a single command header (SYSTem:HELP:SYNTax?).
Definition scpi.cpp:567
int errorTail
Points to the oldest error.
Definition scpi.h:232
void printSocketList(Stream &interface)
Print the raw state of all 8 W5500 hardware sockets, not just the ones tracked as SCPI sessions (DIAG...
Definition scpi.cpp:525
int errorHead
Points to the next free slot.
Definition scpi.h:231
void printSessionLog(Stream &interface)
Print the session close-reason ring buffer (DIAGnostic:SESSion:LOG?).
Definition scpi.cpp:505
void PrintDebugInfo(Stream &interface)
Print debug information about the SCPI parser to the specified interface.
Definition scpi.cpp:431
void logError(int code, const char *message)
Log an error message to the SCPI error queue, compliant with section 21.8 of the SCPI standard.
Definition scpi.cpp:435
void processEthernet()
Process incoming SCPI commands over Ethernet.
Definition scpi.cpp:305
void printSessionList(Stream &interface)
Print the live SCPI session table (DIAGnostic:SESSion:LIST?).
Definition scpi.cpp:475
const char * getError()
Get the oldest error message from the error queue, compliant with section 21.8 of the SCPI standard.
Definition scpi.cpp:449
void processSerial()
Process incoming SCPI commands over Serial.
Definition scpi.cpp:427
void setup()
Set up the SCPI server and register all available commands.
Definition scpi.cpp:280
void printAllHeaders(Stream &interface)
Print every registered SCPI command header, one per line (SYSTem:HELP:HEADers?).
Definition scpi.cpp:557
SCPI()
Construct a new SCPI object.
Definition scpi.cpp:43
bool queueOverflow
Tracks if queue overflowed.
Definition scpi.h:234
int port
Store SCPI port number at time of initialization.
Definition scpi.h:236
void reinitEthernet()
Re-initialize the Ethernet stack (re-reads IP/MAC from flash, re-begins Ethernet and the SCPI server,...
Definition scpi.cpp:406
void printSystemDiagnostics(Stream &interface)
Print system/reset health diagnostics (DIAGnostic:SYStem?).
Definition scpi.cpp:546
Handles the flash memory for the ESP32.
This file contains the functions and classes related to SCPI communication.
void GetHelpSyntax(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends syntax help for a single command header, given as a parameter (e.g. SYSTem:HELP:SYNTax?...
void GetSocketList(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends the raw state of all W5500 hardware sockets over the given interface.
void GetSessionLog(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends the ring buffer of recent SCPI session closures (with reason) over the given interface.
void GetDebug(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends the debug information from Vrekrer-SCPI-Parser over the given interface.
void Preset(SCPI_C commands, SCPI_P parameters, Stream &interface)
Rewrites the parameters stored in flash memory with the defaults from the config.h file.
void OPC(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends a 1 over the given interface to indicate that the previous command has finished executing.
void Identify(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends an identification message over the given interface.
void GetSessionList(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends the live SCPI Ethernet session table over the given interface.
void GetVersion(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends the current firmware version of the device over the given interface.
void GetDiagSystem(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends system/reset health diagnostics (uptime, heap, reset reason, boot count) over the given interfa...
void GetHelp(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends a help message with all available SCPI commands over the given interface.
void Reset(SCPI_C commands, SCPI_P parameters, Stream &interface)
Triggers a full reset of the ESP32.
void GetError(SCPI_C commands, SCPI_P parameters, Stream &interface)
Sends the oldest error message over the given interface.
This file contains the function declarations for the functions whic are called when a SCPI command is...