706 lines
16 KiB
C
706 lines
16 KiB
C
/** @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 <zephyr/logging/log.h>
|
|
LOG_MODULE_REGISTER(modem_cmd_handler, CONFIG_MODEM_LOG_LEVEL);
|
|
|
|
#include <zephyr/kernel.h>
|
|
#include <stddef.h>
|
|
#include <zephyr/net_buf.h>
|
|
|
|
#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;
|
|
}
|