/****************************************************************** scpi Parser for reporting data on Loxone Andrej Konkow, 05/2026, v02 ******************************************************************/ #ifdef LOXMOCK #include "loxmock_scpireporter.c" #endif // Flag for debugoutput //#define DEBUGMODE // Maximum values to be addressed for printing by the user // We have 13 numeric channels, native String are not supported use mapping instead #define MAXCHANNELS 13 // Pseudovalue for identfying a "no param" #define STRING_NOPARAM -99999 // Blocksize upper limit for reading response data #define RD_BLOCK_SIZE 200 // Maximum number of individual mappings #define MAX_VALUE_MAPPINGS 15 #define INPUT_TCP_SERVER getinputtext(0) #define INPUT_SCPI_QUERY getinputtext(1) #define INPUT_RESPONSE_WAIT_MS getinput(0) #define INPUT_ITERATION_WAIT_MS getinput(1) /* Recognized datatypes for being printed */ enum scpi_datatype { DT_NONE = 0, DT_INT = 1, DT_FLOAT = 2, DT_STRING = 3 }; /* Holds individual value mappings */ struct scpi_queryMapping { char *charValue; float value; }; /* Structure holding a query specified in a line */ struct scpi_query { int datatype; char *query; float defaultValue; struct scpi_queryMapping mapping[MAX_VALUE_MAPPINGS]; }; // Maps for making efficient decisions static char MapIsEOL[127]; static char MapIsPrimitive[127]; static char MapIsNumber[127]; void initMapIsPrimitive() { int i; memset(MapIsPrimitive, 0, 127*sizeof(char)); for (i='a'; i<='z'; i++) MapIsPrimitive[i] = 1; for (i='A'; i<='Z'; i++) MapIsPrimitive[i] = 1; for (i='0'; i<='9'; i++) MapIsPrimitive[i] = 1; MapIsPrimitive[':'] = 1; MapIsPrimitive['-'] = 1; MapIsPrimitive['+'] = 1; } void initMapIsNumber() { memset(MapIsNumber, 0, 127*sizeof(char)); for (int i='0'; i<='9'; i++) MapIsNumber[i] = 1; MapIsNumber['-'] = 1; } void initMapIsEOL() { memset(MapIsEOL, 0, 127*sizeof(char)); MapIsEOL[0] = 1; // Treat end of String as End Of Line MapIsEOL['\n'] = 1; MapIsEOL['\r'] = 1; MapIsEOL['\t'] = 1; } initMapIsEOL(); initMapIsPrimitive(); initMapIsNumber(); /* Helperfunctions for messaging output */ void messageParam (char *msg, int val) { messageParamTimed (msg, val, 1000); } void messageParamTimed(char *msg, int val, int sleepMs) { messageParamTimedChannel(msg, val, sleepMs, 0); } void messageParamTimedChannel(char *msg, int val, int sleepMs, int channel) { if (val == STRING_NOPARAM) { setoutputtext(channel, msg); } else { char szOutMsg[150]; sprintf(szOutMsg,"%s %d", msg, val); setoutputtext(channel, szOutMsg); } if (sleepMs > 0) sleep(sleepMs); } /* Helperfunction for formatting output-values, printing etc. */ void padValue(char *retValue, int value) { if (value <10) sprintf(retValue, "0%d", value); else sprintf(retValue, "%d", value); } /* Gets the current time and returns it formatted. */ char *getFormattedTime(char *buffer, int time) { char s_hour[3], s_minute[3], s_second[3]; padValue(s_hour, gethour(time, 1)); padValue(s_minute, getminute(time, 1)); padValue(s_second, getsecond(time, 1)); sprintf (buffer, "%s:%s:%s", s_hour, s_minute, s_second); return buffer; } /* Build connection string from an ip address including the port. No errorhandling is done as we assume a correct address */ void buildConnectionString(char *targetaddress, char *connectionString) { char ip[25], port[10]; int colon; colon = strfind(targetaddress,":",0); strncpy(ip, targetaddress, colon); strncpy(port, &targetaddress[colon+1], strlen(targetaddress)-colon-1); sprintf(connectionString,"/dev/tcp/%s/%s", ip, port); } /*********************************************************** Functions for parsing the queries Parse the query to the end of the line and terminate it */ /* Parse a line mapping, for example: "Messung wird gestartet"=1,"Messung wird gestoppt"=2,-1) Returns number of characters parsed */ int parseMapping(char *toParse, struct scpi_query *query) { int mapIdx = 0; char *partStart = toParse; char *partEnd = strstr(toParse, ","); while (partEnd != NULL) { struct scpi_queryMapping *mapping = &query->mapping[mapIdx++]; *partEnd = 0; // Just in case: Skip all leading whitespaces and quotes while ( *partStart == ' ' || *partStart == '"') partStart++; mapping->charValue = partStart; while ( *partStart != '"') partStart++; *partStart = 0; partStart++; while (!MapIsNumber[*partStart]) partStart++; if (query->datatype == DT_INT) mapping->value = atoi(partStart); else // DT_FLOAT and DT_STRING mapping->value = atof(partStart); partStart = partEnd+1; partEnd = strstr(partStart, ","); } while (!MapIsNumber[*partStart]) partStart++; query->defaultValue = atof(partStart); while (!MapIsEOL[*partStart] && *partStart != ')') partStart++; if (!MapIsEOL[*partStart]) partStart++; return partStart-toParse; } int parseLine(char *toParse, struct scpi_query *query) { int idx=0; char *lineEnd; query->query = toParse; for (idx=0; !MapIsEOL[toParse[idx]] ; idx++) { if (toParse[idx] == ' ') { toParse[idx] = 0; continue; } else if (toParse[idx] == '(') { toParse[idx++] = 0; // Limit line to be parsed lineEnd = &toParse[idx]; while (!MapIsEOL[*lineEnd] && *lineEnd != ')') lineEnd++; if (!MapIsEOL[*lineEnd]) *lineEnd = 0; idx += parseMapping(&toParse[idx], query); } } if (toParse[idx] != 0) { toParse[idx] = 0; idx++; } return idx; } /* Map character to datatype */ int getDatatype(char c, char d) { if (d != ':') return DT_NONE; if (c == 'i') return DT_INT; else if (c == 'f') return DT_FLOAT; else if (c == 's') return DT_STRING; return DT_NONE; } /* Get the queried data string and parse it. Returns number of queries */ int parseQuery(char *raw_queries, struct scpi_query *scpi_queries, int size) { int charStartIdx = 0, queryIdx = 0, len = strlen(raw_queries); do { // Just in case: Skip all leading whitespaces while (charStartIdxdatatype = getDatatype(raw_queries[charStartIdx], raw_queries[charStartIdx+1]); charStartIdx+=2; // Skip the datatype, for example: f: charStartIdx += parseLine(&raw_queries[charStartIdx], aQuery); } } while (charStartIdxquery == NULL) continue; i=i; // picoc specific dummy #ifdef LOXMOCK pTcpStream = stream_create(connectionString, 0, 0); #endif if (pTcpStream == NULL) { sprintf(szHlp, "Failed to create stream for %s", server); messageParamTimedChannel(szHlp, STRING_NOPARAM, 3000, 0); break; } memset(streamResponse, 0, RD_BLOCK_SIZE * sizeof(char)); #ifdef DEBUGMODE sprintf(szHlp, "Requesting(incl. newline character): %s", query->query); messageParamTimedChannel(szHlp, STRING_NOPARAM, 3000, 0); #endif // TCP Request/Response. stream_write(pTcpStream, query->query, strlen(query->query)); stream_write(pTcpStream, "\n", 1); // Necessary as request-commit character stream_flush(pTcpStream); messageParamTimedChannel("Reading TCP response with timeout ms:", responseWaitMs, 0, 0); int nBytesReceived = stream_read(pTcpStream, streamResponse, RD_BLOCK_SIZE, responseWaitMs); #ifdef DEBUGMODE sprintf(szHlp, "Received %d bytes: %s", nBytesReceived, streamResponse); messageParamTimedChannel(szHlp, STRING_NOPARAM, 3000, 0); #endif result[i] = query->defaultValue; if (nBytesReceived > 0) { if (query->datatype == DT_INT) result[i] = atoi(streamResponse); else if (query->datatype == DT_FLOAT) result[i] = atof(streamResponse); else if (query->datatype == DT_STRING) { for (j=0; jmapping[j]; if (mapping->charValue == NULL) break; // Check for prefix char *prefix = mapping->charValue; if (strncmp(prefix, streamResponse, strlen(prefix)) == 0) { result[i] = mapping->value; break; } } } } i=i; // picoc specific dummy #ifdef LOXMOCK stream_close(pTcpStream); #endif } i=i; // picoc specific dummy #ifndef LOXMOCK stream_close(pTcpStream); #endif // Now print results at once for (i=0; i