diff --git a/drivers/at/at.c b/drivers/at/at.c index d927329999..1d1f522cfc 100644 --- a/drivers/at/at.c +++ b/drivers/at/at.c @@ -6,6 +6,39 @@ * 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 @@ -32,6 +65,10 @@ #define AT_EVENT_PRIO EVENT_PRIO_HIGHEST #endif +#if defined(MODULE_AT_URC) +static int _check_urc(clist_node_t *node, void *arg); +#endif + #if defined(MODULE_AT_URC_ISR) static void _event_process_urc(event_t *_event) { @@ -62,8 +99,7 @@ int at_dev_init(at_dev_t *dev, uart_t uart, uint32_t baudrate, char *buf, size_t isrpipe_init(&dev->isrpipe, (uint8_t *)buf, bufsize); - return uart_init(uart, baudrate, _isrpipe_write_one_wrapper, - dev); + return uart_init(uart, baudrate, _isrpipe_write_one_wrapper, dev); } int at_expect_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout) @@ -99,6 +135,15 @@ out: return res; } +int at_wait_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout) +{ + int res; + do { + res = at_expect_bytes(dev, bytes, timeout); + } while (res != 0 && res != -ETIMEDOUT); + return res; +} + void at_send_bytes(at_dev_t *dev, const char *bytes, size_t len) { uart_write(dev->uart, (const uint8_t *)bytes, len); @@ -175,11 +220,12 @@ int at_send_cmd(at_dev_t *dev, const char *command, uint32_t timeout) 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_expect_bytes(dev, command, timeout)) { + if (at_wait_bytes(dev, command, 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 AT_RECV_EOL_1 AT_RECV_EOL_2, + timeout)) { return -2; } } @@ -218,11 +264,7 @@ ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, goto out; } - res = at_readline(dev, resp_buf, len, false, timeout); - if (res == 0) { - /* skip possible empty line */ - res = at_readline(dev, resp_buf, len, false, timeout); - } + res = at_readline_skip_empty(dev, resp_buf, len, false, timeout); out: return res; @@ -242,35 +284,35 @@ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const c goto out; } - res = at_readline(dev, resp_buf, len, false, timeout); - if (res == 0) { - /* skip possible empty line */ - res = at_readline(dev, resp_buf, len, false, timeout); - } + /* 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); - /* 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; + /* 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; + } } - } + } while (res >= 0); /* wait for OK */ if (res >= 0) { - res_ok = at_readline(dev, ok_buf, sizeof(ok_buf), false, timeout); - if (res_ok == 0) { - /* skip possible empty line */ - res_ok = at_readline(dev, ok_buf, sizeof(ok_buf), false, timeout); + res_ok = at_readline_skip_empty(dev, ok_buf, sizeof(ok_buf), false, timeout); + if (res_ok < 0) { + return -1; } ssize_t len_ok = sizeof(CONFIG_AT_RECV_OK) - 1; if ((len_ok != 0) && (strcmp(ok_buf, CONFIG_AT_RECV_OK) == 0)) { } else { - /* Something else then OK */ + /* Something else than OK */ res = -1; } } @@ -295,8 +337,6 @@ ssize_t at_send_cmd_get_lines(at_dev_t *dev, const char *command, goto out; } - /* only clear the response buffer after sending the command, - * so the same buffer can be used for command and response */ memset(resp_buf, '\0', len); while (1) { @@ -358,19 +398,16 @@ int at_send_cmd_wait_prompt(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 (at_expect_bytes(dev, command, timeout)) { - return -1; + 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; + } } - if (at_expect_bytes(dev, CONFIG_AT_SEND_EOL AT_RECV_EOL_2, timeout)) { - return -2; - } - - if (at_expect_bytes(dev, ">", timeout)) { - return -3; - } - - return 0; + return at_wait_bytes(dev, ">", timeout); } int at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout) @@ -378,16 +415,31 @@ 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); + res = at_send_cmd_get_resp(dev, command, resp_buf, sizeof(resp_buf), + timeout); - if (res > 0) { - ssize_t len_ok = sizeof(CONFIG_AT_RECV_OK) - 1; - if ((len_ok != 0) && (strcmp(resp_buf, CONFIG_AT_RECV_OK) == 0)) { - res = 0; + 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; + + while (res >= 0) { + if (strncmp(resp_buf, CONFIG_AT_RECV_OK, len_ok) == 0) { + return 0; } - else { - res = -1; + 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); +#endif + res = at_readline_skip_empty(dev, resp_buf, sizeof(resp_buf), false, timeout); } return res; @@ -445,6 +497,17 @@ out: 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; +} + #ifdef MODULE_AT_URC void at_add_urc(at_dev_t *dev, at_urc_t *urc) { diff --git a/drivers/include/at.h b/drivers/include/at.h index 90126a0630..4b0133ca47 100644 --- a/drivers/include/at.h +++ b/drivers/include/at.h @@ -195,8 +195,8 @@ typedef struct { * @param[in] buf input buffer * @param[in] bufsize size of @p buf * - * @returns success code UART_OK on success - * @returns error code UART_NODEV or UART_NOBAUD otherwise + * @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); @@ -209,8 +209,8 @@ int at_dev_init(at_dev_t *dev, uart_t uart, uint32_t baudrate, char *buf, size_t * @param[in] command command string to send * @param[in] timeout timeout (in usec) * - * @returns 0 when device answers "OK" - * @returns <0 otherwise + * @retval 0 when device answers "OK" + * @retval <0 otherwise */ int at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout); @@ -224,8 +224,8 @@ int at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout); * @param[in] command command string to send * @param[in] timeout timeout (in usec) * - * @return 0 when prompt is received - * @return <0 otherwise + * @retval 0 when prompt is received + * @retval <0 otherwise */ int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout); @@ -243,8 +243,8 @@ int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout * @param[in] len len of @p buffer * @param[in] timeout timeout (in usec) * - * @returns length of response on success - * @returns <0 on error + * @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); @@ -263,8 +263,8 @@ ssize_t at_send_cmd_get_resp(at_dev_t *dev, const char *command, char *resp_buf, * @param[in] len len of @p buffer * @param[in] timeout timeout (in usec) * - * @returns length of response on success - * @returns <0 on error + * @retval n length of response on success + * @retval <0 on error */ 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); @@ -286,9 +286,9 @@ ssize_t at_send_cmd_get_resp_wait_ok(at_dev_t *dev, const char *command, const c * @param[in] keep_eol true to keep the CR character in the response * @param[in] timeout timeout (in usec) * - * @returns length of response on success - * @returns -1 on error - * @returns -2 on CMS or CME error + * @retval n length of response on success + * @retval -1 on error + * @retval -2 on CMS or CME error */ 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); @@ -300,11 +300,23 @@ ssize_t at_send_cmd_get_lines(at_dev_t *dev, const char *command, char *resp_buf * @param[in] bytes buffer containing bytes to expect (NULL-terminated) * @param[in] timeout timeout (in usec) * - * @returns 0 on success - * @returns <0 otherwise + * @retval 0 on success + * @retval <0 otherwise */ int at_expect_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout); +/** + * @brief Repeatedly calls at_expect_bytes() until a match or timeout occurs + * + * @param[in] dev device to operate on + * @param[in] bytes buffer containing bytes to expect (NULL-terminated) + * @param[in] timeout timeout (in usec) + * + * @retval 0 on success + * @retval <0 otherwise + */ +int at_wait_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout); + /** * @brief Receives bytes into @p bytes buffer until the string pattern * @p string is received or the buffer is full. @@ -317,8 +329,8 @@ int at_expect_bytes(at_dev_t *dev, const char *bytes, uint32_t timeout); * bytes. * @param[in] timeout timeout (in usec) of inactivity to finish read * - * @returns 0 on success - * @returns <0 on error + * @retval 0 on success + * @retval <0 on error */ int at_recv_bytes_until_string(at_dev_t *dev, const char *string, char *bytes, size_t *bytes_len, @@ -341,7 +353,7 @@ void at_send_bytes(at_dev_t *dev, const char *bytes, size_t len); * @param[in] len maximum number of bytes to receive * @param[in] timeout timeout (in usec) of inactivity to finish read * - * @returns Number of bytes read, eventually zero if no bytes available + * @retval n Number of bytes read, eventually zero if no bytes available */ ssize_t at_recv_bytes(at_dev_t *dev, char *bytes, size_t len, uint32_t timeout); @@ -352,8 +364,8 @@ ssize_t at_recv_bytes(at_dev_t *dev, char *bytes, size_t len, uint32_t timeout); * @param[in] command command to send * @param[in] timeout timeout (in usec) * - * @returns 0 on success - * @returns <0 otherwise + * @retval 0 on success + * @retval <0 otherwise */ int at_send_cmd(at_dev_t *dev, const char *command, uint32_t timeout); @@ -366,11 +378,26 @@ int at_send_cmd(at_dev_t *dev, const char *command, uint32_t timeout); * @param[in] keep_eol true to keep the CR character in the response * @param[in] timeout timeout (in usec) * - * @returns line length on success - * @returns <0 on error + * @retval n line length on success + * @retval <0 on error */ ssize_t at_readline(at_dev_t *dev, char *resp_buf, size_t len, bool keep_eol, uint32_t timeout); +/** + * @brief Read a line from device, skipping a possibly empty line. + * + * @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] timeout timeout (in usec) + * + * @retval n line length on success + * @retval <0 on error + */ +ssize_t at_readline_skip_empty(at_dev_t *dev, char *resp_buf, size_t len, + bool keep_eol, uint32_t timeout); + /** * @brief Drain device input buffer * diff --git a/tests/drivers/at/main.c b/tests/drivers/at/main.c index 4313700423..e424ac8b78 100644 --- a/tests/drivers/at/main.c +++ b/tests/drivers/at/main.c @@ -285,6 +285,67 @@ static int remove_urc(int argc, char **argv) } #endif +static int sneaky_urc(int argc, char **argv) +{ + (void)argc; + (void)argv; + + int res = 0; + char resp_buf[128]; + +#ifdef MODULE_AT_URC + at_urc_t urc = {.cb = _urc_cb, .code = "+CSCON"}; + at_add_urc(&at_dev, &urc); +#endif + + res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=1", US_PER_SEC); + + if (res) { + puts("Error AT+CFUN=1"); + res = 1; + goto exit; + } + + res = at_send_cmd_get_resp_wait_ok(&at_dev, "AT+CEREG?", + "+CEREG:", resp_buf, + sizeof(resp_buf), US_PER_SEC); + if (res < 0) { + puts("Error AT+CEREG?"); + 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"); + 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"); + 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"); + res = 1; + goto exit; + } + + res = 0; +exit: +#ifdef MODULE_AT_URC + at_remove_urc(&at_dev, &urc); +#endif + return res; +} + static const shell_command_t shell_commands[] = { { "init", "Initialize AT device", init }, { "send", "Send a command and wait response", send }, @@ -296,6 +357,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}, #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/sneaky_urc.py new file mode 100755 index 0000000000..7f4d059219 --- /dev/null +++ b/tests/drivers/at/tests-with-config/sneaky_urc.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +# This script simulates a modem sending a "sneaky URC", a URC that happens exactly +# after the command is received, but before the first line of answer is sent. This +# is possible behavior at least on the U-Blox LTE modules. +# +# Running this test requires the board to be connected twice +# - one connection simulating the modem (e.g.: /dev/ttyUSB0), connected to the +# serial device this script is listening to +# - one to access the RIOT CLI (e.g.: debugger, USB or a second serial connection) +# +# How to get it running: +# 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 +# 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 +# +# 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. + +import sys +import pexpect + +baud = sys.argv[1] +ser = sys.argv[2] +tty = pexpect.spawn(f'picocom --baud {baud} {ser}') + +# EOL sent by the device +EOL_IN = '\r' +# EOL to send back to the device +EOL_OUT = '\r\n' +# Command echoing enabled +ECHO_ON = False + +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 + +while True: + try: + idx = tty.expect_exact([CFUN_CMD, CFUN_ERR_CMD, CFUN_CME_CMD, CEREG_CMD, USECMNG_CMD]) + if idx == 0: + print(CFUN_CMD) + tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) + if ECHO_ON: + tty.send(CFUN_CMD) + tty.send(EOL_OUT + "OK" + EOL_OUT) + if idx == 1: + print(CFUN_ERR_CMD) + tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) + if ECHO_ON: + tty.send(CFUN_ERR_CMD) + tty.send(EOL_OUT + "ERROR" + EOL_OUT) + if idx == 2: + print(CFUN_CME_CMD) + tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) + if ECHO_ON: + tty.send(CFUN_CME_CMD) + tty.send(EOL_OUT + "+CME ERROR:" + EOL_OUT) + elif idx == 3: + print(CEREG_CMD) + tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) + if ECHO_ON: + tty.send(CEREG_CMD) + tty.send(EOL_OUT + "+CEREG: 0,1" + EOL_OUT) + tty.send(EOL_OUT + "OK" + EOL_OUT) + elif idx == 4: + print(USECMNG_CMD) + tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) + if ECHO_ON: + tty.send(USECMNG_CMD) + tty.send(">") + except pexpect.EOF: + print("ERROR: EOF") + except pexpect.TIMEOUT: + print("ERROR: TIMEOUT")