/** @file * @brief Modem command handler for modem context driver * * Text-based command handler implementation for modem context driver. */ /* * Copyright (c) 2019-2020 Foundries.io * * SPDX-License-Identifier: Apache-2.0 */ #include LOG_MODULE_REGISTER(modem_cmd_handler, CONFIG_MODEM_LOG_LEVEL); #include #include #include #include "modem_context.h" #include "modem_cmd_handler.h" /* * Parsing Functions */ static bool is_crlf(uint8_t c) { if (c == '\n' || c == '\r') { return true; } else { return false; } } static void skipcrlf(struct modem_cmd_handler_data *data) { while (data->rx_buf && data->rx_buf->len && is_crlf(*data->rx_buf->data)) { net_buf_pull_u8(data->rx_buf); if (!data->rx_buf->len) { data->rx_buf = net_buf_frag_del(NULL, data->rx_buf); } } } static uint16_t findcrlf(struct modem_cmd_handler_data *data, struct net_buf **frag, uint16_t *offset) { struct net_buf *buf = data->rx_buf; uint16_t len = 0U, pos = 0U; while (buf && buf->len && !is_crlf(*(buf->data + pos))) { if (pos + 1 >= buf->len) { len += buf->len; buf = buf->frags; pos = 0U; } else { pos++; } } if (buf && buf->len && is_crlf(*(buf->data + pos))) { len += pos; *offset = pos; *frag = buf; return len; } return 0; } static bool starts_with(struct net_buf *buf, const char *str) { int pos = 0; while (buf && buf->len && *str) { if (*(buf->data + pos) == *str) { str++; pos++; if (pos >= buf->len) { buf = buf->frags; pos = 0; } } else { return false; } } if (*str == 0) { return true; } return false; } /* * Cmd Handler Functions */ static inline struct net_buf *read_rx_allocator(k_timeout_t timeout, void *user_data) { return net_buf_alloc((struct net_buf_pool *)user_data, timeout); } /* return scanned length for params */ static int parse_params(struct modem_cmd_handler_data *data, size_t match_len, const struct modem_cmd *cmd, uint8_t **argv, size_t argv_len, uint16_t *argc) { int count = 0; size_t delim_len, begin, end, i; bool quoted = false; if (!data || !data->match_buf || !match_len || !cmd || !argv || !argc) { return -EINVAL; } begin = cmd->cmd_len; end = cmd->cmd_len; delim_len = strlen(cmd->delim); while (end < match_len) { /* Don't look for delimiters in the middle of a quoted parameter */ if (data->match_buf[end] == '"') { quoted = !quoted; } if (quoted) { end++; continue; } /* Look for delimiter characters */ for (i = 0; i < delim_len; i++) { if (data->match_buf[end] == cmd->delim[i]) { /* mark a parameter beginning */ argv[*argc] = &data->match_buf[begin]; /* end parameter with NUL char */ data->match_buf[end] = '\0'; /* bump begin */ begin = end + 1; count += 1; (*argc)++; break; } } if (count >= cmd->arg_count_max) { break; } if (*argc == argv_len) { break; } end++; } /* consider the ending portion a param if end > begin */ if (end > begin) { /* mark a parameter beginning */ argv[*argc] = &data->match_buf[begin]; /* end parameter with NUL char * NOTE: if this is at the end of match_len will probably * be overwriting a NUL that's already there */ data->match_buf[end] = '\0'; (*argc)++; } /* missing arguments */ if (*argc < cmd->arg_count_min) { /* Do not return -EAGAIN here as there is no way new argument * can be parsed later because match_len is computed to be * the minimum of the distance to the first CRLF and the size * of the buffer. * Therefore, waiting more data on the interface won't change * match_len value, which mean there is no point in waiting * for more arguments, this will just end in a infinite loop * parsing data and finding that some arguments are missing. */ return -EINVAL; } /* * return the beginning of the next unfinished param so we don't * "skip" any data that could be parsed later. */ return begin - cmd->cmd_len; } /* process a "matched" command */ static int process_cmd(const struct modem_cmd *cmd, size_t match_len, struct modem_cmd_handler_data *data) { int parsed_len = 0, ret = 0; uint8_t *argv[CONFIG_MODEM_CMD_HANDLER_MAX_PARAM_COUNT]; uint16_t argc = 0U; /* reset params */ memset(argv, 0, sizeof(argv[0]) * ARRAY_SIZE(argv)); /* do we need to parse arguments? */ if (cmd->arg_count_max > 0U) { /* returns < 0 on error and > 0 for parsed len */ parsed_len = parse_params(data, match_len, cmd, argv, ARRAY_SIZE(argv), &argc); if (parsed_len < 0) { return parsed_len; } } /* skip cmd_len + parsed len */ data->rx_buf = net_buf_skip(data->rx_buf, cmd->cmd_len + parsed_len); /* call handler */ if (cmd->func) { ret = cmd->func(data, match_len - cmd->cmd_len - parsed_len, argv, argc); if (ret == -EAGAIN) { /* wait for more data */ net_buf_push(data->rx_buf, cmd->cmd_len + parsed_len); } } return ret; } /* * check 3 arrays of commands for a match in match_buf: * - response handlers[0] * - unsolicited handlers[1] * - current assigned handlers[2] */ static const struct modem_cmd *find_cmd_match( struct modem_cmd_handler_data *data) { int j; size_t i; for (j = 0; j < ARRAY_SIZE(data->cmds); j++) { if (!data->cmds[j] || data->cmds_len[j] == 0U) { continue; } for (i = 0; i < data->cmds_len[j]; i++) { /* match on "empty" cmd */ if (strlen(data->cmds[j][i].cmd) == 0 || strncmp(data->match_buf, data->cmds[j][i].cmd, data->cmds[j][i].cmd_len) == 0) { return &data->cmds[j][i]; } } } return NULL; } static const struct modem_cmd *find_cmd_direct_match( struct modem_cmd_handler_data *data) { size_t j, i; for (j = 0; j < ARRAY_SIZE(data->cmds); j++) { if (!data->cmds[j] || data->cmds_len[j] == 0U) { continue; } for (i = 0; i < data->cmds_len[j]; i++) { /* match start of cmd */ if (data->cmds[j][i].direct && (data->cmds[j][i].cmd[0] == '\0' || starts_with(data->rx_buf, data->cmds[j][i].cmd))) { return &data->cmds[j][i]; } } } return NULL; } static int cmd_handler_process_iface_data(struct modem_cmd_handler_data *data, struct modem_iface *iface) { struct net_buf *last; size_t bytes_read = 0; int ret; if (!data->rx_buf) { data->rx_buf = net_buf_alloc(data->buf_pool, data->alloc_timeout); if (!data->rx_buf) { /* there is potentially more data waiting */ return -ENOMEM; } } last = net_buf_frag_last(data->rx_buf); /* read all of the data from modem iface */ while (true) { struct net_buf *frag = last; size_t frag_room = net_buf_tailroom(frag); if (!frag_room) { frag = net_buf_alloc(data->buf_pool, data->alloc_timeout); if (!frag) { /* there is potentially more data waiting */ return -ENOMEM; } net_buf_frag_insert(last, frag); last = frag; frag_room = net_buf_tailroom(frag); } ret = iface->read(iface, net_buf_tail(frag), frag_room, &bytes_read); if (ret < 0 || bytes_read == 0) { /* modem context buffer is empty */ return 0; } net_buf_add(frag, bytes_read); } } static void cmd_handler_process_rx_buf(struct modem_cmd_handler_data *data) { const struct modem_cmd *cmd; struct net_buf *frag = NULL; size_t match_len; int ret; uint16_t offset, len; /* process all of the data in the net_buf */ while (data->rx_buf && data->rx_buf->len) { skipcrlf(data); if (!data->rx_buf || !data->rx_buf->len) { break; } cmd = find_cmd_direct_match(data); if (cmd && cmd->func) { ret = cmd->func(data, cmd->cmd_len, NULL, 0); if (ret == -EAGAIN) { /* Wait for more data */ break; } else if (ret > 0) { LOG_DBG("match direct cmd [%s] (ret:%d)", cmd->cmd, ret); data->rx_buf = net_buf_skip(data->rx_buf, ret); } continue; } frag = NULL; /* locate next CR/LF */ len = findcrlf(data, &frag, &offset); if (!frag) { /* * No CR/LF found. Let's exit and leave any data * for next time */ break; } /* load match_buf with content up to the next CR/LF */ /* NOTE: keep room in match_buf for ending NUL char */ match_len = net_buf_linearize(data->match_buf, data->match_buf_len - 1, data->rx_buf, 0, len); if ((data->match_buf_len - 1) < match_len) { LOG_ERR("Match buffer size (%zu) is too small for " "incoming command size: %zu! Truncating!", data->match_buf_len - 1, match_len); } #if defined(CONFIG_MODEM_CONTEXT_VERBOSE_DEBUG) LOG_HEXDUMP_DBG(data->match_buf, match_len, "RECV"); #endif k_sem_take(&data->sem_parse_lock, K_FOREVER); cmd = find_cmd_match(data); if (cmd) { LOG_DBG("match cmd [%s] (len:%zu)", cmd->cmd, match_len); ret = process_cmd(cmd, match_len, data); if (ret == -EAGAIN) { k_sem_give(&data->sem_parse_lock); break; } else if (ret < 0) { LOG_ERR("process cmd [%s] (len:%zu, ret:%d)", cmd->cmd, match_len, ret); } /* * make sure we didn't run out of data during * command processing */ if (!data->rx_buf) { /* we're out of data, exit early */ k_sem_give(&data->sem_parse_lock); break; } frag = NULL; /* * We've handled the current line. * Let's skip any "extra" data in that * line, and look for the next CR/LF. * This leaves us ready for the next * handler search. * Ignore the length returned. */ (void)findcrlf(data, &frag, &offset); } k_sem_give(&data->sem_parse_lock); if (frag && data->rx_buf) { /* clear out processed line (net_buf's) */ while (frag && data->rx_buf != frag) { data->rx_buf = net_buf_frag_del(NULL, data->rx_buf); } net_buf_pull(data->rx_buf, offset); } } } static void cmd_handler_process(struct modem_cmd_handler *cmd_handler, struct modem_iface *iface) { struct modem_cmd_handler_data *data; int err; if (!cmd_handler || !cmd_handler->cmd_handler_data || !iface || !iface->read) { return; } data = (struct modem_cmd_handler_data *)(cmd_handler->cmd_handler_data); do { err = cmd_handler_process_iface_data(data, iface); cmd_handler_process_rx_buf(data); } while (err); } int modem_cmd_handler_get_error(struct modem_cmd_handler_data *data) { if (!data) { return -EINVAL; } return data->last_error; } int modem_cmd_handler_set_error(struct modem_cmd_handler_data *data, int error_code) { if (!data) { return -EINVAL; } data->last_error = error_code; return 0; } int modem_cmd_handler_update_cmds(struct modem_cmd_handler_data *data, const struct modem_cmd *handler_cmds, size_t handler_cmds_len, bool reset_error_flag) { if (!data) { return -EINVAL; } data->cmds[CMD_HANDLER] = handler_cmds; data->cmds_len[CMD_HANDLER] = handler_cmds_len; if (reset_error_flag) { data->last_error = 0; } return 0; } int modem_cmd_send_ext(struct modem_iface *iface, struct modem_cmd_handler *handler, const struct modem_cmd *handler_cmds, size_t handler_cmds_len, const uint8_t *buf, struct k_sem *sem, k_timeout_t timeout, int flags) { struct modem_cmd_handler_data *data; int ret = 0; if (!iface || !handler || !handler->cmd_handler_data || !buf) { return -EINVAL; } if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) { /* semaphore is not needed if there is no timeout */ sem = NULL; } else if (!sem) { /* cannot respect timeout without semaphore */ return -EINVAL; } data = (struct modem_cmd_handler_data *)(handler->cmd_handler_data); if (!(flags & MODEM_NO_TX_LOCK)) { k_sem_take(&data->sem_tx_lock, K_FOREVER); } if (!(flags & MODEM_NO_SET_CMDS)) { ret = modem_cmd_handler_update_cmds(data, handler_cmds, handler_cmds_len, true); if (ret < 0) { goto unlock_tx_lock; } } #if defined(CONFIG_MODEM_CONTEXT_VERBOSE_DEBUG) LOG_HEXDUMP_DBG(buf, strlen(buf), "SENT DATA"); if (data->eol_len > 0) { if (data->eol[0] != '\r') { /* Print the EOL only if it is not \r, otherwise there * is just too much printing. */ LOG_HEXDUMP_DBG(data->eol, data->eol_len, "SENT EOL"); } } else { LOG_DBG("EOL not set!!!"); } #endif if (sem) { k_sem_reset(sem); } iface->write(iface, buf, strlen(buf)); iface->write(iface, data->eol, data->eol_len); if (sem) { ret = k_sem_take(sem, timeout); if (ret == 0) { ret = data->last_error; } else if (ret == -EAGAIN) { ret = -ETIMEDOUT; } } if (!(flags & MODEM_NO_UNSET_CMDS)) { /* unset handlers and ignore any errors */ (void)modem_cmd_handler_update_cmds(data, NULL, 0U, false); } unlock_tx_lock: if (!(flags & MODEM_NO_TX_LOCK)) { k_sem_give(&data->sem_tx_lock); } return ret; } /* run a set of AT commands */ int modem_cmd_handler_setup_cmds(struct modem_iface *iface, struct modem_cmd_handler *handler, const struct setup_cmd *cmds, size_t cmds_len, struct k_sem *sem, k_timeout_t timeout) { int ret = 0; size_t i; for (i = 0; i < cmds_len; i++) { if (cmds[i].handle_cmd.cmd && cmds[i].handle_cmd.func) { ret = modem_cmd_send(iface, handler, &cmds[i].handle_cmd, 1U, cmds[i].send_cmd, sem, timeout); } else { ret = modem_cmd_send(iface, handler, NULL, 0, cmds[i].send_cmd, sem, timeout); } k_sleep(K_MSEC(50)); if (ret < 0) { LOG_ERR("command %s ret:%d", cmds[i].send_cmd, ret); break; } } return ret; } /* run a set of AT commands, without lock */ int modem_cmd_handler_setup_cmds_nolock(struct modem_iface *iface, struct modem_cmd_handler *handler, const struct setup_cmd *cmds, size_t cmds_len, struct k_sem *sem, k_timeout_t timeout) { int ret = 0; size_t i; for (i = 0; i < cmds_len; i++) { if (cmds[i].handle_cmd.cmd && cmds[i].handle_cmd.func) { ret = modem_cmd_send_nolock(iface, handler, &cmds[i].handle_cmd, 1U, cmds[i].send_cmd, sem, timeout); } else { ret = modem_cmd_send_nolock(iface, handler, NULL, 0, cmds[i].send_cmd, sem, timeout); } k_sleep(K_MSEC(50)); if (ret < 0) { LOG_ERR("command %s ret:%d", cmds[i].send_cmd, ret); break; } } return ret; } int modem_cmd_handler_tx_lock(struct modem_cmd_handler *handler, k_timeout_t timeout) { struct modem_cmd_handler_data *data; data = (struct modem_cmd_handler_data *)(handler->cmd_handler_data); return k_sem_take(&data->sem_tx_lock, timeout); } void modem_cmd_handler_tx_unlock(struct modem_cmd_handler *handler) { struct modem_cmd_handler_data *data; data = (struct modem_cmd_handler_data *)(handler->cmd_handler_data); k_sem_give(&data->sem_tx_lock); } int modem_cmd_handler_init(struct modem_cmd_handler *handler, struct modem_cmd_handler_data *data, const struct modem_cmd_handler_config *config) { /* Verify arguments */ if (handler == NULL || data == NULL || config == NULL) { return -EINVAL; } /* Verify config */ if ((config->match_buf == NULL) || (config->match_buf_len == 0) || (config->buf_pool == NULL) || (NULL != config->response_cmds && 0 == config->response_cmds_len) || (NULL != config->unsol_cmds && 0 == config->unsol_cmds_len)) { return -EINVAL; } /* Assign data to command handler */ handler->cmd_handler_data = data; /* Assign command process implementation to command handler */ handler->process = cmd_handler_process; /* Store arguments */ data->match_buf = config->match_buf; data->match_buf_len = config->match_buf_len; data->buf_pool = config->buf_pool; data->alloc_timeout = config->alloc_timeout; data->eol = config->eol; data->cmds[CMD_RESP] = config->response_cmds; data->cmds_len[CMD_RESP] = config->response_cmds_len; data->cmds[CMD_UNSOL] = config->unsol_cmds; data->cmds_len[CMD_UNSOL] = config->unsol_cmds_len; /* Process end of line */ data->eol_len = data->eol == NULL ? 0 : strlen(data->eol); /* Store optional user data */ data->user_data = config->user_data; /* Initialize command handler data members */ k_sem_init(&data->sem_tx_lock, 1, 1); k_sem_init(&data->sem_parse_lock, 1, 1); return 0; }