From c58b71b899683a0e3be1b8ce078edbb33c9c558c Mon Sep 17 00:00:00 2001 From: Mihai Renea Date: Fri, 12 Jan 2024 16:52:06 +0100 Subject: [PATCH] drivers/at: parse +CME/+CMS responses and save error value --- drivers/at/at.c | 330 +++++++++++------- drivers/include/at.h | 133 +++++-- tests/drivers/at/Makefile | 9 +- tests/drivers/at/main.c | 84 ++++- .../{sneaky_urc.py => emulate_dce.py} | 33 +- .../drivers/at/tests-with-config/native_stdin | 2 + 6 files changed, 429 insertions(+), 162 deletions(-) rename tests/drivers/at/tests-with-config/{sneaky_urc.py => emulate_dce.py} (67%) create mode 100644 tests/drivers/at/tests-with-config/native_stdin diff --git a/drivers/at/at.c b/drivers/at/at.c index 1d1f522cfc..33d16d027d 100644 --- a/drivers/at/at.c +++ b/drivers/at/at.c @@ -41,6 +41,7 @@ #include #include +#include #include #include "at.h" @@ -77,6 +78,18 @@ static void _event_process_urc(event_t *_event) } #endif +static ssize_t at_readline_skip_empty_stop_at_str(at_dev_t *dev, char *resp_buf, + size_t len, bool keep_eol, + char const *substr, uint32_t timeout); +static size_t at_readline_stop_at_str(at_dev_t *dev, char *resp_buf, size_t len, + bool keep_eol, char const *substr, + uint32_t timeout); + +static inline bool starts_with(char const *str, char const *prefix) +{ + return strncmp(str, prefix, strlen(prefix)) == 0; +} + static void _isrpipe_write_one_wrapper(void *_dev, uint8_t data) { at_dev_t *dev = (at_dev_t *) _dev; @@ -88,18 +101,50 @@ static void _isrpipe_write_one_wrapper(void *_dev, uint8_t data) #endif } -int at_dev_init(at_dev_t *dev, uart_t uart, uint32_t baudrate, char *buf, size_t bufsize) +int at_dev_init(at_dev_t *dev, at_dev_init_t const *init) { - dev->uart = uart; + dev->uart = init->uart; + assert(init->rp_buf_size >= 16); + dev->rp_buf = init->rp_buf; + dev->rp_buf_size = init->rp_buf_size; #if IS_USED(MODULE_AT_URC_ISR) dev->awaiting_response = false; dev->event.handler = _event_process_urc; #endif - isrpipe_init(&dev->isrpipe, (uint8_t *)buf, bufsize); + isrpipe_init(&dev->isrpipe, (uint8_t *)init->rx_buf, init->rx_buf_size); - return uart_init(uart, baudrate, _isrpipe_write_one_wrapper, dev); + return uart_init(init->uart, init->baudrate, _isrpipe_write_one_wrapper, dev); +} + +int at_parse_resp(at_dev_t *dev, char const *resp) +{ + if (*resp == '\0') { + return 1; + } + if (starts_with(resp, CONFIG_AT_RECV_OK)) { + dev->rp_buf[0] = '\0'; + return 0; + } + if (starts_with(resp, CONFIG_AT_RECV_ERROR)) { + return -1; + } + /* A specific command may return either CME or CMS, we need not differentiate */ + if (!starts_with(resp, "+CME ERROR: ") && + !starts_with(resp, "+CMS ERROR: ")) { + /* neither `OK` nor error, must be a response or URC */ + return 1; + } + + resp += strlen("+CMx ERROR: "); + size_t resp_len = strlen(resp); + if (resp_len + 1 > dev->rp_buf_size) { + return -ENOBUFS; + } + /* dev->rp_buf and resp may overlap */ + memmove(dev->rp_buf, resp, resp_len + 1); + return -AT_ERR_EXTENDED; } int at_expect_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout) @@ -224,8 +269,7 @@ int at_send_cmd(at_dev_t *dev, const char *command, uint32_t timeout) return -1; } - if (at_expect_bytes(dev, CONFIG_AT_SEND_EOL AT_RECV_EOL_1 AT_RECV_EOL_2, - timeout)) { + if (at_expect_bytes(dev, CONFIG_AT_SEND_EOL, timeout)) { return -2; } } @@ -275,53 +319,64 @@ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const c { ssize_t res; ssize_t res_ok; - char ok_buf[64]; at_drain(dev); res = at_send_cmd(dev, command, timeout); if (res) { - goto out; + return res; } /* URCs may occur right after the command has been sent and before the * expected response */ - do { - res = at_readline_skip_empty(dev, resp_buf, len, false, timeout); - + while ((res = at_readline_skip_empty(dev, resp_buf, len, false, timeout)) >= 0) { + if (!resp_prefix || *resp_prefix == '\0') { + break; + } /* Strip the expected prefix */ - if (res > 0 && resp_prefix && *resp_prefix) { - size_t prefix_len = strlen(resp_prefix); - if (strncmp(resp_buf, resp_prefix, prefix_len) == 0) { - size_t remaining_len = strlen(resp_buf) - prefix_len; - /* The one extra byte in the copy is the terminating nul byte */ - memmove(resp_buf, resp_buf + prefix_len, remaining_len + 1); - res -= prefix_len; - break; - } + size_t prefix_len = strlen(resp_prefix); + if (starts_with(resp_buf, resp_prefix)) { + size_t remaining_len = strlen(resp_buf) - prefix_len; + /* The one extra byte in the copy is the terminating nul byte */ + memmove(resp_buf, resp_buf + prefix_len, remaining_len + 1); + res -= prefix_len; + break; } - } while (res >= 0); - - /* wait for OK */ - if (res >= 0) { - res_ok = at_readline_skip_empty(dev, ok_buf, sizeof(ok_buf), false, timeout); - if (res_ok < 0) { - return -1; + res = at_parse_resp(dev, resp_buf); + if (res == 0) { + /* empty response */ + return 0; } - ssize_t len_ok = sizeof(CONFIG_AT_RECV_OK) - 1; - if ((len_ok != 0) && (strcmp(ok_buf, CONFIG_AT_RECV_OK) == 0)) { + if (res < 0) { + return res; } +#if IS_USED(MODULE_AT_URC) else { - /* Something else than OK */ - res = -1; + clist_foreach(&dev->urc_list, _check_urc, resp_buf); } +#endif } -out: - return res; + if (res < 0) { + return res; + } + /* wait for OK */ + res_ok = at_readline_skip_empty(dev, dev->rp_buf, dev->rp_buf_size, false, timeout); + if (res_ok < 0) { + return -1; + } + res_ok = at_parse_resp(dev, dev->rp_buf); + if (res_ok == 0) { + return res; + } + if (res_ok < 0) { + return res_ok; + } + /* Neither OK nor error, go figure... */ + return -1; } -ssize_t at_send_cmd_get_lines(at_dev_t *dev, const char *command, - char *resp_buf, size_t len, bool keep_eol, uint32_t timeout) +ssize_t at_send_cmd_get_lines(at_dev_t *dev, const char *command, char *resp_buf, + size_t len, bool keep_eol, uint32_t timeout) { const char eol[] = AT_RECV_EOL_1 AT_RECV_EOL_2; assert(sizeof(eol) > 1); @@ -334,123 +389,113 @@ ssize_t at_send_cmd_get_lines(at_dev_t *dev, const char *command, res = at_send_cmd(dev, command, timeout); if (res) { - goto out; + return res; } memset(resp_buf, '\0', len); - while (1) { - res = at_readline(dev, pos, bytes_left, keep_eol, timeout); + bool first_line = true; + while (bytes_left) { + if (first_line) { + res = at_readline_skip_empty(dev, pos, bytes_left, keep_eol, timeout); + first_line = false; + } else { + /* keep subsequent empty lines, for whatever reason */ + res = at_readline(dev, pos, bytes_left, keep_eol, timeout); + } if (res == 0) { - if (bytes_left) { - *pos++ = eol[sizeof(eol) - 2]; - bytes_left--; - } + *pos++ = eol[sizeof(eol) - 2]; + bytes_left--; continue; } else if (res > 0) { - bytes_left -= res; - size_t len_ok = sizeof(CONFIG_AT_RECV_OK) - 1; - size_t len_error = sizeof(CONFIG_AT_RECV_ERROR) - 1; - if (((size_t )res == (len_ok + keep_eol)) && - (len_ok != 0) && - (strncmp(pos, CONFIG_AT_RECV_OK, len_ok) == 0)) { + size_t const res_len = res; + bytes_left -= res_len; + res = at_parse_resp(dev, pos); + + switch (res) { + case 0: /* OK */ res = len - bytes_left; - break; - } - else if (((size_t )res == (len_error + keep_eol)) && - (len_error != 0) && - (strncmp(pos, CONFIG_AT_RECV_ERROR, len_error) == 0)) { - return -1; - } - else if (strncmp(pos, "+CME ERROR:", 11) == 0) { - return -2; - } - else if (strncmp(pos, "+CMS ERROR:", 11) == 0) { - return -2; - } - else { - pos += res; - if (bytes_left) { - *pos++ = eol[sizeof(eol) - 2]; - bytes_left--; - } - else { - return -1; + return res; + case 1: /* response or URC */ + pos += res_len; + if (bytes_left == 0) { + return -ENOBUFS; } + *pos++ = eol[sizeof(eol) - 2]; + bytes_left--; + continue; + default: /* <0 */ + return res; } } else { - break; + return res; } } -out: - return res; + return -ENOBUFS; } int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout) { - unsigned cmdlen = strlen(command); - at_drain(dev); - uart_write(dev->uart, (const uint8_t *)command, cmdlen); - uart_write(dev->uart, (const uint8_t *)CONFIG_AT_SEND_EOL, AT_SEND_EOL_LEN); - - if (!IS_ACTIVE(CONFIG_AT_SEND_SKIP_ECHO)) { - if (at_wait_bytes(dev, command, timeout)) { - return -1; - } - if (at_expect_bytes(dev, CONFIG_AT_SEND_EOL, timeout)) { - return -2; - } + int res = at_send_cmd(dev, command, timeout); + if (res) { + return res; } - return at_wait_bytes(dev, ">", timeout); + do { + res = at_readline_skip_empty_stop_at_str(dev, dev->rp_buf, dev->rp_buf_size, + false, ">", timeout); + if (res < 0) { + break; + } + if (strstr(dev->rp_buf, ">")) { + return 0; + } + res = at_parse_resp(dev, dev->rp_buf); +#ifdef MODULE_AT_URC + if (res == 1) { + clist_foreach(&dev->urc_list, _check_urc, dev->rp_buf); + } +#endif + } while (res >= 0); + + return res; } int at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout) { int res; - char resp_buf[64]; - res = at_send_cmd_get_resp(dev, command, resp_buf, sizeof(resp_buf), - timeout); - - size_t const len_ok = sizeof(CONFIG_AT_RECV_OK) - 1; - size_t const len_err = sizeof(CONFIG_AT_RECV_ERROR) - 1; - size_t const len_cme_cms = sizeof("+CME ERROR:") - 1; + res = at_send_cmd_get_resp(dev, command, dev->rp_buf, dev->rp_buf_size, timeout); while (res >= 0) { - if (strncmp(resp_buf, CONFIG_AT_RECV_OK, len_ok) == 0) { - return 0; + res = at_parse_resp(dev, dev->rp_buf); + if (res < 1) { + return res; } - else if (strncmp(resp_buf, CONFIG_AT_RECV_ERROR, len_err) == 0) { - return -1; - } - else if (strncmp(resp_buf, "+CME ERROR:", len_cme_cms) == 0) { - return -2; - } - else if (strncmp(resp_buf, "+CMS ERROR:", len_cme_cms) == 0) { - return -2; - } - /* probably a sneaky URC */ #ifdef MODULE_AT_URC - clist_foreach(&dev->urc_list, _check_urc, resp_buf); + clist_foreach(&dev->urc_list, _check_urc, dev->rp_buf); #endif - res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, timeout); + res = at_readline_skip_empty(dev, dev->rp_buf, dev->rp_buf_size, false, timeout); } return res; } -ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, uint32_t timeout) +/* Used to detect a substring that may happen before the EOL. For example, + * Ublox LTE modules don't add EOL after the prompt character `>`. */ +static size_t at_readline_stop_at_str(at_dev_t *dev, char *resp_buf, size_t len, + bool keep_eol, char const *substr, + uint32_t timeout) { const char eol[] = AT_RECV_EOL_1 AT_RECV_EOL_2; assert(sizeof(eol) > 1); - ssize_t res = -1; + ssize_t res = 0; char *resp_pos = resp_buf; #if IS_USED(MODULE_AT_URC_ISR) @@ -458,8 +503,15 @@ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, ui #endif memset(resp_buf, 0, len); - - while (len) { + size_t substr_len = 0; + if (substr) { + substr_len = strlen(substr); + if (substr_len == 0) { + return -EINVAL; + } + } + char const *substr_p = resp_buf; + while (len > 1) { int read_res; if ((read_res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)resp_pos, 1, timeout)) == 1) { @@ -473,12 +525,19 @@ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, ui } if (*resp_pos == eol[sizeof(eol) - 2]) { *resp_pos = '\0'; - res = resp_pos - resp_buf; - goto out; + break; } resp_pos += read_res; len -= read_res; + + if (substr && (size_t)(resp_pos - resp_buf) >= substr_len) { + if (strncmp(substr_p, substr, substr_len) == 0) { + break; + } else { + substr_p++; + } + } } else if (read_res == -ETIMEDOUT) { res = -ETIMEDOUT; @@ -486,28 +545,59 @@ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, ui } } -out: #if IS_USED(MODULE_AT_URC_ISR) dev->awaiting_response = false; #endif if (res < 0) { *resp_buf = '\0'; + } else { + res = resp_pos - resp_buf; } return res; } +ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, + uint32_t timeout) +{ + return at_readline_stop_at_str(dev, resp_buf, len, keep_eol, NULL, timeout); +} + +static ssize_t at_readline_skip_empty_stop_at_str(at_dev_t *dev, char *resp_buf, + size_t len, bool keep_eol, + char const *substr, uint32_t timeout) +{ + ssize_t res = at_readline_stop_at_str(dev, resp_buf, len, keep_eol, substr, timeout); + if (res == 0) { + /* skip possible empty line */ + res = at_readline_stop_at_str(dev, resp_buf, len, keep_eol, substr, timeout); + } + return res; + +} ssize_t at_readline_skip_empty(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, uint32_t timeout) { - ssize_t res = at_readline(dev, resp_buf, len, keep_eol, timeout); - if (res == 0) { - /* skip possible empty line */ - res = at_readline(dev, resp_buf, len, keep_eol, timeout); - } - return res; + return at_readline_skip_empty_stop_at_str(dev, resp_buf, len, keep_eol, NULL, timeout); } +int at_wait_ok(at_dev_t *dev, uint32_t timeout) +{ + while (1) { + ssize_t res = at_readline_skip_empty(dev, dev->rp_buf, dev->rp_buf_size, + false, timeout); + if (res < 0) { + return res; + } + res = at_parse_resp(dev, dev->rp_buf); + if (res < 1) { + return res; + } +#ifdef MODULE_AT_URC + clist_foreach(&dev->urc_list, _check_urc, dev->rp_buf); +#endif + } +} #ifdef MODULE_AT_URC void at_add_urc(at_dev_t *dev, at_urc_t *urc) { @@ -531,7 +621,7 @@ static int _check_urc(clist_node_t *node, void *arg) DEBUG("Trying to match with %s\n", urc->code); - if (strncmp(buf, urc->code, strlen(urc->code)) == 0) { + if (starts_with(buf, urc->code)) { urc->cb(urc->arg, buf); return 1; } diff --git a/drivers/include/at.h b/drivers/include/at.h index 4b0133ca47..a749e31c35 100644 --- a/drivers/include/at.h +++ b/drivers/include/at.h @@ -17,10 +17,10 @@ * intended to send, and bail out if there's no match. * * Furthermore, the library tries to cope with difficulties regarding different - * line endings. It usually sends "", but expects - * "\LF\CR" as echo. + * line endings. It usually sends ``, but expects + * `\LF\CR` as echo. * - * As a debugging aid, when compiled with "-DAT_PRINT_INCOMING=1", every input + * As a debugging aid, when compiled with `-DAT_PRINT_INCOMING=1`, every input * byte gets printed. * * ## Unsolicited Result Codes (URC) ## @@ -44,6 +44,24 @@ * character is detected and there is no pending response. This works by posting * an @ref sys_event "event" to an event thread that processes the URCs. * + * ## Error reporting ## + * Most DCEs (Data Circuit-terminating Equipment, aka modem) can return extra error + * information instead of the rather opaque "ERROR" message. They have the form: + * - `+CMS ERROR: err_code>` for SMS-related commands + * - `+CME ERROR: ` for other commands + * + * For `+CME`, this behavior is usually off by default and can be toggled with: + * `AT+CMEE=` + * where `` may be: + * - 0 disable extended error reporting, return `ERROR` + * - 1 enable extended error reporting, with `` integer + * - 2 enable extended error reporting, with `` as string + * Check your DCE's manual for more information. + * + * Some of the API calls below support detailed error reporting. Whenever they + * detect extended error responses, -AT_ERR_EXTENDED is returned and `` + * can be retrieved by calling @ref at_get_err_info(). + * * @{ * * @file @@ -168,6 +186,9 @@ typedef struct { #endif /* MODULE_AT_URC */ +/** Error cause can be further investigated. */ +#define AT_ERR_EXTENDED 200 + /** Shortcut for getting send end of line length */ #define AT_SEND_EOL_LEN (sizeof(CONFIG_AT_SEND_EOL) - 1) @@ -177,6 +198,8 @@ typedef struct { typedef struct { isrpipe_t isrpipe; /**< isrpipe used for getting data from uart */ uart_t uart; /**< UART device where the AT device is attached */ + char *rp_buf; /**< response parsing buffer */ + size_t rp_buf_size; /**< response parsing buffer size */ #ifdef MODULE_AT_URC clist_node_t urc_list; /**< list to keep track of all registered urc's */ #ifdef MODULE_AT_URC_ISR @@ -186,19 +209,49 @@ typedef struct { #endif } at_dev_t; +/** + * @brief AT device initialization parameters +*/ +typedef struct { + uart_t uart; /**< UART device where the AT device is attached */ + uint32_t baudrate; /**< UART device baudrate */ + char *rx_buf; /**< UART rx buffer */ + size_t rx_buf_size; /**< UART rx buffer size */ + /** + * Response parsing buffer - used for classifying DCE responses and holding + * detailed error information. Must be at least 16 bytes. + * If you don't care about URCs (MODULE_AT_URC is undefined) this must only + * be large enough to hold responses like `OK`, `ERROR` or `+CME ERROR: `. + * Otherwise adapt its size to the maximum length of the URCs you are expecting + * and actually care about. */ + char *rp_buf; + size_t rp_buf_size; /**< response parsing buffer size */ +} at_dev_init_t; + +/** + * @brief Get extended error information of the last command sent. + * + * If a previous at_* method returned with -AT_ERR_EXTENDED, you can retrieve + * a pointer to the error string with this. + * + * @param[in] dev device to operate on + * + * @retval string containing the error value. + */ +static inline char const *at_get_err_info(at_dev_t *dev) +{ + return dev->rp_buf; +} /** * @brief Initialize AT device struct * * @param[in] dev struct to initialize - * @param[in] uart UART the device is connected to - * @param[in] baudrate baudrate of the device - * @param[in] buf input buffer - * @param[in] bufsize size of @p buf + * @param[in] init init struct, may be destroyed after return * * @retval success code UART_OK on success * @retval error code UART_NODEV or UART_NOBAUD otherwise */ -int at_dev_init(at_dev_t *dev, uart_t uart, uint32_t baudrate, char *buf, size_t bufsize); +int at_dev_init(at_dev_t *dev, at_dev_init_t const *init); /** * @brief Simple command helper @@ -210,7 +263,9 @@ int at_dev_init(at_dev_t *dev, uart_t uart, uint32_t baudrate, char *buf, size_t * @param[in] timeout timeout (in usec) * * @retval 0 when device answers "OK" - * @retval <0 otherwise + * @retval -AT_ERR_EXTENDED if failed and a error code can be retrieved with + * @ref at_get_err_info() (i.e. DCE answered with `CMx ERROR`) + * @retval <0 other failures */ int at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout); @@ -220,12 +275,14 @@ int at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout); * This function sends the supplied @p command, then waits for the prompt (>) * character and returns * - * @param[in] dev device to operate on - * @param[in] command command string to send - * @param[in] timeout timeout (in usec) + * @param[in] dev device to operate on + * @param[in] command command string to send + * @param[in] timeout timeout (in usec) * - * @retval 0 when prompt is received - * @retval <0 otherwise + * @retval 0 when prompt is received + * @retval -AT_ERR_EXTENDED if failed and a error code can be retrieved with + * @ref at_get_err_info() (i.e. DCE answered with `CMx ERROR`) + * @retval <0 other failures */ int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout); @@ -246,7 +303,8 @@ int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout * @retval n length of response on success * @retval <0 on error */ -ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, char *resp_buf, size_t len, uint32_t timeout); +ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, char *resp_buf, + size_t len, uint32_t timeout); /** * @brief Send AT command, wait for response plus OK @@ -264,7 +322,9 @@ ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, char *resp_buf, * @param[in] timeout timeout (in usec) * * @retval n length of response on success - * @retval <0 on error + * @retval -AT_ERR_EXTENDED if failed and a error code can be retrieved with + * @ref at_get_err_info() (i.e. DCE answered with `CMx ERROR`) + * @retval <0 other failures */ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const char *resp_prefix, char *resp_buf, size_t len, uint32_t timeout); @@ -275,9 +335,8 @@ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const c * This function sends the supplied @p command, then returns all response * lines until the device sends "OK". * - * If a line starts with "ERROR" or the buffer is full, the function returns -1. - * If a line starts with "+CME ERROR" or +CMS ERROR", the function returns -2. - * In this case resp_buf contains the error string. + * If a line contains a DTE error response, this function stops and returns + * accordingly. * * @param[in] dev device to operate on * @param[in] command command to send @@ -287,8 +346,9 @@ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const c * @param[in] timeout timeout (in usec) * * @retval n length of response on success - * @retval -1 on error - * @retval -2 on CMS or CME error + * @retval -AT_ERR_EXTENDED if failed and a error code can be retrieved with + * @ref at_get_err_info() (i.e. DCE answered with `CMx ERROR`) + * @retval <0 other failures */ ssize_t at_send_cmd_get_lines(at_dev_t *dev, const char *command, char *resp_buf, size_t len, bool keep_eol, uint32_t timeout); @@ -369,6 +429,22 @@ ssize_t at_recv_bytes(at_dev_t *dev, char *bytes, size_t len, uint32_t timeout); */ int at_send_cmd(at_dev_t *dev, const char *command, uint32_t timeout); +/** + * @brief Parse a response from the device. + * + * This is always called automatically in functions that may return -AT_ERR_EXTENDED. + * However, if you read the response by other methods (e.g. with @ref at_recv_bytes()), + * you might want to call this on the response so that you don't have to parse it yourself. + * + * @retval 0 if the response is "OK" + * @retval -AT_ERR_EXTENDED if the response is `+CMx ERROR: `, and `` + * has been successfully copied to @p dev->rp_buf + * @retval -1 if the response is "ERROR", or `+CMx ERROR: ` but `` + * could not be copied + * @retval 1 otherwise + */ +int at_parse_resp(at_dev_t *dev, char const *resp); + /** * @brief Read a line from device * @@ -398,6 +474,21 @@ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, ui ssize_t at_readline_skip_empty(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, uint32_t timeout); +/** + * @brief Wait for an OK response. + * + * Useful when crafting the command-response sequence by yourself. + * + * @param[in] dev device to operate on + * @param[in] timeout timeout (in usec) + * + * @retval 0 when device answers "OK" + * @retval -AT_ERR_EXTENDED if failed and a error code can be retrieved with + * @ref at_get_err_info() (i.e. DCE answered with `CMx ERROR`) + * @retval <0 other failures + */ +int at_wait_ok(at_dev_t *dev, uint32_t timeout); + /** * @brief Drain device input buffer * diff --git a/tests/drivers/at/Makefile b/tests/drivers/at/Makefile index 39526a60b3..5a27093a7f 100644 --- a/tests/drivers/at/Makefile +++ b/tests/drivers/at/Makefile @@ -2,7 +2,14 @@ include ../Makefile.drivers_common USEMODULE += shell USEMODULE += at -USEMODULE += at_urc_isr_medium +USEMODULE += at_urc + +# Enable if the DCE is sending only \n for EOL +# CFLAGS += -DAT_RECV_EOL_1="" + +# Enable this to test with echo off. Don't forget to disable echo in +# 'tests-with-config/emulated_dce.py' too! +# CFLAGS += -DCONFIG_AT_SEND_SKIP_ECHO=1 # we are printing from the event thread, we need more stack CFLAGS += -DEVENT_THREAD_MEDIUM_STACKSIZE=1024 diff --git a/tests/drivers/at/main.c b/tests/drivers/at/main.c index 14e6f4c17a..903e02f57c 100644 --- a/tests/drivers/at/main.c +++ b/tests/drivers/at/main.c @@ -20,6 +20,7 @@ * @} */ +#include #include #include #include @@ -33,6 +34,7 @@ static at_dev_t at_dev; static char buf[256]; static char resp[1024]; +static char rp_buf[256]; static int init(int argc, char **argv) { @@ -51,9 +53,15 @@ static int init(int argc, char **argv) printf("Wrong UART device number - should be in range 0-%d.\n", UART_NUMOF - 1); return 1; } - - int res = at_dev_init(&at_dev, UART_DEV(uart), baudrate, buf, sizeof(buf)); - + at_dev_init_t at_init_params = { + .baudrate = baudrate, + .rp_buf = rp_buf, + .rp_buf_size = sizeof(rp_buf), + .rx_buf = buf, + .rx_buf_size = sizeof(buf), + .uart = UART_DEV(uart), + }; + int res = at_dev_init(&at_dev, &at_init_params); /* check the UART initialization return value and respond as needed */ if (res == UART_NODEV) { puts("Invalid UART device given!"); @@ -110,7 +118,8 @@ static int send_lines(int argc, char **argv) } ssize_t len; - if ((len = at_send_cmd_get_lines(&at_dev, argv[1], resp, sizeof(resp), true, 10 * US_PER_SEC)) < 0) { + if ((len = at_send_cmd_get_lines(&at_dev, argv[1], resp, sizeof(resp), + true, 10 * US_PER_SEC)) < 0) { puts("Error"); return 1; } @@ -285,7 +294,7 @@ static int remove_urc(int argc, char **argv) } #endif -static int sneaky_urc(int argc, char **argv) +static int emulate_dce(int argc, char **argv) { (void)argc; (void)argv; @@ -299,9 +308,21 @@ static int sneaky_urc(int argc, char **argv) #endif res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=1", US_PER_SEC); - if (res) { - puts("Error AT+CFUN=1"); + printf("%u: Error AT+CFUN=1: %d\n", __LINE__, res); + res = 1; + goto exit; + } + + res = at_send_cmd(&at_dev, "AT+CFUN=1", US_PER_SEC); + if (res) { + printf("%u: Error AT+CFUN=1: %d\n", __LINE__, res); + res = 1; + goto exit; + } + res = at_wait_ok(&at_dev, US_PER_SEC); + if (res) { + printf("%u: Error AT+CFUN=1: %d\n", __LINE__, res); res = 1; goto exit; } @@ -310,39 +331,72 @@ static int sneaky_urc(int argc, char **argv) "+CEREG:", resp_buf, sizeof(resp_buf), US_PER_SEC); if (res < 0) { - puts("Error AT+CEREG?"); + printf("%u: Error AT+CEREG?: %d\n", __LINE__, res); res = 1; goto exit; } res = at_send_cmd_wait_prompt(&at_dev, "AT+USECMNG=0,0,\"cert\",128", US_PER_SEC); if (res) { - puts("Error AT+USECMNG"); + printf("%u: Error AT+USECMNG: %d\n", __LINE__, res); + res = 1; + goto exit; + } + + res = at_send_cmd_wait_prompt(&at_dev, "AT+PROMPTERROR", US_PER_SEC); + if (res != -AT_ERR_EXTENDED) { + printf("%u: Error AT+PROMPTERROR: %d\n", __LINE__, res); + res = 1; + goto exit; + } + res = atol(at_get_err_info(&at_dev)); + if (res != 1984) { + printf("%u: Error AT+PROMPTERROR: %d\n", __LINE__, res); res = 1; goto exit; } res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=8", US_PER_SEC); - if (res != -1) { - puts("Error AT+CFUN=8"); + printf("%u: Error AT+CFUN=8: %d\n", __LINE__, res); res = 1; goto exit; } res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=9", US_PER_SEC); - - if (res != -2) { - puts("Error AT+CFUN=9"); + if (res != -AT_ERR_EXTENDED) { + printf("%u: Error AT+CFUN=9: %d\n", __LINE__, res); res = 1; goto exit; } + res = atol(at_get_err_info(&at_dev)); + if (res != 666) { + printf("%u: Error AT+CFUN=9: %d\n", __LINE__, res); + res = 1; + goto exit; + } + + res = at_send_cmd_get_lines(&at_dev, "AT+GETTWOLINES", resp_buf, + sizeof(resp_buf), false, US_PER_SEC); + if (res < 0) { + printf("%u: Error AT+GETTWOLINES: %d\n", __LINE__, res); + res = 1; + goto exit; + } + + if (strcmp(resp_buf, "first_line\nsecond_line\nOK")) { + printf("%u: Error AT+GETTWOLINES: response not matching\n", __LINE__); + res = 1; + goto exit; + } + res = 0; exit: #ifdef MODULE_AT_URC at_remove_urc(&at_dev, &urc); #endif + printf("%s finished with %d\n", __func__, res); return res; } @@ -357,7 +411,7 @@ static const shell_command_t shell_commands[] = { { "drain", "Drain AT device", drain }, { "power_on", "Power on AT device", power_on }, { "power_off", "Power off AT device", power_off }, - { "sneaky_urc", "Test sneaky URC interference", sneaky_urc}, + { "emulate_dce", "Test against the DCE emulation script.", emulate_dce}, #ifdef MODULE_AT_URC { "add_urc", "Register an URC", add_urc }, { "remove_urc", "De-register an URC", remove_urc }, diff --git a/tests/drivers/at/tests-with-config/sneaky_urc.py b/tests/drivers/at/tests-with-config/emulate_dce.py similarity index 67% rename from tests/drivers/at/tests-with-config/sneaky_urc.py rename to tests/drivers/at/tests-with-config/emulate_dce.py index 7f4d059219..2222b1b79b 100755 --- a/tests/drivers/at/tests-with-config/sneaky_urc.py +++ b/tests/drivers/at/tests-with-config/emulate_dce.py @@ -13,17 +13,27 @@ # 1. Adapt the `EOL_IN`, `EOL_OUT`, `ECHO_ON` variables below to match your use case # 2. Run this script with the baud rate and the serial dev the device is connected # to, e.g.: -# $ ./sneaky_urc 115200 /dev/ttyUSB0 +# $ ./emulate_dce.py 115200 /dev/ttyUSB0 # 4. run the test (e.g. make term) # 5. inside the test console: # a) run the `init` command (e.g. init 0 115200) -# b) run `sneaky_urc` command +# b) run `emulate_dce` command # # If the command echoing is enabled, you will miss the URCs, but the commands # should work (e.g. the URC should not interfere with response parsing). # # If command echoing is enabled AND `MODULE_AT_URC` is defined, you should see # *some* of the URCs being parsed. +# +# The easiest way is to run this test on native, with an socat tty bridge: +# a) run `$ socat -d -d pty,raw,echo=0 pty,raw,echo=0` +# socat will display the pty endpoints it just created, e.g: +# 2024/01/23 10:24:56 socat[45360] N PTY is /dev/pts/9 +# 2024/01/23 10:24:56 socat[45360] N PTY is /dev/pts/10 +# b) pick one pty endpoint (e.g. /dev/pts/9) and run this script: +# `$ emulate_dce.py 115200 /dev/pts/9` +# c) pick the other endpoint and run the compiled binary +# ` $ ./bin/native/tests_at.elf -c /dev/pts/10 < tests-with-config/native_stdin` import sys import pexpect @@ -37,17 +47,20 @@ EOL_IN = '\r' # EOL to send back to the device EOL_OUT = '\r\n' # Command echoing enabled -ECHO_ON = False +ECHO_ON = True CFUN_CMD = "AT+CFUN=1" + EOL_IN CFUN_ERR_CMD = "AT+CFUN=8" + EOL_IN CFUN_CME_CMD = "AT+CFUN=9" + EOL_IN CEREG_CMD = "AT+CEREG?" + EOL_IN USECMNG_CMD = "AT+USECMNG=0,0,\"cert\",128" + EOL_IN +GETTWOLINES_CMD = "AT+GETTWOLINES" + EOL_IN +PROMPTERROR_CMD = "AT+PROMPTERROR" + EOL_IN while True: try: - idx = tty.expect_exact([CFUN_CMD, CFUN_ERR_CMD, CFUN_CME_CMD, CEREG_CMD, USECMNG_CMD]) + idx = tty.expect_exact([CFUN_CMD, CFUN_ERR_CMD, CFUN_CME_CMD, CEREG_CMD, USECMNG_CMD, + GETTWOLINES_CMD, PROMPTERROR_CMD]) if idx == 0: print(CFUN_CMD) tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) @@ -65,7 +78,7 @@ while True: tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) if ECHO_ON: tty.send(CFUN_CME_CMD) - tty.send(EOL_OUT + "+CME ERROR:" + EOL_OUT) + tty.send(EOL_OUT + "+CME ERROR: 666" + EOL_OUT) elif idx == 3: print(CEREG_CMD) tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) @@ -79,6 +92,16 @@ while True: if ECHO_ON: tty.send(USECMNG_CMD) tty.send(">") + elif idx == 5: + print(GETTWOLINES_CMD) + if ECHO_ON: + tty.send(GETTWOLINES_CMD) + tty.send(EOL_OUT + "first_line" + EOL_OUT + "second_line" + EOL_OUT + "OK" + EOL_OUT) + elif idx == 6: + print(PROMPTERROR_CMD) + if ECHO_ON: + tty.send(PROMPTERROR_CMD) + tty.send(EOL_OUT + "+CME ERROR: 1984" + EOL_OUT) except pexpect.EOF: print("ERROR: EOF") except pexpect.TIMEOUT: diff --git a/tests/drivers/at/tests-with-config/native_stdin b/tests/drivers/at/tests-with-config/native_stdin new file mode 100644 index 0000000000..95ca27ece9 --- /dev/null +++ b/tests/drivers/at/tests-with-config/native_stdin @@ -0,0 +1,2 @@ +init 0 115200 +emulate_dce