1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2025-12-18 11:03:50 +01:00

drivers/at: parse +CME/+CMS responses and save error value

This commit is contained in:
Mihai Renea 2024-01-12 16:52:06 +01:00
parent d83ec632e3
commit c58b71b899
6 changed files with 429 additions and 162 deletions

View File

@ -41,6 +41,7 @@
#include <assert.h> #include <assert.h>
#include <errno.h> #include <errno.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "at.h" #include "at.h"
@ -77,6 +78,18 @@ static void _event_process_urc(event_t *_event)
} }
#endif #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) static void _isrpipe_write_one_wrapper(void *_dev, uint8_t data)
{ {
at_dev_t *dev = (at_dev_t *) _dev; at_dev_t *dev = (at_dev_t *) _dev;
@ -88,18 +101,50 @@ static void _isrpipe_write_one_wrapper(void *_dev, uint8_t data)
#endif #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) #if IS_USED(MODULE_AT_URC_ISR)
dev->awaiting_response = false; dev->awaiting_response = false;
dev->event.handler = _event_process_urc; dev->event.handler = _event_process_urc;
#endif #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) 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; return -1;
} }
if (at_expect_bytes(dev, CONFIG_AT_SEND_EOL AT_RECV_EOL_1 AT_RECV_EOL_2, if (at_expect_bytes(dev, CONFIG_AT_SEND_EOL, timeout)) {
timeout)) {
return -2; 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;
ssize_t res_ok; ssize_t res_ok;
char ok_buf[64];
at_drain(dev); at_drain(dev);
res = at_send_cmd(dev, command, timeout); res = at_send_cmd(dev, command, timeout);
if (res) { if (res) {
goto out; return res;
} }
/* URCs may occur right after the command has been sent and before the /* URCs may occur right after the command has been sent and before the
* expected response */ * expected response */
do { while ((res = at_readline_skip_empty(dev, resp_buf, len, false, timeout)) >= 0) {
res = at_readline_skip_empty(dev, resp_buf, len, false, timeout); if (!resp_prefix || *resp_prefix == '\0') {
break;
}
/* Strip the expected prefix */ /* Strip the expected prefix */
if (res > 0 && resp_prefix && *resp_prefix) { size_t prefix_len = strlen(resp_prefix);
size_t prefix_len = strlen(resp_prefix); if (starts_with(resp_buf, resp_prefix)) {
if (strncmp(resp_buf, resp_prefix, prefix_len) == 0) { size_t remaining_len = strlen(resp_buf) - prefix_len;
size_t remaining_len = strlen(resp_buf) - prefix_len; /* The one extra byte in the copy is the terminating nul byte */
/* The one extra byte in the copy is the terminating nul byte */ memmove(resp_buf, resp_buf + prefix_len, remaining_len + 1);
memmove(resp_buf, resp_buf + prefix_len, remaining_len + 1); res -= prefix_len;
res -= prefix_len; break;
break;
}
} }
} while (res >= 0); res = at_parse_resp(dev, resp_buf);
if (res == 0) {
/* wait for OK */ /* empty response */
if (res >= 0) { return 0;
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 (res < 0) {
if ((len_ok != 0) && (strcmp(ok_buf, CONFIG_AT_RECV_OK) == 0)) { return res;
} }
#if IS_USED(MODULE_AT_URC)
else { else {
/* Something else than OK */ clist_foreach(&dev->urc_list, _check_urc, resp_buf);
res = -1;
} }
#endif
} }
out: if (res < 0) {
return res; 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, ssize_t at_send_cmd_get_lines(at_dev_t *dev, const char *command, char *resp_buf,
char *resp_buf, size_t len, bool keep_eol, uint32_t timeout) size_t len, bool keep_eol, uint32_t timeout)
{ {
const char eol[] = AT_RECV_EOL_1 AT_RECV_EOL_2; const char eol[] = AT_RECV_EOL_1 AT_RECV_EOL_2;
assert(sizeof(eol) > 1); 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); res = at_send_cmd(dev, command, timeout);
if (res) { if (res) {
goto out; return res;
} }
memset(resp_buf, '\0', len); memset(resp_buf, '\0', len);
while (1) { bool first_line = true;
res = at_readline(dev, pos, bytes_left, keep_eol, timeout); 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 (res == 0) {
if (bytes_left) { *pos++ = eol[sizeof(eol) - 2];
*pos++ = eol[sizeof(eol) - 2]; bytes_left--;
bytes_left--;
}
continue; continue;
} }
else if (res > 0) { else if (res > 0) {
bytes_left -= res; size_t const res_len = res;
size_t len_ok = sizeof(CONFIG_AT_RECV_OK) - 1; bytes_left -= res_len;
size_t len_error = sizeof(CONFIG_AT_RECV_ERROR) - 1; res = at_parse_resp(dev, pos);
if (((size_t )res == (len_ok + keep_eol)) &&
(len_ok != 0) && switch (res) {
(strncmp(pos, CONFIG_AT_RECV_OK, len_ok) == 0)) { case 0: /* OK */
res = len - bytes_left; res = len - bytes_left;
break; return res;
} case 1: /* response or URC */
else if (((size_t )res == (len_error + keep_eol)) && pos += res_len;
(len_error != 0) && if (bytes_left == 0) {
(strncmp(pos, CONFIG_AT_RECV_ERROR, len_error) == 0)) { return -ENOBUFS;
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;
} }
*pos++ = eol[sizeof(eol) - 2];
bytes_left--;
continue;
default: /* <0 */
return res;
} }
} }
else { else {
break; return res;
} }
} }
out: return -ENOBUFS;
return res;
} }
int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout) int at_send_cmd_wait_prompt(at_dev_t *dev, const char *command, uint32_t timeout)
{ {
unsigned cmdlen = strlen(command);
at_drain(dev); at_drain(dev);
uart_write(dev->uart, (const uint8_t *)command, cmdlen); int res = at_send_cmd(dev, command, timeout);
uart_write(dev->uart, (const uint8_t *)CONFIG_AT_SEND_EOL, AT_SEND_EOL_LEN); if (res) {
return res;
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;
}
} }
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 at_send_cmd_wait_ok(at_dev_t *dev, const char *command, uint32_t timeout)
{ {
int res; int res;
char resp_buf[64];
res = at_send_cmd_get_resp(dev, command, resp_buf, sizeof(resp_buf), res = at_send_cmd_get_resp(dev, command, dev->rp_buf, dev->rp_buf_size, timeout);
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;
while (res >= 0) { while (res >= 0) {
if (strncmp(resp_buf, CONFIG_AT_RECV_OK, len_ok) == 0) { res = at_parse_resp(dev, dev->rp_buf);
return 0; 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 #ifdef MODULE_AT_URC
clist_foreach(&dev->urc_list, _check_urc, resp_buf); clist_foreach(&dev->urc_list, _check_urc, dev->rp_buf);
#endif #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; 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; const char eol[] = AT_RECV_EOL_1 AT_RECV_EOL_2;
assert(sizeof(eol) > 1); assert(sizeof(eol) > 1);
ssize_t res = -1; ssize_t res = 0;
char *resp_pos = resp_buf; char *resp_pos = resp_buf;
#if IS_USED(MODULE_AT_URC_ISR) #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 #endif
memset(resp_buf, 0, len); memset(resp_buf, 0, len);
size_t substr_len = 0;
while (len) { if (substr) {
substr_len = strlen(substr);
if (substr_len == 0) {
return -EINVAL;
}
}
char const *substr_p = resp_buf;
while (len > 1) {
int read_res; int read_res;
if ((read_res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)resp_pos, if ((read_res = isrpipe_read_timeout(&dev->isrpipe, (uint8_t *)resp_pos,
1, timeout)) == 1) { 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]) { if (*resp_pos == eol[sizeof(eol) - 2]) {
*resp_pos = '\0'; *resp_pos = '\0';
res = resp_pos - resp_buf; break;
goto out;
} }
resp_pos += read_res; resp_pos += read_res;
len -= 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) { else if (read_res == -ETIMEDOUT) {
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) #if IS_USED(MODULE_AT_URC_ISR)
dev->awaiting_response = false; dev->awaiting_response = false;
#endif #endif
if (res < 0) { if (res < 0) {
*resp_buf = '\0'; *resp_buf = '\0';
} else {
res = resp_pos - resp_buf;
} }
return res; 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, ssize_t at_readline_skip_empty(at_dev_t *dev, char *resp_buf, size_t len,
bool keep_eol, uint32_t timeout) bool keep_eol, uint32_t timeout)
{ {
ssize_t res = at_readline(dev, resp_buf, len, keep_eol, timeout); return at_readline_skip_empty_stop_at_str(dev, resp_buf, len, keep_eol, NULL, timeout);
if (res == 0) {
/* skip possible empty line */
res = at_readline(dev, resp_buf, len, keep_eol, timeout);
}
return res;
} }
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 #ifdef MODULE_AT_URC
void at_add_urc(at_dev_t *dev, at_urc_t *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); 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); urc->cb(urc->arg, buf);
return 1; return 1;
} }

View File

@ -17,10 +17,10 @@
* intended to send, and bail out if there's no match. * intended to send, and bail out if there's no match.
* *
* Furthermore, the library tries to cope with difficulties regarding different * Furthermore, the library tries to cope with difficulties regarding different
* line endings. It usually sends "<command><CR>", but expects * line endings. It usually sends `<command><CR>`, but expects
* "<command>\LF\CR" as echo. * `<command>\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. * byte gets printed.
* *
* ## Unsolicited Result Codes (URC) ## * ## Unsolicited Result Codes (URC) ##
@ -44,6 +44,24 @@
* character is detected and there is no pending response. This works by posting * 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. * 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: <err_code>` for other commands
*
* For `+CME`, this behavior is usually off by default and can be toggled with:
* `AT+CMEE=<type>`
* where `<type>` may be:
* - 0 disable extended error reporting, return `ERROR`
* - 1 enable extended error reporting, with `<err_code>` integer
* - 2 enable extended error reporting, with `<err_code>` 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 `<err_code>`
* can be retrieved by calling @ref at_get_err_info().
*
* @{ * @{
* *
* @file * @file
@ -168,6 +186,9 @@ typedef struct {
#endif /* MODULE_AT_URC */ #endif /* MODULE_AT_URC */
/** Error cause can be further investigated. */
#define AT_ERR_EXTENDED 200
/** Shortcut for getting send end of line length */ /** Shortcut for getting send end of line length */
#define AT_SEND_EOL_LEN (sizeof(CONFIG_AT_SEND_EOL) - 1) #define AT_SEND_EOL_LEN (sizeof(CONFIG_AT_SEND_EOL) - 1)
@ -177,6 +198,8 @@ typedef struct {
typedef struct { typedef struct {
isrpipe_t isrpipe; /**< isrpipe used for getting data from uart */ isrpipe_t isrpipe; /**< isrpipe used for getting data from uart */
uart_t uart; /**< UART device where the AT device is attached */ 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 #ifdef MODULE_AT_URC
clist_node_t urc_list; /**< list to keep track of all registered urc's */ clist_node_t urc_list; /**< list to keep track of all registered urc's */
#ifdef MODULE_AT_URC_ISR #ifdef MODULE_AT_URC_ISR
@ -186,19 +209,49 @@ typedef struct {
#endif #endif
} at_dev_t; } 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: <err_code>`.
* 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 * @brief Initialize AT device struct
* *
* @param[in] dev struct to initialize * @param[in] dev struct to initialize
* @param[in] uart UART the device is connected to * @param[in] init init struct, may be destroyed after return
* @param[in] baudrate baudrate of the device
* @param[in] buf input buffer
* @param[in] bufsize size of @p buf
* *
* @retval success code UART_OK on success * @retval success code UART_OK on success
* @retval error code UART_NODEV or UART_NOBAUD otherwise * @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 * @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) * @param[in] timeout timeout (in usec)
* *
* @retval 0 when device answers "OK" * @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); 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 (>) * This function sends the supplied @p command, then waits for the prompt (>)
* character and returns * character and returns
* *
* @param[in] dev device to operate on * @param[in] dev device to operate on
* @param[in] command command string to send * @param[in] command command string to send
* @param[in] timeout timeout (in usec) * @param[in] timeout timeout (in usec)
* *
* @retval 0 when prompt is received * @retval 0 when prompt is received
* @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_prompt(at_dev_t *dev, const char *command, uint32_t timeout); 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 n length of response on success
* @retval <0 on error * @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 * @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) * @param[in] timeout timeout (in usec)
* *
* @retval n length of response on success * @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, 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); 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 * This function sends the supplied @p command, then returns all response
* lines until the device sends "OK". * lines until the device sends "OK".
* *
* If a line starts with "ERROR" or the buffer is full, the function returns -1. * If a line contains a DTE error response, this function stops and returns
* If a line starts with "+CME ERROR" or +CMS ERROR", the function returns -2. * accordingly.
* In this case resp_buf contains the error string.
* *
* @param[in] dev device to operate on * @param[in] dev device to operate on
* @param[in] command command to send * @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) * @param[in] timeout timeout (in usec)
* *
* @retval n length of response on success * @retval n length of response on success
* @retval -1 on error * @retval -AT_ERR_EXTENDED if failed and a error code can be retrieved with
* @retval -2 on CMS or CME error * @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, 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, 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); 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: <err>`, and `<err>`
* has been successfully copied to @p dev->rp_buf
* @retval -1 if the response is "ERROR", or `+CMx ERROR: <err>` but `<err>`
* could not be copied
* @retval 1 otherwise
*/
int at_parse_resp(at_dev_t *dev, char const *resp);
/** /**
* @brief Read a line from device * @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, ssize_t at_readline_skip_empty(at_dev_t *dev, char *resp_buf, size_t len,
bool keep_eol, uint32_t timeout); 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 * @brief Drain device input buffer
* *

View File

@ -2,7 +2,14 @@ include ../Makefile.drivers_common
USEMODULE += shell USEMODULE += shell
USEMODULE += at 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 # we are printing from the event thread, we need more stack
CFLAGS += -DEVENT_THREAD_MEDIUM_STACKSIZE=1024 CFLAGS += -DEVENT_THREAD_MEDIUM_STACKSIZE=1024

View File

@ -20,6 +20,7 @@
* @} * @}
*/ */
#include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -33,6 +34,7 @@
static at_dev_t at_dev; static at_dev_t at_dev;
static char buf[256]; static char buf[256];
static char resp[1024]; static char resp[1024];
static char rp_buf[256];
static int init(int argc, char **argv) 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); printf("Wrong UART device number - should be in range 0-%d.\n", UART_NUMOF - 1);
return 1; return 1;
} }
at_dev_init_t at_init_params = {
int res = at_dev_init(&at_dev, UART_DEV(uart), baudrate, buf, sizeof(buf)); .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 */ /* check the UART initialization return value and respond as needed */
if (res == UART_NODEV) { if (res == UART_NODEV) {
puts("Invalid UART device given!"); puts("Invalid UART device given!");
@ -110,7 +118,8 @@ static int send_lines(int argc, char **argv)
} }
ssize_t len; 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"); puts("Error");
return 1; return 1;
} }
@ -285,7 +294,7 @@ static int remove_urc(int argc, char **argv)
} }
#endif #endif
static int sneaky_urc(int argc, char **argv) static int emulate_dce(int argc, char **argv)
{ {
(void)argc; (void)argc;
(void)argv; (void)argv;
@ -299,9 +308,21 @@ static int sneaky_urc(int argc, char **argv)
#endif #endif
res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=1", US_PER_SEC); res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=1", US_PER_SEC);
if (res) { 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; res = 1;
goto exit; goto exit;
} }
@ -310,39 +331,72 @@ static int sneaky_urc(int argc, char **argv)
"+CEREG:", resp_buf, "+CEREG:", resp_buf,
sizeof(resp_buf), US_PER_SEC); sizeof(resp_buf), US_PER_SEC);
if (res < 0) { if (res < 0) {
puts("Error AT+CEREG?"); printf("%u: Error AT+CEREG?: %d\n", __LINE__, res);
res = 1; res = 1;
goto exit; goto exit;
} }
res = at_send_cmd_wait_prompt(&at_dev, "AT+USECMNG=0,0,\"cert\",128", US_PER_SEC); res = at_send_cmd_wait_prompt(&at_dev, "AT+USECMNG=0,0,\"cert\",128", US_PER_SEC);
if (res) { 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; res = 1;
goto exit; goto exit;
} }
res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=8", US_PER_SEC); res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=8", US_PER_SEC);
if (res != -1) { if (res != -1) {
puts("Error AT+CFUN=8"); printf("%u: Error AT+CFUN=8: %d\n", __LINE__, res);
res = 1; res = 1;
goto exit; goto exit;
} }
res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=9", US_PER_SEC); res = at_send_cmd_wait_ok(&at_dev, "AT+CFUN=9", US_PER_SEC);
if (res != -AT_ERR_EXTENDED) {
if (res != -2) { printf("%u: Error AT+CFUN=9: %d\n", __LINE__, res);
puts("Error AT+CFUN=9");
res = 1; res = 1;
goto exit; 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; res = 0;
exit: exit:
#ifdef MODULE_AT_URC #ifdef MODULE_AT_URC
at_remove_urc(&at_dev, &urc); at_remove_urc(&at_dev, &urc);
#endif #endif
printf("%s finished with %d\n", __func__, res);
return res; return res;
} }
@ -357,7 +411,7 @@ static const shell_command_t shell_commands[] = {
{ "drain", "Drain AT device", drain }, { "drain", "Drain AT device", drain },
{ "power_on", "Power on AT device", power_on }, { "power_on", "Power on AT device", power_on },
{ "power_off", "Power off AT device", power_off }, { "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 #ifdef MODULE_AT_URC
{ "add_urc", "Register an URC", add_urc }, { "add_urc", "Register an URC", add_urc },
{ "remove_urc", "De-register an URC", remove_urc }, { "remove_urc", "De-register an URC", remove_urc },

View File

@ -13,17 +13,27 @@
# 1. Adapt the `EOL_IN`, `EOL_OUT`, `ECHO_ON` variables below to match your use case # 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 # 2. Run this script with the baud rate and the serial dev the device is connected
# to, e.g.: # to, e.g.:
# $ ./sneaky_urc 115200 /dev/ttyUSB0 # $ ./emulate_dce.py 115200 /dev/ttyUSB0
# 4. run the test (e.g. make term) # 4. run the test (e.g. make term)
# 5. inside the test console: # 5. inside the test console:
# a) run the `init` command (e.g. init 0 115200) # 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 # 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). # 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 # If command echoing is enabled AND `MODULE_AT_URC` is defined, you should see
# *some* of the URCs being parsed. # *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 sys
import pexpect import pexpect
@ -37,17 +47,20 @@ EOL_IN = '\r'
# EOL to send back to the device # EOL to send back to the device
EOL_OUT = '\r\n' EOL_OUT = '\r\n'
# Command echoing enabled # Command echoing enabled
ECHO_ON = False ECHO_ON = True
CFUN_CMD = "AT+CFUN=1" + EOL_IN CFUN_CMD = "AT+CFUN=1" + EOL_IN
CFUN_ERR_CMD = "AT+CFUN=8" + EOL_IN CFUN_ERR_CMD = "AT+CFUN=8" + EOL_IN
CFUN_CME_CMD = "AT+CFUN=9" + EOL_IN CFUN_CME_CMD = "AT+CFUN=9" + EOL_IN
CEREG_CMD = "AT+CEREG?" + EOL_IN CEREG_CMD = "AT+CEREG?" + EOL_IN
USECMNG_CMD = "AT+USECMNG=0,0,\"cert\",128" + 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: while True:
try: 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: if idx == 0:
print(CFUN_CMD) print(CFUN_CMD)
tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT)
@ -65,7 +78,7 @@ while True:
tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT)
if ECHO_ON: if ECHO_ON:
tty.send(CFUN_CME_CMD) 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: elif idx == 3:
print(CEREG_CMD) print(CEREG_CMD)
tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT) tty.send(EOL_OUT + "+CSCON: 1" + EOL_OUT)
@ -79,6 +92,16 @@ while True:
if ECHO_ON: if ECHO_ON:
tty.send(USECMNG_CMD) tty.send(USECMNG_CMD)
tty.send(">") 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: except pexpect.EOF:
print("ERROR: EOF") print("ERROR: EOF")
except pexpect.TIMEOUT: except pexpect.TIMEOUT:

View File

@ -0,0 +1,2 @@
init 0 115200
emulate_dce