From 63e057cb50ea1176381b4bc1178339af9a057975 Mon Sep 17 00:00:00 2001 From: Mihai Renea Date: Thu, 1 Feb 2024 12:36:58 +0100 Subject: [PATCH] drivers/at: Fix sync URC handling. at_send_cmd_get_lines() keeps EOL. --- drivers/at/at.c | 455 +++++++----- drivers/include/at.h | 169 +++-- tests/drivers/at/Makefile | 23 +- tests/drivers/at/main.c | 4 +- .../at/tests-with-config/test_all_configs.sh | 70 ++ tests/unittests/tests-at/Makefile | 1 + tests/unittests/tests-at/Makefile.include | 19 + tests/unittests/tests-at/tests-at.c | 692 ++++++++++++++++++ tests/unittests/tests-at/tests-at.h | 36 + 9 files changed, 1231 insertions(+), 238 deletions(-) create mode 100755 tests/drivers/at/tests-with-config/test_all_configs.sh create mode 100644 tests/unittests/tests-at/Makefile create mode 100644 tests/unittests/tests-at/Makefile.include create mode 100644 tests/unittests/tests-at/tests-at.c create mode 100644 tests/unittests/tests-at/tests-at.h diff --git a/drivers/at/at.c b/drivers/at/at.c index 57d833d6d9ad..acb0dc79cd3d 100644 --- a/drivers/at/at.c +++ b/drivers/at/at.c @@ -6,41 +6,9 @@ * directory for more details. */ -/* A note on URCs (Unsolicited Result Codes), regardless of whether URC handling - * is enabled or not. - * - * Some DCEs (Data Circuit-terminating Equipment, aka modem), like the LTE - * modules from uBlox define a grace period where URCs are guaranteed NOT to be - * sent as the time span between: - * - the command EOL character reception AND command being internally accepted - * - the EOL character of the last response line - * - * As follows, there is an indeterminate amount of time between: - * - the command EOL character being sent - * - the command EOL character reception AND command being internally accepted, - * i.e. the begin of the grace period - * - * In other words, we can get a URC (or more?) just after issuing the command - * and before the first line of response. The net effect is that such URCs will - * appear to be the first line of response to the last issued command. - * - * The current solution is to skip characters that don't match the expected - * response, at the expense of losing these URCs. Note, we may already lose URCs - * when calling at_drain() just before any at_send_cmd(). Success partially - * depends on whether command echoing is enabled or not: - * 1. echo enabled: by observation, it seems that the grace period begins - * BEFORE the echoed command. This has the advantage that we ALWAYS know what - * the first line of response must look like and so if it doesn't, then it's a - * URC. Thus, any procedure that calls at_send_cmd() will catch and discard - * these URCs. - * 2. echo disabled: commands that expect a response (e.g. at_send_cmd_get_resp_wait_ok()) - * will catch and discard any URC (or, if MODULE_AT_URC enabled, hand it over - * to the URC callbacks). For the rest, it is the application's responsibility - * to handle it. - */ - #include #include +#include #include #include @@ -49,7 +17,6 @@ #include "isrpipe.h" #include "isrpipe/read_timeout.h" #include "periph/uart.h" -#include "event/thread.h" #define ENABLE_DEBUG 0 #include "debug.h" @@ -58,6 +25,7 @@ #define AT_PRINT_INCOMING (0) #endif + #if defined(MODULE_AT_URC) static int _check_urc(clist_node_t *node, void *arg); #endif @@ -65,9 +33,11 @@ static int _check_urc(clist_node_t *node, void *arg); 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, +static ssize_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 ssize_t read_line_or_echo(at_dev_t *dev, char const *cmd, char *resp_buf, + size_t len, uint32_t timeout); static inline bool starts_with(char const *str, char const *prefix) { @@ -82,8 +52,10 @@ static void _isrpipe_write_one_wrapper(void *_dev, uint8_t data) int at_dev_init(at_dev_t *dev, at_dev_init_t const *init) { - dev->uart = init->uart; + assert(strlen(AT_RECV_EOL) >= 1); assert(init->rp_buf_size >= 16); + + dev->uart = init->uart; dev->rp_buf = init->rp_buf; dev->rp_buf_size = init->rp_buf_size; @@ -123,7 +95,7 @@ int at_parse_resp(at_dev_t *dev, char const *resp) int at_expect_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout) { - int res = 0; + ssize_t res = 0; while (*bytes) { char c; if ((res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)&c, 1, timeout)) == 1) { @@ -141,7 +113,7 @@ int at_expect_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout) } res = 0; out: - return res; + return (int)res; } int at_wait_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout) @@ -162,13 +134,16 @@ ssize_t at_recv_bytes(at_dev_t *dev, char *bytes, size_t len, uint32_t timeout) { char *resp_pos = bytes; while (len) { - int read_res; - if ((read_res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)resp_pos, - 1, timeout)) == 1) { - resp_pos += read_res; - len -= read_res; + ssize_t res; + if ((res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)resp_pos, + 1, timeout)) == 1) { + if (AT_PRINT_INCOMING) { + print(resp_pos, 1); + } + resp_pos += res; + len -= res; } - else if (read_res == -ETIMEDOUT) { + else if (res == -ETIMEDOUT) { break; } } @@ -180,7 +155,7 @@ int at_recv_bytes_until_string(at_dev_t *dev, const char *string, { size_t len = 0; char *_string = (char *)string; - int res = 0; + ssize_t res = 0; while (*_string && len < *bytes_len) { char c; @@ -199,19 +174,19 @@ int at_recv_bytes_until_string(at_dev_t *dev, const char *string, } } *bytes_len = len; - return res; + return (int)res; } -static int wait_echo(at_dev_t *dev, const char *command, uint32_t timeout) +static int wait_echo(at_dev_t *dev, char const *command, uint32_t timeout) { - if (at_wait_bytes(dev, command, timeout)) { - return -1; - } - - if (at_expect_bytes(dev, CONFIG_AT_SEND_EOL, timeout)) { - return -2; + ssize_t res; + while ((res = read_line_or_echo(dev, command, dev->rp_buf, dev->rp_buf_size, timeout)) > 0) { + /* keep reading until echo or some error happens. */ +#ifdef MODULE_AT_URC + clist_foreach(&dev->urc_list, _check_urc, dev->rp_buf); +#endif } - return 0; + return (int)res; } int at_send_cmd(at_dev_t *dev, const char *command, uint32_t timeout) @@ -221,17 +196,16 @@ int at_send_cmd(at_dev_t *dev, const char *command, uint32_t timeout) 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)) { - return wait_echo(dev, command, timeout); + if (IS_ACTIVE(CONFIG_AT_SEND_SKIP_ECHO)) { + return 0; } - - return 0; + return wait_echo(dev, command, timeout); } void at_drain(at_dev_t *dev) { uint8_t _tmp[16]; - int res; + ssize_t res; do { /* consider no character within 10ms "drained" */ @@ -239,28 +213,122 @@ void at_drain(at_dev_t *dev) } while (res > 0); } -ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, - char *resp_buf, size_t len, uint32_t timeout) +static bool is_eol(char p) { - ssize_t res; + return p == '\r' || p == '\n'; +} - at_drain(dev); +static char *skip_leading_eol(char *line) +{ + while (is_eol(*line)) { + line++; + } + return line; +} - res = at_send_cmd(dev, command, timeout); - if (res) { - goto out; +static size_t trim_leading_eol(char *buf, size_t str_len) +{ + char *p = skip_leading_eol(buf); + if (p == buf) { + /* not sure if memmove is a no-op in this case */ + return str_len; } + size_t size_left = str_len - (size_t)(p - buf); + /* +1 for the terminating \0 */ + memmove(buf, p, size_left + 1); + return size_left; +} - res = at_readline_skip_empty(dev, resp_buf, len, false, timeout); +static size_t trim_trailing_eol(char *line, size_t str_len) +{ + while (str_len && is_eol(line[str_len - 1])) { + line[--str_len] = '\0'; + } + return str_len; +} -out: - return res; +static size_t at_drain_n(at_dev_t *dev, size_t n) +{ + unsigned char drain_buf[16]; + while (n > 0) { + size_t const to_read = n > sizeof(drain_buf) ? sizeof(drain_buf) : n; + ssize_t res = isrpipe_read_timeout(&dev->isrpipe, drain_buf, to_read, 10000U); + if (res < 1) { + break; + } + n -= res; + } + return n; } -static ssize_t get_resp(at_dev_t *dev, const char *resp_prefix, char *resp_buf, +/** + * @retval 0 if an echo was received and flushed + * @retval length of the line if an URC was intercepted + * @retval <0 on error */ +static ssize_t read_line_or_echo(at_dev_t *dev, char const *cmd, char *resp_buf, size_t len, uint32_t timeout) { - int res; + size_t const cmd_len = strlen(cmd); + if (cmd_len == 0 || len == 0) { + return -EINVAL; + } + + if (len == 1) { + return -ENOBUFS; + } + /* We keep EOL in case the echoed command contains binary data and, by + * chance, a EOL sequence. */ + ssize_t res = at_readline_skip_empty_stop_at_str(dev, resp_buf, len, true, + cmd, timeout); + bool overflow = false; + if (res < 0) { + if (res != -ENOBUFS) { + return res; + } + /* fine to overflow when intercepting an echo */ + overflow = true; + res = len - 1; + } + if ((unsigned)res > cmd_len) { + /* definitely a URC */ + if (overflow) { + /* URC didn't fit into the buffer so it's garbage */ + return -ENOBUFS; + } + return res; + } + /* maybe a URC, but might also be: + * 1. the command contained binary data and, by chance, a newline sequence + * 2. the command overflowed the resp_buf */ + if (strncmp(cmd, resp_buf, res)) { + /* no match, indeed a URC. */ + return trim_trailing_eol(resp_buf, res); + } + /* very good chance this is a echo, flush the rest */ + size_t const left_in_echo = cmd_len - res + AT_SEND_EOL_LEN; + res = at_drain_n(dev, left_in_echo); + if (res > 0) { + res = -ETIMEDOUT; + } + + resp_buf[0] = '\0'; + return res; +} + +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 res = at_send_cmd(dev, command, timeout); + if (res) { + return res; + } + return at_readline_skip_empty(dev, resp_buf, len, false, timeout); +} + +static ssize_t get_resp_with_prefix(at_dev_t *dev, const char *resp_prefix, + char *resp_buf, size_t len, uint32_t timeout) +{ + ssize_t res; /* URCs may occur right after the command has been sent and before the * expected response */ while ((res = at_readline_skip_empty(dev, resp_buf, len, false, timeout)) >= 0) { @@ -296,95 +364,83 @@ static ssize_t get_resp(at_dev_t *dev, const char *resp_prefix, char *resp_buf, 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) { - ssize_t res; - - at_drain(dev); - - res = at_send_cmd(dev, command, timeout); + ssize_t res = at_send_cmd(dev, command, timeout); if (res) { return res; } - res = get_resp(dev, resp_prefix, resp_buf, len, timeout); + res = get_resp_with_prefix(dev, resp_prefix, resp_buf, len, timeout); if (res < 1) { - // error or OK (empty response) + /* error or OK (empty response) */ return res; } - // got response, wait for OK + /* got response, wait for OK */ return at_wait_ok(dev, timeout); } -static ssize_t get_lines(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, - uint32_t timeout) +#if IS_USED(MODULE_AT_URC) +static char *next_line(char *p) { - const char eol[] = AT_RECV_EOL_1 AT_RECV_EOL_2; - assert(sizeof(eol) > 1); + while (*p && *p != '\r' && *p != '\n') { + p++; + } + return skip_leading_eol(p); +} + +static void handle_urc_lines(at_dev_t *dev, char *resp_buf) +{ + char *p = resp_buf; + do { + char *next = next_line(p); + clist_foreach(&dev->urc_list, _check_urc, p); + p = next; + } while (*p); +} +#endif +static ssize_t get_lines(at_dev_t *dev, char *resp_buf, size_t len, uint32_t timeout) +{ ssize_t res; - size_t bytes_left = len - 1; char *pos = resp_buf; - memset(resp_buf, '\0', len); - - 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) { - *pos++ = eol[sizeof(eol) - 2]; - bytes_left--; - continue; - } - else if (res > 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; - 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; + while ((res = at_readline_skip_empty(dev, pos, len, true, timeout)) > 0) { + size_t const line_len = res; + res = at_parse_resp(dev, pos); + + len -= line_len; + pos += line_len; + + switch (res) { + case 0: /* OK */ + return (size_t)(pos - resp_buf); + case 1: /* response or URC */ + if (len == 0) { + return -ENOBUFS; } - } - else { + continue; + default: /* <0 */ +#if IS_USED(MODULE_AT_URC) + /* DCE responded with an error. If we got some lines before that, + * they must be URCs. */ + handle_urc_lines(dev, resp_buf); +#endif return res; } } - - return -ENOBUFS; + return res; } 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) + size_t len, uint32_t timeout) { - ssize_t res; - - at_drain(dev); - - res = at_send_cmd(dev, command, timeout); + ssize_t res = at_send_cmd(dev, command, timeout); if (res) { return res; } - return get_lines(dev, resp_buf, len, keep_eol, timeout); + return get_lines(dev, resp_buf, len, timeout); } static int wait_prompt(at_dev_t *dev, uint32_t timeout) { - int res; - + ssize_t res; do { res = at_readline_skip_empty_stop_at_str(dev, dev->rp_buf, dev->rp_buf_size, false, ">", timeout); @@ -402,15 +458,14 @@ static int wait_prompt(at_dev_t *dev, uint32_t timeout) #endif } while (res >= 0); - return res; + return (int)res; } int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout) { - - int res = at_send_cmd(dev, command, timeout); + ssize_t res = at_send_cmd(dev, command, timeout); if (res) { - return res; + return (int)res; } return wait_prompt(dev, timeout); } @@ -421,64 +476,68 @@ int at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout) if (res < 0) { return res; } - return at_wait_ok(dev, 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, +static ssize_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 = 0; - char *resp_pos = resp_buf; - - memset(resp_buf, 0, len); size_t substr_len = 0; + if (len < 1) { + return -EINVAL; + } + if (substr) { substr_len = strlen(substr); if (substr_len == 0) { return -EINVAL; } } + + char *resp_pos = resp_buf; char const *substr_p = resp_buf; + memset(resp_buf, 0, len); + while (len > 1) { - int read_res; - if ((read_res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)resp_pos, - 1, timeout)) == 1) { - if (AT_PRINT_INCOMING) { - print(resp_pos, read_res); - } - if (sizeof(eol) > 2 && *resp_pos == eol[0]) { - if (!keep_eol) { - continue; - } - } - if (*resp_pos == eol[sizeof(eol) - 2]) { - *resp_pos = '\0'; - break; - } + res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)resp_pos, 1, timeout); + if (res < 1) { + res = -ETIMEDOUT; + break; + } + if (AT_PRINT_INCOMING) { + print(resp_pos, 1); + } - resp_pos += read_res; - len -= read_res; + resp_pos++; + len--; - if (substr && (size_t)(resp_pos - resp_buf) >= substr_len) { - if (strncmp(substr_p, substr, substr_len) == 0) { + if ((size_t)(resp_pos - resp_buf) >= strlen(AT_RECV_EOL)) { + char *const eol_begin = resp_pos - strlen(AT_RECV_EOL); + if (strcmp(eol_begin, AT_RECV_EOL) == 0) { + if (keep_eol) { break; - } else { - substr_p++; } + *eol_begin = '\0'; + resp_pos -= strlen(AT_RECV_EOL); + break; } } - else if (read_res == -ETIMEDOUT) { - res = -ETIMEDOUT; - break; + + if (substr && (size_t)(resp_pos - substr_p) >= substr_len) { + if (strncmp(substr_p, substr, substr_len) == 0) { + break; + } else { + substr_p++; + } } } + if (len <= 1) { + return -ENOBUFS; + } if (res < 0) { *resp_buf = '\0'; } else { @@ -497,11 +556,20 @@ 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); + ssize_t res; + if (len == 1) { + /* Reading in a buffer of length 1 will forever return an empty line */ + return -ENOBUFS; } + do { + res = at_readline_stop_at_str(dev, resp_buf, len, keep_eol, substr, timeout); + /* Trim any rogue EOL characters */ + if (res > 0) { + res = trim_leading_eol(resp_buf, (size_t)res); + } else if (res == -ENOBUFS) { + res = trim_leading_eol(resp_buf, len - 1); + } + } while (res == 0); return res; } @@ -517,11 +585,11 @@ int at_wait_ok(at_dev_t *dev, uint32_t timeout) ssize_t res = at_readline_skip_empty(dev, dev->rp_buf, dev->rp_buf_size, false, timeout); if (res < 0) { - return res; + return (int)res; } res = at_parse_resp(dev, dev->rp_buf); if (res < 1) { - return res; + return (int)res; } #ifdef MODULE_AT_URC clist_foreach(&dev->urc_list, _check_urc, dev->rp_buf); @@ -549,9 +617,10 @@ static int _check_urc(clist_node_t *node, void *arg) const char *buf = arg; at_urc_t *urc = container_of(node, at_urc_t, list_node); - DEBUG("Trying to match with %s\n", urc->code); + DEBUG("Trying to match %s with %s\n", buf, urc->code); if (starts_with(buf, urc->code)) { + DEBUG("Matched %s\n", urc->code); urc->cb(urc->arg, buf); return 1; } @@ -561,20 +630,22 @@ static int _check_urc(clist_node_t *node, void *arg) void at_process_urc(at_dev_t *dev, uint32_t timeout) { - char buf[AT_BUF_SIZE]; - DEBUG("Processing URC (timeout=%" PRIu32 "us)\n", timeout); - ssize_t res; - /* keep reading while received data are shorter than EOL */ - while ((res = at_readline(dev, buf, sizeof(buf), true, timeout)) < - (ssize_t)sizeof(AT_RECV_EOL_1 AT_RECV_EOL_2) - 1) { - if (res < 0) { - return; - } + while (at_readline_skip_empty(dev, dev->rp_buf, dev->rp_buf_size, false, timeout) > 0) { + clist_foreach(&dev->urc_list, _check_urc, dev->rp_buf); } +} + +void at_postprocess_urc(at_dev_t *dev, char *buf) +{ clist_foreach(&dev->urc_list, _check_urc, buf); } + +void at_postprocess_urc_all(at_dev_t *dev, char *buf) +{ + handle_urc_lines(dev, buf); +} #endif void at_dev_poweron(at_dev_t *dev) @@ -586,3 +657,19 @@ void at_dev_poweroff(at_dev_t *dev) { uart_poweroff(dev->uart); } + +#ifdef MODULE_EMBUNIT +/* Exports for unit tests */ +__attribute__((alias("read_line_or_echo"))) +ssize_t _emb_read_line_or_echo(at_dev_t *dev, char const *cmd, char *resp_buf, + size_t len, uint32_t timeout); +__attribute__((alias("get_lines"))) +ssize_t _emb_get_lines(at_dev_t *dev, char *resp_buf, size_t len, uint32_t timeout); +__attribute__((alias("get_resp_with_prefix"))) +ssize_t _emb_get_resp_with_prefix(at_dev_t *dev, const char *resp_prefix, + char *resp_buf, size_t len, uint32_t timeout); +__attribute__((alias("wait_echo"))) +int _emb_wait_echo(at_dev_t *dev, char const *command, uint32_t timeout); +__attribute__((alias("wait_prompt"))) +int _emb_wait_prompt(at_dev_t *dev, uint32_t timeout); +#endif diff --git a/drivers/include/at.h b/drivers/include/at.h index a749e31c3565..b7b73ba711df 100644 --- a/drivers/include/at.h +++ b/drivers/include/at.h @@ -23,30 +23,71 @@ * As a debugging aid, when compiled with `-DAT_PRINT_INCOMING=1`, every input * byte gets printed. * + * ## Command echoing ## + * Most DCEs (Data Circuit-terminating Equipment, aka modem) support command + * echoing and enable it by default, and so does this driver. + * If you disabled echoing on the DCE, you can compile this driver NOT to expect + * echoing by defining CONFIG_AT_SEND_SKIP_ECHO. + * Note, if the driver is NOT expecting command echoing but the DCE is echoing, + * it should work just fine if and only if the EOL sequences of both DCE and + * DTE (Data Terminal Equipmend - i.e. the device using this driver) match, i.e. + * `AT_RECV_EOL_1 AT_RECV_EOL_2 == AT_SEND_EOL`. + * In other words, if you are unsure about the echoing behavior of the DCE or + * want to support both, set AT_SEND_EOL = AT_RECV_EOL_1 AT_RECV_EOL_2 and + * define CONFIG_AT_SEND_SKIP_ECHO. This works because the URC (Unsolicited + * Result Code) logic will intercept the echoes (see below). + * * ## Unsolicited Result Codes (URC) ## * An unsolicited result code is a string message that is not triggered as a * information text response to a previous AT command and can be output at any * time to inform a specific event or status change. * - * The module provides a basic URC handling by adding the `at_urc` module to the - * application. This allows to @ref at_add_urc "register" and - * @ref at_remove_urc "de-register" URC strings to check. Later, - * @ref at_process_urc can be called to check if any of the registered URCs have - * been detected. If a registered URC has been detected the correspondent - * @ref at_urc_t::cb "callback function" is called. The mode of operation - * requires that the user of the module processes periodically the URCs. - * - * Alternatively, one of the `at_urc_isr_` modules can be included. - * `priority` can be one of `low`, `medium` or `highest`, which correspond to - * the priority of the thread that processes the URCs. For more information on - * the priorities check the @ref sys_event module. This will extend the - * functionality of `at_urc` by processing the URCs when the @ref AT_RECV_EOL_2 - * 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. + * Some DCEs (Data Circuit-terminating Equipment, aka modem), like the LTE + * modules from uBlox define a grace period where URCs are guaranteed NOT to be + * sent as the time span between: + * - the command EOL character reception AND command being internally accepted + * - the EOL character of the last response line + * + * As follows, there is an indeterminate amount of time between: + * - the command EOL character being sent + * - the command EOL character reception AND command being internally accepted, + * i.e. the begin of the grace period + * + * In other words, we can get a URC (or more?) just after issuing the command + * and before the first line of response. The net effect is that such URCs will + * appear to be the first line of response to the last issued command. + * + * Regardless of whether URC handling is enabled or not, URC interception + * mechanics depend on command echoing: + * 1. echo enabled: by observation, it seems that the grace period begins + * BEFORE the echoed command. This has the advantage that we ALWAYS know what + * the first line of response must look like and so if it doesn't, then it's a + * URC. Thus, any procedure that calls at_send_cmd() internally will catch any + * URC. + * 2. echo disabled: commands that expect a particular type of response (e.g. + * @ref at_send_cmd_get_resp_wait_ok() with a non-trivial prefix, + * @ref at_send_cmd_wait_ok() etc.) will catch any URC. For the rest, it is + * the application's responsibility to decide whether the received answer is + * an URC or not and if yes, then @ref at_postprocess_urc() can be called with + * the response as parameter. + * + * URC handling can be enabled by adding the `at_urc` module to the + * application. This allows to @ref at_add_urc "register" and @ref at_remove_urc + * "de-register" URC strings to check. Later, URCs can be processed in three + * different ways: + * - automatically, whenever any at_* method that intercepts URCs is called. + * Such methods are marked in their docstring + * - manually, by calling at_process_urc() periodically + * - manually, by calling at_postprocess_urc() with an URC as parameter. The + * URC is assumed to have been obtained from the device through methods that + * do not automatically handle URCs (for example through @ref at_recv_bytes()) + * If a registered URC has been detected the correspondent @ref at_urc_t::cb + * "callback function" is called. * * ## 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: + * 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 * @@ -130,6 +171,11 @@ extern "C" { #define AT_RECV_EOL_2 "\n" #endif +/** + * @brief convenience macro for the EOL sequence sent by the DCE + */ +#define AT_RECV_EOL AT_RECV_EOL_1 AT_RECV_EOL_2 + /** * @brief default OK reply of an AT device. */ @@ -143,29 +189,9 @@ extern "C" { #ifndef CONFIG_AT_RECV_ERROR #define CONFIG_AT_RECV_ERROR "ERROR" #endif - -#if defined(MODULE_AT_URC) || DOXYGEN - -/** - * @brief Default buffer size used to process unsolicited result code data. - * (as exponent of 2^n). - * - * As the buffer size ALWAYS needs to be power of two, this option - * represents the exponent of 2^n, which will be used as the size of - * the buffer. - */ -#ifndef CONFIG_AT_BUF_SIZE_EXP -#define CONFIG_AT_BUF_SIZE_EXP (7U) -#endif /** @} */ -/** - * @brief Size of buffer used to process unsolicited result code data. - */ -#ifndef AT_BUF_SIZE -#define AT_BUF_SIZE (1 << CONFIG_AT_BUF_SIZE_EXP) -#endif - +#if defined(MODULE_AT_URC) || DOXYGEN /** * @brief Unsolicited result code callback * @@ -258,6 +284,8 @@ int at_dev_init(at_dev_t *dev, at_dev_init_t const *init); * * This function sends an AT command to the device and waits for "OK". * + * URCs are automatically handled + * * @param[in] dev device to operate on * @param[in] command command string to send * @param[in] timeout timeout (in usec) @@ -275,6 +303,8 @@ 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 * + * URCs are automatically handled + * * @param[in] dev device to operate on * @param[in] command command string to send * @param[in] timeout timeout (in usec) @@ -290,7 +320,11 @@ int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout * @brief Send AT command, wait for response * * This function sends the supplied @p command, then waits and returns one - * line of response. + * line of response. The response is guaranteed null-terminated. + * + * Some URCs are automatically handled. The response returned can be an + * URC. In that case, @ref at_postprocess_urc() can be called with the response + * as parameter. * * A possible empty line will be skipped. * @@ -301,6 +335,7 @@ int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout * @param[in] timeout timeout (in usec) * * @retval n length of response on success + * @retval -ENOBUFS if the supplied buffer was to small. * @retval <0 on error */ ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, char *resp_buf, @@ -310,10 +345,14 @@ ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, char *resp_buf, * @brief Send AT command, wait for response plus OK * * This function sends the supplied @p command, then waits and returns one - * line of response. + * line of response. The response is guaranteed null-terminated. * * A possible empty line will be skipped. * + * URCs are automatically handled. If no prefix is provided, the response + * may be an URC. In that case, @ref at_postprocess_urc() can be called with the + * response as parameter. + * * @param[in] dev device to operate on * @param[in] command command to send * @param[in] resp_prefix expected prefix in the response @@ -324,6 +363,7 @@ ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, char *resp_buf, * @retval n length of response on success * @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 -ENOBUFS if the supplied buffer was to small. * @retval <0 other failures */ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const char *resp_prefix, @@ -333,25 +373,30 @@ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const c * @brief Send AT command, wait for multiline response * * This function sends the supplied @p command, then returns all response - * lines until the device sends "OK". + * lines until the device sends "OK". The response is guaranteed null-terminated. + * + * Some URCs are automatically handled. The first m response lines can be + * URCs. In that case, @ref at_postprocess_urc() can be called with each line + * as parameter. * * If a line contains a DTE error response, this function stops and returns - * accordingly. + * accordingly. Any lines received prior to that are considered to be URCs and + * thus handled. * * @param[in] dev device to operate on * @param[in] command command to send * @param[out] resp_buf buffer for storing response * @param[in] len len of @p resp_buf - * @param[in] keep_eol true to keep the CR character in the response * @param[in] timeout timeout (in usec) * * @retval n length of response on success * @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 -ENOBUFS if the supplied buffer was to small. * @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); + size_t len, uint32_t timeout); /** * @brief Expect bytes from device @@ -420,6 +465,8 @@ ssize_t at_recv_bytes(at_dev_t *dev, char *bytes, size_t len, uint32_t timeout); /** * @brief Send command to device * + * Some URCs may be handled. + * * @param[in] dev device to operate on * @param[in] command command to send * @param[in] timeout timeout (in usec) @@ -448,13 +495,16 @@ int at_parse_resp(at_dev_t *dev, char const *resp); /** * @brief Read a line from device * + * Stops at the first DCE EOL sequence. The response is guaranteed null-terminated. + * * @param[in] dev device to operate on * @param[in] resp_buf buffer to store line * @param[in] len size of @p resp_buf - * @param[in] keep_eol true to keep the CR character in the response + * @param[in] keep_eol true to keep the trailing EOL sequence in the response * @param[in] timeout timeout (in usec) * * @retval n line length on success + * @retval -ENOBUFS if the supplied buffer was to small. * @retval <0 on error */ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, uint32_t timeout); @@ -462,13 +512,17 @@ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, ui /** * @brief Read a line from device, skipping a possibly empty line. * + * Stops at the first DCE EOL sequence AFTER any non-EOL sequence. The response + * is guaranteed null-terminated. + * * @param[in] dev device to operate on * @param[in] resp_buf buffer to store line * @param[in] len size of @p resp_buf - * @param[in] keep_eol true to keep the CR character in the response + * @param[in] keep_eol true to keep the trailing EOL sequence in the response * @param[in] timeout timeout (in usec) * * @retval n line length on success + * @retval -ENOBUFS if the supplied buffer was to small. * @retval <0 on error */ ssize_t at_readline_skip_empty(at_dev_t *dev, char *resp_buf, size_t len, @@ -479,6 +533,8 @@ ssize_t at_readline_skip_empty(at_dev_t *dev, char *resp_buf, size_t len, * * Useful when crafting the command-response sequence by yourself. * + * URCs are automatically handled + * * @param[in] dev device to operate on * @param[in] timeout timeout (in usec) * @@ -537,6 +593,27 @@ void at_remove_urc(at_dev_t *dev, at_urc_t *urc); * @param[in] timeout timeout (in usec) */ void at_process_urc(at_dev_t *dev, uint32_t timeout); + +/** + * @brief Process one URC from the provided buffer + * + * Useful if you e.g. called @ref at_send_cmd_get_lines() and the first lines + * are URCs. + * + * @param[in] dev device to operate on + * @param[in] buf buffer containing an URC + */ +void at_postprocess_urc(at_dev_t *dev, char *buf); +/** + * @brief Process all URCs from the provided buffer + * + * Useful if you e.g. called @ref at_recv_bytes(), found what you were interested + * in, and there might be some URCs left in the buffer. + * + * @param[in] dev device to operate on + * @param[in] buf buffer containing URCs + */ +void at_postprocess_urc_all(at_dev_t *dev, char *buf); #endif #ifdef __cplusplus diff --git a/tests/drivers/at/Makefile b/tests/drivers/at/Makefile index 5a27093a7fb3..288aa8e65421 100644 --- a/tests/drivers/at/Makefile +++ b/tests/drivers/at/Makefile @@ -2,14 +2,25 @@ include ../Makefile.drivers_common USEMODULE += shell USEMODULE += at -USEMODULE += at_urc -# Enable if the DCE is sending only \n for EOL -# CFLAGS += -DAT_RECV_EOL_1="" +HANDLE_URC ?= 1 +ECHO_ON ?= 1 +SEND_EOL ?= "\\xd" +RECV_EOL_1 ?= "\\xd" +RECV_EOL_2 ?= "\\xa" -# 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 + +ifeq ($(HANDLE_URC), 1) + USEMODULE += at_urc +endif + +ifeq ($(ECHO_ON), 0) + CFLAGS += -DCONFIG_AT_SEND_SKIP_ECHO=1 +endif + +CFLAGS += -DAT_RECV_EOL_1="\"$(RECV_EOL_1)\"" +CFLAGS += -DAT_RECV_EOL_2="\"$(RECV_EOL_2)\"" +CFLAGS += -DCONFIG_AT_SEND_EOL="\"$(SEND_EOL)\"" # 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 903e02f57c18..31a38092c053 100644 --- a/tests/drivers/at/main.c +++ b/tests/drivers/at/main.c @@ -119,7 +119,7 @@ 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) { + 10 * US_PER_SEC)) < 0) { puts("Error"); return 1; } @@ -378,7 +378,7 @@ static int emulate_dce(int argc, char **argv) } res = at_send_cmd_get_lines(&at_dev, "AT+GETTWOLINES", resp_buf, - sizeof(resp_buf), false, US_PER_SEC); + sizeof(resp_buf), US_PER_SEC); if (res < 0) { printf("%u: Error AT+GETTWOLINES: %d\n", __LINE__, res); res = 1; diff --git a/tests/drivers/at/tests-with-config/test_all_configs.sh b/tests/drivers/at/tests-with-config/test_all_configs.sh new file mode 100755 index 000000000000..d0dc78621fa8 --- /dev/null +++ b/tests/drivers/at/tests-with-config/test_all_configs.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +handle_urc=0 +echo=0 +rcv_eol_1="" +rcv_eol_2="" +send_eol="" + +run_test() { + echo "Running test with:" + echo " URC handling = $handle_urc" + echo " echo = $echo" + echo " send EOL = $send_eol" + echo " rcv EOL 1 = $rcv_eol_1" + echo " rcv EOL 2 = $rcv_eol_2" + + make -j --silent BOARD=native "HANDLE_URC=$handle_urc" "ECHO_ON=$echo" "SEND_EOL=\"$send_eol\"" "RECV_EOL_1=\"$rcv_eol_1\"" "RECV_EOL_2=\"$rcv_eol_2\"" tests-at + + # take /dev/ttyS0 as serial interface. It is only required s.t. UART + # initialization succeeds and it gets turned off right away. + set +e + if ! ./bin/native/tests_unittests.elf -c /dev/ttyS0 <<< "s\n"; + then + echo "================================================================================" + echo "Test failed! Generating compile-commands.json of the last build configuration..." + echo "================================================================================" + make -j --silent BOARD=native "HANDLE_URC=$handle_urc" "ECHO_ON=$echo" "SEND_EOL=\"$send_eol\"" "RECV_EOL_1=\"$rcv_eol_1\"" "RECV_EOL_2=\"$rcv_eol_2\"" compile-commands + exit 1 + fi + set -e +} + +# set -x +set -e + +SCRIPT=$(readlink -f "$0") +BASEDIR=$(dirname "$SCRIPT")/../../../unittests + +cd "$BASEDIR" + +for urc_i in 0 1; do + handle_urc=$urc_i + for echo_i in 0 1; do + echo=$echo_i + # 0xd == \r, 0xa == \n - I'm using this notation because I can't wrap my head around + # how many parsers along the way try to interpret \r and \n. Worse, every time the + # string is entering a new parser, one `\` is shaved off. So it looks like four + # times is the right amount so that the final C string will be something like "\xd". + for send_i in "\\\\xd" "\\\\xa" "\\\\xd\\\\xa"; do + send_eol=$send_i + + rcv_eol_1="\\\\xd" + rcv_eol_2="\\\\xa" + run_test + + rcv_eol_1="\\\\xd" + rcv_eol_2="" + run_test + + rcv_eol_1="\\\\xa" + rcv_eol_2="" + run_test + done + + done +done + +echo "=================" +echo "All tests passed!" +echo "=================" diff --git a/tests/unittests/tests-at/Makefile b/tests/unittests/tests-at/Makefile new file mode 100644 index 000000000000..48422e909a47 --- /dev/null +++ b/tests/unittests/tests-at/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/tests/unittests/tests-at/Makefile.include b/tests/unittests/tests-at/Makefile.include new file mode 100644 index 000000000000..e80eb834ae17 --- /dev/null +++ b/tests/unittests/tests-at/Makefile.include @@ -0,0 +1,19 @@ +HANDLE_URC ?= 1 +ECHO_ON ?= 1 +SEND_EOL ?= "\\xd" +RECV_EOL_1 ?= "\\xd" +RECV_EOL_2 ?= "\\xa" + +ifeq ($(HANDLE_URC), 1) + USEMODULE += at_urc +endif + +ifeq ($(ECHO_ON), 0) + CFLAGS += -DCONFIG_AT_SEND_SKIP_ECHO=1 +endif + +CFLAGS += -DAT_RECV_EOL_1="\"$(RECV_EOL_1)\"" +CFLAGS += -DAT_RECV_EOL_2="\"$(RECV_EOL_2)\"" +CFLAGS += -DCONFIG_AT_SEND_EOL="\"$(SEND_EOL)\"" + +USEMODULE += at diff --git a/tests/unittests/tests-at/tests-at.c b/tests/unittests/tests-at/tests-at.c new file mode 100644 index 000000000000..390f1344204e --- /dev/null +++ b/tests/unittests/tests-at/tests-at.c @@ -0,0 +1,692 @@ +/* + * Copyright (C) 2024 ML!PA GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#include "embUnit.h" + +#include "at.h" +#include "isrpipe/read_timeout.h" + +#include "tests-at.h" + +#define UNIT_TEST_LONG_URC "+UNITTEST_LONG_URC_VEEERY_LONG" +#define UNIT_TEST_SHORT_URC "+U" +#define LONG_COMMAND "AT+COMMAND_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"\ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +#define COMMAND_WITH_EOL "AT+COMMAND_"AT_RECV_EOL"BLABLA"AT_RECV_EOL"BLA" + +at_dev_t at_dev; +static char buf[256]; +static char rp_buf[256]; +#ifdef MODULE_AT_URC +static unsigned urc_count = 0; + +void unit_test_urc_long_handler(void *arg, char const *code) +{ + TEST_ASSERT(strncmp(UNIT_TEST_LONG_URC, code, strlen(UNIT_TEST_LONG_URC)) == 0); + unsigned *urc_count = (unsigned *)arg; + *urc_count += 1; +} + +void unit_test_urc_short_handler(void *arg, char const *code) +{ + TEST_ASSERT(strncmp(UNIT_TEST_SHORT_URC, code, strlen(UNIT_TEST_SHORT_URC)) == 0); + unsigned *urc_count = (unsigned *)arg; + *urc_count += 1; +} + +at_urc_t urc_long = { + .arg = &urc_count, + .code = UNIT_TEST_LONG_URC, + .cb = unit_test_urc_long_handler +}; +at_urc_t urc_short = { + .arg = &urc_count, + .code = UNIT_TEST_SHORT_URC, + .cb = unit_test_urc_short_handler +}; +#endif + +static void set_up(void) +{ + at_dev_init_t at_init_params = { + .baudrate = 115200, + .rp_buf = rp_buf, + .rp_buf_size = sizeof(rp_buf), + .rx_buf = buf, + .rx_buf_size = sizeof(buf), + .uart = UART_DEV(0), + }; + int res = at_dev_init(&at_dev, &at_init_params); + /* check the UART initialization return value and respond as needed */ + if (res == UART_NODEV) { + TEST_FAIL("Invalid UART device given!"); + } + if (res == UART_NOBAUD) { + TEST_FAIL("Baudrate is not applicable!"); + } + + /* we don't use the serial device, make sure it doesn't clobber our rx buffer */ + at_dev_poweroff(&at_dev); + at_drain(&at_dev); + +#ifdef MODULE_AT_URC + at_add_urc(&at_dev, &urc_long); + at_add_urc(&at_dev, &urc_short); +#endif +} + +static void tear_down(void) +{ +#ifdef MODULE_AT_URC + at_remove_urc(&at_dev, &urc_long); + at_remove_urc(&at_dev, &urc_short); +#endif +} + +static void assert_urc_count(unsigned expected) +{ +#ifdef MODULE_AT_URC + TEST_ASSERT_EQUAL_INT(expected, urc_count); + urc_count = 0; +#endif + (void)expected; +} + +int _emb_read_line_or_echo(at_dev_t *dev, char const *cmd, char *resp_buf, + size_t len, uint32_t timeout); +ssize_t _emb_get_lines(at_dev_t *dev, char *resp_buf, size_t len, uint32_t timeout); +ssize_t _emb_get_resp_with_prefix(at_dev_t *dev, const char *resp_prefix, + char *resp_buf, size_t len, uint32_t timeout); +int _emb_wait_echo(at_dev_t *dev, char const *command, uint32_t timeout); +int _emb_wait_prompt(at_dev_t *dev, uint32_t timeout); + +static void inject_resp_str(at_dev_t *dev, char const *str) +{ + isrpipe_write(&dev->isrpipe, (unsigned char const *)str, strlen(str)); +} + +void test_readline_or_echo(void) +{ + int res; + char resp_buf[64]; + at_dev_t *dev = &at_dev; + at_drain(dev); + + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == -ETIMEDOUT); + + inject_resp_str(dev, + "AT+COMMAND" + CONFIG_AT_SEND_EOL); + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == 0); + res = isrpipe_read_timeout(&dev->isrpipe, (unsigned char *)resp_buf, 1, 1000); + TEST_ASSERT(res -ETIMEDOUT); + + inject_resp_str(dev, + "AT+COMMAND" + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + /* Reading in a buffer <= 1 should not read any characters from the RX */ + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, 0, 1000); + TEST_ASSERT(res == -EINVAL); + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, 1, 1000); + TEST_ASSERT(res == -ENOBUFS); + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == 0); + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, 1000); + TEST_ASSERT(res > 0); + res = strcmp("OK", resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + "" + CONFIG_AT_SEND_EOL); + res = _emb_read_line_or_echo(dev, "", resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == -EINVAL); + + /* here we should have a rogue CONFIG_AT_SEND_EOL left in the buffer from before */ + inject_resp_str(dev, + LONG_COMMAND + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_read_line_or_echo(dev, LONG_COMMAND, resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == 0); + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, 1000); + TEST_ASSERT(res > 0); + res = strcmp("OK", resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + "+R" + AT_RECV_EOL); + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res > 0); + res = strcmp("+R", resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "+R" + AT_RECV_EOL); + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res > 0); + res = strcmp("+R", resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + "+R" + AT_RECV_EOL); + res = _emb_read_line_or_echo(dev, "AT+COMMAND", resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res > 0); + res = strcmp("+R", resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + COMMAND_WITH_EOL + CONFIG_AT_SEND_EOL); + res = _emb_read_line_or_echo(dev, COMMAND_WITH_EOL, resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == 0); + res = isrpipe_read_timeout(&dev->isrpipe, (unsigned char *)resp_buf, 1, 1000); + TEST_ASSERT(res -ETIMEDOUT); +} + +void test_wait_echo(void) +{ + int res; + char resp_buf[64]; + at_dev_t *dev = &at_dev; + + at_drain(dev); + + res = _emb_wait_echo(dev, "AT+COMMAND", 1000); + TEST_ASSERT(res == -ETIMEDOUT); + + inject_resp_str(dev, "AT+COMMAND" CONFIG_AT_SEND_EOL); + res = _emb_wait_echo(dev, "AT+COMMAND", 1000); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_LONG_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL); + res = _emb_wait_echo(dev, "AT+COMMAND", 1000); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_LONG_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_wait_echo(dev, "AT+COMMAND", 1000); + TEST_ASSERT(res == 0); + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, 1000); + TEST_ASSERT(res > 0); + res = at_parse_resp(dev, resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + "" + CONFIG_AT_SEND_EOL); + res = _emb_wait_echo(dev, "", 1000); + TEST_ASSERT(res == -EINVAL); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_wait_echo(dev, "AT+COMMAND", 1000); + TEST_ASSERT(res == 0); + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, 1000); + TEST_ASSERT(res > 0); + res = at_parse_resp(dev, resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_LONG_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_wait_echo(dev, "AT+COMMAND", 1000); + TEST_ASSERT(res == 0); + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, 1000); + TEST_ASSERT(res > 0); + res = at_parse_resp(dev, resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_LONG_URC + AT_RECV_EOL + COMMAND_WITH_EOL + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_wait_echo(dev, COMMAND_WITH_EOL, 1000); + TEST_ASSERT(res == 0); + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, 1000); + TEST_ASSERT(res > 0); + res = at_parse_resp(dev, resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_LONG_URC + AT_RECV_EOL + COMMAND_WITH_EOL + CONFIG_AT_SEND_EOL + "OK" + AT_RECV_EOL); + res = _emb_wait_echo(dev, COMMAND_WITH_EOL, 1000); + TEST_ASSERT(res == 0); + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, 1000); + TEST_ASSERT(res > 0); + res = at_parse_resp(dev, resp_buf); + TEST_ASSERT(res == 0); + + assert_urc_count(9); +} + +void test_get_resp_with_prefix(void) +{ + int res; + char resp_buf[64]; + at_dev_t *dev = &at_dev; + + at_drain(dev); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "+RESPONSE: 123" + AT_RECV_EOL); + res = _emb_get_resp_with_prefix(dev, "+RESPONSE: ", resp_buf, sizeof(resp_buf), 10000); + TEST_ASSERT(res > 0); + res = strcmp("123", resp_buf); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_get_resp_with_prefix(dev, "+RESPONSE: ", resp_buf, sizeof(resp_buf), 10000); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL "+CME ERROR: 1" + AT_RECV_EOL); + res = _emb_get_resp_with_prefix(dev, "+RESPONSE: ", resp_buf, sizeof(resp_buf), 10000); + TEST_ASSERT(res == -AT_ERR_EXTENDED); + res = strncmp("1", dev->rp_buf, 1); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "ERROR" + AT_RECV_EOL); + res = _emb_get_resp_with_prefix(dev, "+RESPONSE: ", resp_buf, sizeof(resp_buf), 10000); + TEST_ASSERT(res == -1); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL); + res = _emb_get_resp_with_prefix(dev, "+RESPONSE: ", resp_buf, sizeof(resp_buf), 10000); + TEST_ASSERT(res == -ETIMEDOUT); + + inject_resp_str(dev, + "trash" + AT_RECV_EOL + "+RESPONSE: 123" + AT_RECV_EOL); + res = _emb_get_resp_with_prefix(dev, "+RESPONSE: ", resp_buf, sizeof(resp_buf), 10000); + TEST_ASSERT(res > 0); + res = strcmp("123", resp_buf); + TEST_ASSERT(res == 0); + + assert_urc_count(5); +} + +void test_read_lines(void) +{ + int res; + char resp_buf[62]; + char *p; + at_dev_t *dev = &at_dev; + + at_drain(dev); + + inject_resp_str(dev, + AT_RECV_EOL + "+R1" + AT_RECV_EOL + "+R2" + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_get_lines(dev, resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res > 0); + p = resp_buf; + p = strstr(resp_buf, "+R1"); + TEST_ASSERT(p); + p = strstr(resp_buf, "+R2"); + TEST_ASSERT(p); + p = strstr(resp_buf, "OK"); + TEST_ASSERT(p); + + /* inconsistent EOL */ + inject_resp_str(dev, + "+R1" + AT_RECV_EOL + "+R2" + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_get_lines(dev, resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res > 0); + p = resp_buf; + p = strstr(resp_buf, "+R1"); + TEST_ASSERT(p); + p = strstr(resp_buf, "+R2"); + TEST_ASSERT(p); + p = strstr(resp_buf, "OK"); + TEST_ASSERT(p); + + /* URCs should get handled here */ + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "ERROR" + AT_RECV_EOL); + res = _emb_get_lines(dev, resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == -1); + + /* URCs shouldn't get handled here. DCE answered neither OK nor error, + * something went terribly wrong anyway, fine to just drop them. */ + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL); + res = _emb_get_lines(dev, resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == -ETIMEDOUT); + + /* overflow the input buffer */ + inject_resp_str(dev, + AT_RECV_EOL + AT_RECV_EOL + "LONG_RESPONSE____________________________________________" + AT_RECV_EOL + "LONG_RESPONSE____________________________________________" + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = _emb_get_lines(dev, resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == -ENOBUFS); + + assert_urc_count(2); +} + +void test_wait_prompt(void) +{ + int res; + char resp_buf[64]; + at_dev_t *dev = &at_dev; + + at_drain(dev); + + inject_resp_str(dev, + AT_RECV_EOL + ">" + "123"); + res = _emb_wait_prompt(dev, 1000); + TEST_ASSERT(res == 0); + res = isrpipe_read_timeout(&dev->isrpipe, (unsigned char *)resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == 3); + res = strncmp("123", resp_buf, 3); + + inject_resp_str(dev, + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + ">" + "456"); + res = _emb_wait_prompt(dev, 1000); + TEST_ASSERT(res == 0); + res = isrpipe_read_timeout(&dev->isrpipe, (unsigned char *)resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == 3); + res = strncmp("456", resp_buf, 3); + + inject_resp_str(dev, + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "ERROR" + AT_RECV_EOL); + res = _emb_wait_prompt(dev, 1000); + TEST_ASSERT(res == -1); + + inject_resp_str(dev, + ">" + "123"); + res = _emb_wait_prompt(dev, 1000); + TEST_ASSERT(res == 0); + res = isrpipe_read_timeout(&dev->isrpipe, (unsigned char *)resp_buf, sizeof(resp_buf), 1000); + TEST_ASSERT(res == 3); + res = strncmp("123", resp_buf, 3); + TEST_ASSERT(res == 0); + + assert_urc_count(2); +} + +void test_wait_ok(void) +{ + int res; + unsigned urc_cnt = 5; + at_dev_t *dev = &at_dev; + + at_drain(dev); + + inject_resp_str(dev, + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + "ERROR" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == -1); + + inject_resp_str(dev, + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "+CME ERROR: 2" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == -AT_ERR_EXTENDED); + + inject_resp_str(dev, + "trash" + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == 0); +#ifdef CONFIG_AT_SEND_SKIP_ECHO + if (strcmp(AT_RECV_EOL, CONFIG_AT_SEND_EOL) == 0) { + /* Test echo handling when none expected */ + urc_cnt += 5; + at_drain(dev); + + inject_resp_str(dev, + UNIT_TEST_SHORT_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == 0); + + inject_resp_str(dev, + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL + "ERROR" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == -1); + + inject_resp_str(dev, + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "+CME ERROR: 2" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == -AT_ERR_EXTENDED); + + inject_resp_str(dev, + "trash" + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + "AT+COMMAND" + CONFIG_AT_SEND_EOL + AT_RECV_EOL + "OK" + AT_RECV_EOL); + res = at_wait_ok(dev, 1000); + TEST_ASSERT(res == 0); + } +#endif /* CONFIG_AT_SEND_SKIP_ECHO */ + assert_urc_count(urc_cnt); +} + +#ifdef MODULE_AT_URC +void test_process_urc(void) +{ + at_dev_t *dev = &at_dev; + at_drain(dev); + + at_process_urc(dev, 1000); + + inject_resp_str(dev, + "trash" + AT_RECV_EOL + UNIT_TEST_SHORT_URC + AT_RECV_EOL + AT_RECV_EOL + UNIT_TEST_LONG_URC + AT_RECV_EOL); + at_process_urc(dev, 1000); + + assert_urc_count(2); +} +#endif /* MODULE_AT_URC */ + +void tests_at(void) +{ + EMB_UNIT_TESTFIXTURES(fixtures) { + new_TestFixture(test_readline_or_echo), + #ifndef CONFIG_AT_SEND_SKIP_ECHO + new_TestFixture(test_wait_echo), + #endif + new_TestFixture(test_get_resp_with_prefix), + new_TestFixture(test_read_lines), + new_TestFixture(test_wait_prompt), + new_TestFixture(test_wait_ok), + #ifdef MODULE_AT_URC + new_TestFixture(test_process_urc), + #endif + }; + + EMB_UNIT_TESTCALLER(at_tests, set_up, tear_down, fixtures); + + TESTS_RUN((Test *)&at_tests); +} diff --git a/tests/unittests/tests-at/tests-at.h b/tests/unittests/tests-at/tests-at.h new file mode 100644 index 000000000000..62c0c17b347a --- /dev/null +++ b/tests/unittests/tests-at/tests-at.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 ML!PA GmbH + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @addtogroup unittests + * @{ + * + * @file + * @brief Unittests for the at module + * + * @author Mihai Renea + */ +#ifndef TESTS_AT_H +#define TESTS_AT_H +#include "embUnit/embUnit.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Tests entry point. + */ +void tests_at(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TESTS_AT_H */ +/** @} */