15#include "esp_system.h"
16#include <utility/w5100.h>
19extern FLASH flash_memory;
23extern const char* resetReasonToString(esp_reset_reason_t reason);
24extern RTC_DATA_ATTR uint32_t bootCount;
26static const char* socketStatusName(uint8_t 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";
54 SCPI_caller_t handler;
55 const char* description;
60 "Returns device identification string.\n"
61 "Returns: \"<manufacturer>, <name>, V<version>, #<serial>\", comma-separated."},
64 "Performs a full device reset (reboot).\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."},
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."},
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."},
81 {
"SYSTem:HELP:HEADers?", &
GetHelp,
82 "Lists all available SCPI command headers.\n"
84 "Returns: \"#HEADERS <n>\" followed by one header per line."},
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."},
94 "Returns the device firmware version.\n"
95 "Returns: \"V<version>\", e.g. \"V1.0.0\"."},
98 "Returns internal SCPI parser debug info.\n"
99 "Returns: registered token/command counts and message buffer usage, for firmware development use."},
101 {
"SYSTem:PRESet", &
Preset,
102 "Performs a factory reset.\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."},
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."},
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)."},
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."},
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."},
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."},
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\"."},
135 {
"SYSTem:COMMunicate:ETHernet:DGATeway?", &GetGW,
136 "Returns the configured default gateway IP address.\n"
137 "Returns: dotted-quad IPv4 string."},
139 {
"SYSTem:COMMunicate:ETHernet:RESET", &ResetEthernet,
140 "Resets the W5500 Ethernet module and re-initializes the network stack.\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)."},
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."},
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."},
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\"."},
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."},
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."},
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)."},
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."},
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."},
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."},
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 "
201 "Affects: both axes simultaneously."},
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."},
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."},
212 {
"CONFigure:MOTor:MICROsteps?", &GetMicrosteps,
213 "Queries the configured microstepping resolution.\n"
214 "Returns: integer, microsteps per full step."},
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."},
222 {
"CONFigure:MOTor:SPEED?", &GetSpeed,
223 "Queries the configured target motion speed.\n"
224 "Returns: integer, steps/s."},
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."},
231 {
"CONFigure:MOTor:ACCELeration?", &GetAcceleration,
232 "Queries the configured motion acceleration.\n"
233 "Returns: integer, steps/s^2."},
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."},
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."},
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."},
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)."},
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."},
269static const int kCommandCount =
sizeof(kCommands) /
sizeof(kCommands[0]);
275 for (
int i = 0; i < kCommandCount; i++) {
276 my_instrument.RegisterCommand(kCommands[i].path, kCommands[i].handler);
283 String ipString = flash_memory.preferences.getString(
"IP-ADDRESS", IP_ADDRESS);
284 ip.fromString(ipString);
288 flash_memory.preferences.getBytes(
"MAC-ADDRESS", mac,
sizeof(mac));
291 port = flash_memory.preferences.getInt(
"SCPI-PORT", SCPI_PORT);
294 if (scpi_server ==
nullptr) {
295 scpi_server =
new EthernetServer(
port);
300 Ethernet.begin((uint8_t*)mac, ip);
301 scpi_server->begin();
306 EthernetLinkStatus linkStatus = Ethernet.linkStatus();
307 if (lastLinkStatus == LinkON && linkStatus != LinkON) {
310 lastLinkStatus = linkStatus;
312 if (linkStatus == LinkON) {
313 if (!server_started) {
314 scpi_server->begin();
315 server_started =
true;
336 EthernetClient newClient = scpi_server->accept();
339 for (
int i = 0; i < MAX_SCPI_CLIENTS; i++) {
340 if (!scpi_clients[i].active) {
346 if (freeSlot == -1) {
349 for (
int i = 1; i < MAX_SCPI_CLIENTS; i++) {
350 if (scpi_clients[i].connectedAt < scpi_clients[freeSlot].connectedAt) {
354 logSessionClose(scpi_clients[freeSlot],
"EVICTED_BY_NEW");
355 scpi_clients[freeSlot].client.stop();
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();
369 for (
int i = 0; i < MAX_SCPI_CLIENTS; i++) {
370 if (scpi_clients[i].active) {
371 if (scpi_clients[i].client.connected()) {
374 my_instrument.ProcessInput(stream,
"\r\n");
378 uint8_t stat = scpi_clients[i].client.status();
379 const char* reason = (stat == SnSR::CLOSE_WAIT) ?
"CLIENT_FIN"
380 : (stat == SnSR::CLOSED) ?
"RST"
382 logSessionClose(scpi_clients[i], reason);
383 scpi_clients[i].client.stop();
384 scpi_clients[i].active =
false;
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++;
410 String ipString = flash_memory.preferences.getString(
"IP-ADDRESS", IP_ADDRESS);
411 ip.fromString(ipString);
414 flash_memory.preferences.getBytes(
"MAC-ADDRESS", mac,
sizeof(mac));
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;
423 Ethernet.begin((uint8_t*)mac, ip);
424 server_started =
false;
428 my_instrument.ProcessInput(Serial,
"\n");
432 my_instrument.PrintDebugInfo(interface);
439 strncpy(errorQueue[
errorHead].message, message,
sizeof(errorQueue[
errorHead].message) - 1);
450 static char response[100];
454 return "0, \"No error\"";
460 return "-350, \"Queue overflow\"";
464 snprintf(response,
sizeof(response),
"%d, \"%s\"", errorQueue[
errorTail].code, errorQueue[
errorTail].message);
477 for (
int i = 0; i < MAX_SCPI_CLIENTS; i++) {
478 if (scpi_clients[i].active) count++;
480 interface.print(F(
"#SESSIONS "));
481 interface.print(count);
482 interface.write(
'\n');
484 unsigned long now = millis();
485 for (
int i = 0; i < MAX_SCPI_CLIENTS; i++) {
486 if (!scpi_clients[i].active)
continue;
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');
506 interface.print(F(
"#CLOSES "));
507 interface.print(sessionLogCount);
508 interface.write(
'\n');
510 unsigned long now = millis();
511 int start = (sessionLogHead - sessionLogCount + SESSION_LOG_SIZE) % SESSION_LOG_SIZE;
512 for (
int n = 0; n < sessionLogCount; n++) {
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');
526 interface.print(F(
"#SOCKETS "));
527 interface.print(MAX_SOCK_NUM);
528 interface.write(
'\n');
530 for (uint8_t i = 0; i < MAX_SOCK_NUM; i++) {
531 EthernetClient probe(i);
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) {
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');
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');
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');
570 String query(header);
572 if (query.length() >= 2 && query[0] ==
'"' && query[query.length() - 1] ==
'"') {
573 query = query.substring(1, query.length() - 1);
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');
585 logError(224,
"Illegal parameter value");
586 interface.print(F(
"UNKNOWN"));
587 interface.write(
'\n');
This class handles access to the flash memory of the ESP32. It is also used to initialize the flash m...
Stream wrapper around a tracked SCPIClient's EthernetClient that transparently counts bytes transferr...
int errorCount
Tracks number of stored errors.
int getPort()
Get the port number of the SCPI server.
void registerCommands()
Register SCPI commands from scpi_commands.h/cpp.
void printHeaderSyntax(Stream &interface, const char *header)
Print syntax help for a single command header (SYSTem:HELP:SYNTax?).
int errorTail
Points to the oldest error.
void printSocketList(Stream &interface)
Print the raw state of all 8 W5500 hardware sockets, not just the ones tracked as SCPI sessions (DIAG...
int errorHead
Points to the next free slot.
void printSessionLog(Stream &interface)
Print the session close-reason ring buffer (DIAGnostic:SESSion:LOG?).
void PrintDebugInfo(Stream &interface)
Print debug information about the SCPI parser to the specified interface.
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.
void processEthernet()
Process incoming SCPI commands over Ethernet.
void printSessionList(Stream &interface)
Print the live SCPI session table (DIAGnostic:SESSion:LIST?).
const char * getError()
Get the oldest error message from the error queue, compliant with section 21.8 of the SCPI standard.
void processSerial()
Process incoming SCPI commands over Serial.
void setup()
Set up the SCPI server and register all available commands.
void printAllHeaders(Stream &interface)
Print every registered SCPI command header, one per line (SYSTem:HELP:HEADers?).
SCPI()
Construct a new SCPI object.
bool queueOverflow
Tracks if queue overflowed.
int port
Store SCPI port number at time of initialization.
void reinitEthernet()
Re-initialize the Ethernet stack (re-reads IP/MAC from flash, re-begins Ethernet and the SCPI server,...
void printSystemDiagnostics(Stream &interface)
Print system/reset health diagnostics (DIAGnostic:SYStem?).
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...