#include #include #include #include #include #include #include "scpi_parser.h" #include "scpi_errors.h" #include "scpi_builtins.h" #include "scpi_regs.h" // Config #define MAX_CHARBUF_LEN 64 // Char matching #define INRANGE(c, a, b) ((c) >= (a) && (c) <= (b)) #define IS_WHITESPACE(c) ((c) <= 9 || INRANGE((c), 11, 32)) #define IS_LCASE_CHAR(c) INRANGE((c), 'a', 'z') #define IS_UCASE_CHAR(c) INRANGE((c), 'A', 'Z') #define IS_NUMBER_CHAR(c) INRANGE((c), '0', '9') //#define IS_MULTIPLIER_CHAR(c) ((c) == 'k' || (c) == 'M' || (c) == 'G' || (c) == 'm' || (c) == 'u' || (c) == 'n' || (c) == 'p') // A-Z a-z 0-9 _ #define IS_CHARDATA_CHAR(c) (IS_LCASE_CHAR((c)) || IS_UCASE_CHAR((c)) || IS_NUMBER_CHAR((c)) || (c) == '_') // A-Z a-z 0-9 _ * ? #define IS_IDENT_CHAR(c) (IS_CHARDATA_CHAR((c)) || (c) == '*' || (c) == '?') // 0-9 - + #define IS_INT_CHAR(c) (IS_NUMBER_CHAR((c)) || (c) == '-' || (c) == '+') // 0-9 . + - eE #define IS_FLOAT_CHAR(c) (IS_NUMBER_CHAR((c)) || (c) == '.' || (c) == 'e' || (c) == 'E' || (c) == '+' || (c) == '-') #define CHAR_TO_LOWER(ucase) ((ucase) + 32) #define CHAR_TO_UPPER(lcase) ((lcase) - 32) /** Parser internal state enum */ typedef enum ParserStateEnum { PARS_COMMAND = 0, // collect generic arg, terminated with comma or newline. Leading and trailing whitespace ignored. PARS_ARG, // generic argument (bool, float...) PARS_ARG_STRING, // collect arg - string (special treatment for quotes) PARS_ARG_BLOB_PREAMBLE, // #nDDD PARS_ARG_BLOB_DISCARD, // discard blob - same as BLOB_BODY, but no callback or buffering PARS_ARG_BLOB_BODY, // blob body, callback for each group PARS_TRAILING_WHITE, // command ready to run, waiting for end, only whitespace allowed PARS_TRAILING_WHITE_NOCB, // discard whitespace until end of line, don't run callback on success PARS_DISCARD_LINE, // used after detecting error - drop all chars until \n } parser_state_t; /** Parser internal state struct */ static struct ParserInternalStateStruct { parser_state_t state; // current parser internal state // string buffer, chars collected here until recognized char charbuf[MAX_CHARBUF_LEN + 1]; uint16_t charbuf_i; uint32_t blob_cnt; // preamble counter, if 0, was just #, must read count. Used also for blob body. uint32_t blob_len; // total blob length to read char string_quote; // symbol used to quote string bool string_escape; // last char was backslash, next quote is literal // recognized complete command level strings (FUNCtion) - exact copy from command struct char cur_levels[SCPI_MAX_LEVEL_COUNT][SCPI_MAX_CMD_LEN]; uint8_t cur_level_i; // next free level slot index bool cmdbuf_kept; // set to 1 after semicolon - cur_levels is kept (removed last part) const SCPI_command_t * matched_cmd; // command is put here after recognition, used as reference for args SCPI_argval_t args[SCPI_MAX_PARAM_COUNT]; uint8_t arg_i; // next free argument slot index } pst = {/*EMPTY*/}; // initialized by all zeros // buffer for error messages static char ebuf[100]; // ---------------- PRIVATE PROTOTYPES ------------------ // Command parsing static void pars_cmd_colon(void); // colon starting a command sub-segment static void pars_cmd_space(void); // space ending a command static void pars_cmd_newline(void); // LF static void pars_cmd_semicolon(void); // semicolon right after a command // Command properties (find length of array) static uint8_t cmd_param_count(const SCPI_command_t *cmd); static uint8_t cmd_level_count(const SCPI_command_t *cmd); static bool match_cmd(bool partial); static bool match_any_cmd_from_array(const SCPI_command_t arr[], bool partial); static bool match_cmd_do(const SCPI_command_t *cmd, bool partial); static void run_command_callback(void); // Argument parsing static void pars_arg_char(char c); static void pars_arg_comma(void); static void pars_arg_newline(void); static void pars_arg_semicolon(void); static void pars_blob_preamble_char(uint8_t c); static void arg_convert_value(void); static void charbuf_terminate(void); static void charbuf_append(char c); // Reset static void pars_reset_cmd(void); static void pars_reset_cmd_keeplevel(void); // ------------------- MESSAGE SEND ------------------ /** Send string, no \r\n */ void scpi_send_string_raw(const char *message) { char c; while ((c = *message++) != 0) { scpi_send_byte_impl(c); } } /** Send a message to master. Trailing newline is added. */ void scpi_send_string(const char *message) { scpi_send_string_raw(message); scpi_send_string_raw(scpi_eol); } // ------- Error shortcuts ---------- static void err_no_such_command(void) { char *b = ebuf; for (int i = 0; i < pst.cur_level_i; i++) { if (i > 0) b += sprintf(b, ":"); b += sprintf(b, "%s", pst.cur_levels[i]); } scpi_add_error(E_CMD_UNDEFINED_HEADER, ebuf); } static void err_no_such_command_partial(void) { char *b = ebuf; for (int i = 0; i < pst.cur_level_i; i++) { b += sprintf(b, "%s:", pst.cur_levels[i]); } scpi_add_error(E_CMD_UNDEFINED_HEADER, ebuf); } // ----------------- INPUT PARSING ---------------- void scpi_handle_string(const char* str) { while (*str != 0) { scpi_handle_byte(*str); str++; } } void scpi_handle_byte(const uint8_t b) { const char c = (char) b; switch (pst.state) { case PARS_COMMAND: // Collecting command if (IS_IDENT_CHAR(c)) { // valid command char if (pst.charbuf_i < SCPI_MAX_CMD_LEN) { charbuf_append(c); } else { scpi_add_error(E_CMD_PROGRAM_MNEMONIC_TOO_LONG, NULL); pst.state = PARS_DISCARD_LINE; } } else { // invalid or delimiter if (IS_WHITESPACE(c)) { pars_cmd_space(); // whitespace in command - end of command, start of args (?) break; } switch (c) { case ':': pars_cmd_colon(); // end of a section break; case '\n': // line terminator pars_cmd_newline(); break; case ';': // ends a command, does not reset cmd path. pars_cmd_semicolon(); break; default: sprintf(ebuf, "Unexpected '%c' in command.", c); scpi_add_error(E_CMD_INVALID_CHARACTER, ebuf); pst.state = PARS_DISCARD_LINE; } } break; case PARS_DISCARD_LINE: // drop it. Clear state on newline. if (c == '\r' || c == '\n') { pars_reset_cmd(); } break; case PARS_TRAILING_WHITE: case PARS_TRAILING_WHITE_NOCB: if (IS_WHITESPACE(c)) break; if (c == '\n') { if (pst.state != PARS_TRAILING_WHITE_NOCB) { run_command_callback(); } pars_reset_cmd(); } else { sprintf(ebuf, "Unexpected '%c' in trailing whitespace.", c); scpi_add_error(E_CMD_INVALID_CHARACTER, ebuf); pst.state = PARS_DISCARD_LINE; } break; // whitespace discarded case PARS_ARG: if (IS_WHITESPACE(c)) break; // discard switch (c) { case ',': pars_arg_comma(); break; case '\n': pars_arg_newline(); break; case ';': pars_arg_semicolon(); break; default: pars_arg_char(c); } break; case PARS_ARG_STRING: // string if (c == pst.string_quote && !pst.string_escape) { // end of string pst.state = PARS_ARG; // next will be newline or comma (or ignored spaces) } else if (c == '\n') { scpi_add_error(E_CMD_STRING_DATA_ERROR, "String not terminated (unexpected newline)."); pst.state = PARS_DISCARD_LINE; } else { if (pst.string_escape) { charbuf_append(c); pst.string_escape = false; } else { if (c == '\\') { pst.string_escape = true; } else { charbuf_append(c); } } } break; case PARS_ARG_BLOB_PREAMBLE: // # pars_blob_preamble_char(c); break; case PARS_ARG_BLOB_BODY: // binary blob body with callback on buffer full charbuf_append(c); pst.blob_cnt++; if (pst.charbuf_i >= pst.matched_cmd->blob_chunk) { charbuf_terminate(); if (pst.matched_cmd->blob_callback != NULL) { pst.matched_cmd->blob_callback((uint8_t *)pst.charbuf); } } if (pst.blob_cnt == pst.blob_len) { pst.state = PARS_TRAILING_WHITE_NOCB; // discard trailing whitespace until newline } break; case PARS_ARG_BLOB_DISCARD: // binary blob, discard incoming data pst.blob_cnt++; if (pst.blob_cnt == pst.blob_len) { pst.state = PARS_DISCARD_LINE; } break; } } // ------------------- RESET INTERNAL STATE ------------------ // public // /** Discard the rest of the currently processed blob */ void scpi_discard_blob(void) { if (pst.state == PARS_ARG_BLOB_BODY) { pst.state = PARS_ARG_BLOB_DISCARD; } } /** Reset parser state. */ static void pars_reset_cmd(void) { pst.state = PARS_COMMAND; pst.charbuf_i = 0; pst.cur_level_i = 0; pst.cmdbuf_kept = false; pst.matched_cmd = NULL; pst.arg_i = 0; pst.string_escape = false; } /** Reset parser state, keep level (semicolon) */ static void pars_reset_cmd_keeplevel(void) { pst.state = PARS_COMMAND; pst.charbuf_i = 0; // rewind to last colon if (pst.cur_level_i > 0) { pst.cur_level_i--; // keep prev levels } pst.cmdbuf_kept = true; pst.matched_cmd = NULL; pst.arg_i = 0; pst.string_escape = false; } // ------------------- COMMAND HELPERS ------------------- /** Get param count from command struct */ static uint8_t cmd_param_count(const SCPI_command_t *cmd) { for (uint8_t i = 0; i < SCPI_MAX_PARAM_COUNT; i++) { if (cmd->params[i] == SCPI_DT_NONE) { return i; } } return SCPI_MAX_PARAM_COUNT; } /** Get level count from command struct */ static uint8_t cmd_level_count(const SCPI_command_t *cmd) { for (uint8_t i = 0; i < SCPI_MAX_LEVEL_COUNT; i++) { if (cmd->levels[i][0] == 0) { return i; } } return SCPI_MAX_LEVEL_COUNT; } // ----------------- CHAR BUFFER HELPERS ------------------- /** Add a byte to charbuf, error on overflow */ static void charbuf_append(char c) { if (pst.charbuf_i >= MAX_CHARBUF_LEN) { scpi_add_error(E_DEV_INPUT_BUFFER_OVERRUN, NULL); pst.state = PARS_DISCARD_LINE; } pst.charbuf[pst.charbuf_i++] = c; } /** Terminate charbuf and rewind the pointer to start */ static void charbuf_terminate(void) { pst.charbuf[pst.charbuf_i] = '\0'; pst.charbuf_i = 0; } // ----------------- PARSING COMMANDS --------------- /** Colon received when collecting command parts */ static void pars_cmd_colon(void) { if (pst.charbuf_i == 0) { // No command text before colon if (pst.cur_level_i == 0 || pst.cmdbuf_kept) { // top level command starts with colon (or after semicolon - reset level) pars_reset_cmd(); // clears keep flag } else { // colon after nothing - error scpi_add_error(E_CMD_SYNTAX_ERROR, "Unexpected colon."); pst.state = PARS_DISCARD_LINE; } } else { // internal colon - partial match if (match_cmd(true)) { // ok pst.cmdbuf_kept = false; // drop the flag (needed for rejecting whitespace) } else { // error err_no_such_command_partial(); pst.state = PARS_DISCARD_LINE; } } } /** Semiolon received when collecting command parts */ static void pars_cmd_semicolon(void) { if (pst.cur_level_i == 0 && pst.charbuf_i == 0) { // nothing before semicolon scpi_add_error(E_CMD_SYNTAX_ERROR, "Semicolon not preceded by command."); pars_reset_cmd(); return; } if (match_cmd(false)) { int req_cnt = cmd_param_count(pst.matched_cmd); if (req_cnt == 0) { // no param command - OK run_command_callback(); pars_reset_cmd_keeplevel(); // keep level - that's what semicolon does } else { sprintf(ebuf, "Required %d, got 0.", req_cnt); scpi_add_error(E_CMD_MISSING_PARAMETER, ebuf); pars_reset_cmd(); } } else { err_no_such_command(); pst.state = PARS_DISCARD_LINE; } } /** Newline received when collecting command - end command and execute. */ static void pars_cmd_newline(void) { if (pst.cur_level_i == 0 && pst.charbuf_i == 0) { // nothing before newline pars_reset_cmd(); return; } // complete match if (match_cmd(false)) { int req_cnt = cmd_param_count(pst.matched_cmd); if (req_cnt == 0) { // no param command - OK run_command_callback(); pars_reset_cmd(); } else { // error sprintf(ebuf, "Required %d, got 0.", req_cnt); scpi_add_error(E_CMD_MISSING_PARAMETER, ebuf); pars_reset_cmd(); } } else { err_no_such_command(); pars_reset_cmd(); } } /** Whitespace received when collecting command parts */ static void pars_cmd_space(void) { if ((pst.cmdbuf_kept || pst.cur_level_i == 0) && pst.charbuf_i == 0) { // leading whitespace, ignore return; } if (match_cmd(false)) { if (cmd_param_count(pst.matched_cmd) == 0) { // no commands pst.state = PARS_TRAILING_WHITE; } else { pst.state = PARS_ARG; } } else { // error err_no_such_command(); pst.state = PARS_DISCARD_LINE; } } /** Check if chars equal, ignore case */ static bool char_equals_ci(char a, char b) { if (IS_LCASE_CHAR(a)) { if (IS_LCASE_CHAR(b)) { return a == b; } else if (IS_UCASE_CHAR(b)) { return a == CHAR_TO_LOWER(b); } else { return false; } } else if (IS_UCASE_CHAR(a)) { if (IS_UCASE_CHAR(b)) { return a == b; } else if (IS_LCASE_CHAR(b)) { return a == CHAR_TO_UPPER(b); } else { return false; } } else { return a == b; // exact match, not letters } } /** Check if command matches a pattern */ static bool level_str_matches(const char *test, const char *pattern) { const uint8_t testlen = strlen(test); uint8_t pat_i, tst_i; bool long_started = false; for (pat_i = 0, tst_i = 0; pat_i < strlen(pattern); pat_i++) { if (tst_i > testlen) return false; // not match const char pat_c = pattern[pat_i]; const char tst_c = test[tst_i]; // may be at the \0 terminator if (IS_LCASE_CHAR(pat_c)) { // optional char if (char_equals_ci(pat_c, tst_c)) { tst_i++; // advance test string long_started = true; } else { if (long_started) return false; // once long variant started, it must be completed. } continue; // next pi - tc stays in place } else { // require exact match (case insensitive) if (char_equals_ci(pat_c, tst_c)) { tst_i++; } else { return false; } } } return (tst_i >= testlen); } /** * Match content of the charbuf to a command. * @param partial - match also parts of a command (until a colon) */ static bool match_cmd(bool partial) { charbuf_terminate(); // zero-end and rewind index // copy to level table char *dest = pst.cur_levels[pst.cur_level_i++]; strcpy(dest, pst.charbuf); // User commands are checked first, can override builtin commands if (match_any_cmd_from_array(scpi_commands, partial)) { return true; } // Try the built-in commands return match_any_cmd_from_array(scpi_commands_builtin, partial); } static bool match_any_cmd_from_array(const SCPI_command_t arr[], bool partial) { for (uint16_t i = 0; i < 0xFFFF; i++) { const SCPI_command_t *cmd = &arr[i]; if (cmd->levels[0][0] == 0) break; // end marker if (cmd_level_count(cmd) > SCPI_MAX_LEVEL_COUNT) { // FAIL, too deep. Bad config continue; } if (match_cmd_do(cmd, partial)) { if (partial) { // match found, OK return true; } else { // exact match found pst.matched_cmd = cmd; return true; } } } return false; } /** Try to match current state to a given command */ static bool match_cmd_do(const SCPI_command_t *cmd, bool partial) { const uint8_t level_cnt = cmd_level_count(cmd); if (pst.cur_level_i > level_cnt) return false; // command too short if (pst.cur_level_i == 0) return false; // nothing to match if (partial) { if (pst.cur_level_i == level_cnt) { return false; // would be exact match } } else { if (pst.cur_level_i != level_cnt) { return false; // can be only partial match } } // check for match up to current index for (uint8_t j = 0; j < pst.cur_level_i; j++) { if (!level_str_matches(pst.cur_levels[j], cmd->levels[j])) { return false; } } return true; } /** Run the matched command's callback with the arguments */ static void run_command_callback(void) { if (pst.matched_cmd != NULL) { pst.matched_cmd->callback(pst.args); // run } } // ---------------------- PARSING ARGS -------------------------- /** Non-whitespace and non-comma char received in arg. */ static void pars_arg_char(char c) { switch (pst.matched_cmd->params[pst.arg_i]) { case SCPI_DT_FLOAT: if (!IS_FLOAT_CHAR(c)) { sprintf(ebuf, "'%c' not allowed in FLOAT.", c); scpi_add_error(E_CMD_INVALID_CHARACTER_IN_NUMBER, ebuf); pst.state = PARS_DISCARD_LINE; } else { charbuf_append(c); } break; case SCPI_DT_INT: if (!IS_INT_CHAR(c)) { sprintf(ebuf, "'%c' not allowed in INT.", c); scpi_add_error(E_CMD_INVALID_CHARACTER_IN_NUMBER, ebuf); pst.state = PARS_DISCARD_LINE; } else { charbuf_append(c); } break; case SCPI_DT_CHARDATA: if (!IS_CHARDATA_CHAR(c)) { sprintf(ebuf, "'%c' not allowed in CHARDATA.", c); scpi_add_error(E_CMD_INVALID_CHARACTER_DATA, ebuf); pst.state = PARS_DISCARD_LINE; } else { charbuf_append(c); } break; case SCPI_DT_STRING: if (c == '\'' || c == '"') { pst.state = PARS_ARG_STRING; pst.string_quote = c; pst.string_escape = false; } else { scpi_add_error(E_CMD_INVALID_STRING_DATA, "Invalid quote, or chars after string."); pst.state = PARS_DISCARD_LINE; } break; case SCPI_DT_BLOB: if (c == '#') { pst.state = PARS_ARG_BLOB_PREAMBLE; pst.blob_cnt = 0; } else { scpi_add_error(E_CMD_INVALID_BLOCK_DATA, "Block data must start with #"); pst.state = PARS_DISCARD_LINE; } break; default: charbuf_append(c); break; } } /** Received a comma while collecting an arg */ static void pars_arg_comma(void) { if (pst.arg_i == cmd_param_count(pst.matched_cmd) - 1) { // it was the last argument scpi_add_error(E_CMD_UNEXPECTED_NUMBER_OF_PARAMETERS, "Comma after last argument."); pst.state = PARS_DISCARD_LINE; return; } if (pst.charbuf_i == 0) { scpi_add_error(E_CMD_SYNTAX_ERROR, "Missing command before comma."); pst.state = PARS_DISCARD_LINE; return; } // Convert to the right type arg_convert_value(); } // line ended with \n or ; static void pars_arg_eol_do(bool keep_levels) { int req_cnt = cmd_param_count(pst.matched_cmd); if (pst.arg_i + (pst.charbuf_i ? 1 : 0) < req_cnt) { // not the last arg yet - fail if (pst.charbuf_i > 0) pst.arg_i++; // acknowledge the last arg sprintf(ebuf, "Required %d arg, got %d.", req_cnt, pst.arg_i); scpi_add_error(E_CMD_MISSING_PARAMETER, ebuf); pst.state = PARS_DISCARD_LINE; return; } arg_convert_value(); run_command_callback(); if (keep_levels) { pars_reset_cmd_keeplevel(); } else { pars_reset_cmd(); // start a new command } } static void pars_arg_newline(void) { pars_arg_eol_do(false); } static void pars_arg_semicolon(void) { pars_arg_eol_do(true); } /** Convert BOOL, FLOAT or INT char to arg type and advance to next */ static void arg_convert_value(void) { charbuf_terminate(); SCPI_argval_t *dest = &pst.args[pst.arg_i]; int j; switch (pst.matched_cmd->params[pst.arg_i]) { case SCPI_DT_BOOL: if (strcasecmp(pst.charbuf, "1") == 0) { dest->BOOL = 1; } else if (strcasecmp(pst.charbuf, "0") == 0) { dest->BOOL = 0; } else if (strcasecmp(pst.charbuf, "ON") == 0) { dest->BOOL = 1; } else if (strcasecmp(pst.charbuf, "OFF") == 0) { dest->BOOL = 0; } else { sprintf(ebuf, "Invalid BOOL value: '%s'", pst.charbuf); scpi_add_error(E_CMD_NUMERIC_DATA_ERROR, ebuf); pst.state = PARS_DISCARD_LINE; } break; case SCPI_DT_FLOAT: j = sscanf(pst.charbuf, "%f", &dest->FLOAT); if (j == 0 || pst.charbuf[0] == '\0') { //fail or empty buffer sprintf(ebuf, "Invalid FLOAT value: '%s'", pst.charbuf); scpi_add_error(E_CMD_NUMERIC_DATA_ERROR, ebuf); pst.state = PARS_DISCARD_LINE; } break; case SCPI_DT_INT: j = sscanf(pst.charbuf, "%" SCNu32, &dest->INT); if (j == 0 || pst.charbuf[0] == '\0') { //fail or empty buffer sprintf(ebuf, "Invalid INT value: '%s'", pst.charbuf); scpi_add_error(E_CMD_NUMERIC_DATA_ERROR, ebuf); pst.state = PARS_DISCARD_LINE; } break; case SCPI_DT_STRING: if (strlen(pst.charbuf) > SCPI_MAX_STRING_LEN) { scpi_add_error(E_CMD_STRING_DATA_ERROR, "String too long."); pst.state = PARS_DISCARD_LINE; } else { strcpy(dest->STRING, pst.charbuf); // copy the string } break; case SCPI_DT_CHARDATA: if (strlen(pst.charbuf) > SCPI_MAX_STRING_LEN) { // using common buffer scpi_add_error(E_CMD_CHARACTER_DATA_TOO_LONG, NULL); pst.state = PARS_DISCARD_LINE; } else { strcpy(dest->CHARDATA, pst.charbuf); // copy the character data text } break; default: // impossible scpi_add_error(E_DEV_SYSTEM_ERROR, "Unexpected argument data type."); pst.state = PARS_DISCARD_LINE; } // proceed to next argument pst.arg_i++; } static void pars_blob_preamble_char(uint8_t c) { if (pst.blob_cnt == 0) { if (!INRANGE(c, '1', '9')) { sprintf(ebuf, "Unexpected '%c' in binary data preamble.", c); scpi_add_error(E_CMD_BLOCK_DATA_ERROR, ebuf); pst.state = PARS_DISCARD_LINE;// (but not enough to remove the blob containing \n) return; } pst.blob_cnt = c - '0'; // 1-9 } else { if (c == '\n') { scpi_add_error(E_CMD_BLOCK_DATA_ERROR, "Unexpected newline in binary data preamble."); pars_reset_cmd(); return; } if (!IS_NUMBER_CHAR(c)) { sprintf(ebuf, "Unexpected '%c' in binary data preamble.", c); scpi_add_error(E_CMD_BLOCK_DATA_ERROR, ebuf); pst.state = PARS_DISCARD_LINE; return; } charbuf_append(c); if (--pst.blob_cnt == 0) { // end of preamble sequence charbuf_terminate(); sscanf(pst.charbuf, "%" SCNu32, &pst.blob_len); pst.args[pst.arg_i].BLOB_LEN = pst.blob_len; run_command_callback(); // Call handler, enter special blob mode pst.state = PARS_ARG_BLOB_BODY; pst.blob_cnt = 0; } } }