From 76bf5646d5fc7e49910ef2bda91bcaf06aa37d42 Mon Sep 17 00:00:00 2001 From: Christopher Collins Date: Wed, 17 Jan 2018 17:59:49 -0800 Subject: [PATCH] subsys: mgmt: Shell transport for SMP (mcumgr). Enable the shell to transport mcumgr SMP requests and responses. Signed-off-by: Christopher Collins --- include/drivers/console/console.h | 2 + include/mgmt/serial.h | 141 +++++++++++++ include/shell/shell.h | 20 ++ subsys/mgmt/serial_util.c | 332 ++++++++++++++++++++++++++++++ subsys/mgmt/smp_shell.c | 80 +++++++ subsys/shell/shell.c | 27 ++- 6 files changed, 599 insertions(+), 3 deletions(-) create mode 100644 include/mgmt/serial.h create mode 100644 subsys/mgmt/serial_util.c create mode 100644 subsys/mgmt/smp_shell.c diff --git a/include/drivers/console/console.h b/include/drivers/console/console.h index 99afe20bf88..63b7798f322 100644 --- a/include/drivers/console/console.h +++ b/include/drivers/console/console.h @@ -21,6 +21,8 @@ extern "C" { struct console_input { /** FIFO uses first 4 bytes itself, reserve space */ int _unused; + /** Whether this is an mcumgr command */ + u8_t is_mcumgr : 1; /** Buffer where the input line is recorded */ char line[CONSOLE_MAX_LINE_LEN]; }; diff --git a/include/mgmt/serial.h b/include/mgmt/serial.h new file mode 100644 index 00000000000..a2c23c2bf47 --- /dev/null +++ b/include/mgmt/serial.h @@ -0,0 +1,141 @@ +/* + * Copyright Runtime.io 2018. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Utility functions used by the UART and shell mcumgr transports. + * + * Mcumgr packets sent over serial are fragmented into frames of 128 bytes or + * fewer. + * + * The initial frame in a packet has the following format: + * offset 0: 0x06 0x09 + * === Begin base64 encoding === + * offset 2: {16-bit packet-length} + * offset ?: {body} + * offset ?: {crc16} (if final frame) + * === End base64 encoding === + * offset ?: 0x0a (newline) + * + * All subsequent frames have the following format: + * offset 0: 0x04 0x14 + * === Begin base64 encoding === + * offset 2: {body} + * offset ?: {crc16} (if final frame) + * === End base64 encoding === + * offset ?: 0x0a (newline) + * + * All integers are represented in big-endian. The packet fields are described + * below: + * + * | Field | Description | + * | -------------- | ------------------------------------------------------- | + * | 0x06 0x09 | Byte pair indicating the start of a packet. | + * | 0x04 0x14 | Byte pair indicating the start of a continuation frame. | + * | Packet length | The combined total length of the *unencoded* body. | + * | Body | The actual SMP data (i.e., 8-byte header and CBOR | + * | | key-value map). | + * | CRC16 | A CRC16 of the *unencoded* body of the entire packet. | + * | | This field is only present in the final frame of a | + * | | packet. | + * | Newline | A 0x0a byte; terminates a frame. | + * + * The packet is fully received when {packet-length} bytes of body has been + * received. + * + * ## CRC details + * + * The CRC16 should be calculated with the following parameters: + * + * | Field | Value | + * | ------------- | ------------- | + * | Polynomial | 0x1021 | + * | Initial Value | 0 | + */ + +#ifndef H_MGMT_SERIAL_ +#define H_MGMT_SERIAL_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define MCUMGR_SERIAL_HDR_PKT 0x0609 +#define MCUMGR_SERIAL_HDR_FRAG 0x0414 +#define MCUMGR_SERIAL_MAX_FRAME 128 + +#define MCUMGR_SERIAL_HDR_PKT_1 (MCUMGR_SERIAL_HDR_PKT >> 8) +#define MCUMGR_SERIAL_HDR_PKT_2 (MCUMGR_SERIAL_HDR_PKT & 0xff) +#define MCUMGR_SERIAL_HDR_FRAG_1 (MCUMGR_SERIAL_HDR_FRAG >> 8) +#define MCUMGR_SERIAL_HDR_FRAG_2 (MCUMGR_SERIAL_HDR_FRAG & 0xff) + +/** + * @brief Maintains state for an incoming mcumgr request packet. + */ +struct mcumgr_serial_rx_ctxt { + /* Contains the partially- or fully-received mcumgr request. Data + * stored in this buffer has already been base64-decoded. + */ + struct net_buf *nb; + + /* Length of full packet, as read from header. */ + u16_t pkt_len; +}; + +/** @typedef mcumgr_serial_tx_cb + * @brief Transmits a chunk of raw response data. + * + * @param data The data to transmit. + * @param len The number of bytes to transmit. + * @param arg An optional argument. + * + * @return 0 on success; negative error code on failure. + */ +typedef int (*mcumgr_serial_tx_cb)(const void *data, int len, void *arg); + +/** + * @brief Processes an mcumgr request fragment received over a serial + * transport. + * + * Processes an mcumgr request fragment received over a serial transport. If + * the fragment is the end of a valid mcumgr request, this function returns a + * net_buf containing the decoded request. It is the caller's responsibility + * to free the net_buf after it has been processed. + * + * @param rx_ctxt The receive context associated with the serial + * transport being used. + * @param frag The incoming fragment to process. + * @param frag_len The length of the fragment, in bytes. + * + * @return A net_buf containing the decoded request if a + * complete and valid request has been + * received. + * NULL if the packet is incomplete or invalid. + */ +struct net_buf *mcumgr_serial_process_frag( + struct mcumgr_serial_rx_ctxt *rx_ctxt, + const u8_t *frag, int frag_len); + +/** + * @brief Encodes and transmits an mcumgr packet over serial. + * + * @param data The mcumgr packet data to send. + * @param len The length of the unencoded mcumgr packet. + * @param cb A callback used to transmit raw bytes. + * @param arg An optional argument to pass to the callback. + * + * @return 0 on success; negative error code on failure. + */ +int mcumgr_serial_tx_pkt(const u8_t *data, int len, mcumgr_serial_tx_cb cb, + void *arg); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/shell/shell.h b/include/shell/shell.h index 176566e985c..1a657bb05bf 100644 --- a/include/shell/shell.h +++ b/include/shell/shell.h @@ -44,6 +44,19 @@ struct shell_module { shell_prompt_function_t prompt; }; +/** @typedef shell_mcumgr_function_t + * @brief Callback that is executed when an mcumgr packet is received over the + * shell. + * + * The packet argument must be exactly what was received over the console, + * except the terminating newline must be replaced with '\0'. + * + * @param line The received mcumgr packet. + * @param arg An optional argument. + * + * @return on success; negative error code on failure. + */ +typedef int (*shell_mcumgr_function_t)(const char *line, void *arg); /** * @brief Kernel Shell API @@ -148,6 +161,13 @@ void shell_register_prompt_handler(shell_prompt_function_t handler); */ void shell_register_default_module(const char *name); +/** @brief Configures a callback for received mcumgr packets. + * + * @param handler The callback to execute when an mcumgr packet is received. + * @param arg An optional argument to pass to the callback. + */ +void shell_register_mcumgr_handler(shell_mcumgr_function_t handler, void *arg); + /** @brief Execute command line. * * Pass command line to shell to execute. The line cannot be a C string literal diff --git a/subsys/mgmt/serial_util.c b/subsys/mgmt/serial_util.c new file mode 100644 index 00000000000..1352d917da7 --- /dev/null +++ b/subsys/mgmt/serial_util.c @@ -0,0 +1,332 @@ +/* + * Copyright Runtime.io 2018. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void mcumgr_serial_free_rx_ctxt(struct mcumgr_serial_rx_ctxt *rx_ctxt) +{ + if (rx_ctxt->nb != NULL) { + mcumgr_buf_free(rx_ctxt->nb); + rx_ctxt->nb = NULL; + } +} + +static u16_t mcumgr_serial_calc_crc(const u8_t *data, int len) +{ + return crc16(data, len, 0x1021, 0, true); +} + +static int mcumgr_serial_parse_op(const u8_t *buf, int len) +{ + u16_t op; + + if (len < sizeof(op)) { + return -EINVAL; + } + + memcpy(&op, buf, sizeof(op)); + op = sys_be16_to_cpu(op); + + if (op != MCUMGR_SERIAL_HDR_PKT && op != MCUMGR_SERIAL_HDR_FRAG) { + return -EINVAL; + } + + return op; +} + +static int mcumgr_serial_extract_len(struct mcumgr_serial_rx_ctxt *rx_ctxt) +{ + if (rx_ctxt->nb->len < 2) { + return -EINVAL; + } + + rx_ctxt->pkt_len = net_buf_pull_be16(rx_ctxt->nb); + return 0; +} + +static int mcumgr_serial_decode_frag(struct mcumgr_serial_rx_ctxt *rx_ctxt, + const u8_t *frag, int frag_len) +{ + int dec_len; + int rc; + + rc = mbedtls_base64_decode(rx_ctxt->nb->data + rx_ctxt->nb->len, + net_buf_tailroom(rx_ctxt->nb), &dec_len, + frag, frag_len); + if (rc != 0) { + return -EINVAL; + } + + rx_ctxt->nb->len += dec_len; + + return 0; +} + +/** + * Processes a received mcumgr frame. + * + * @return true if a complete packet was received; + * false if the frame is invalid or if additional + * fragments are expected. + */ +struct net_buf *mcumgr_serial_process_frag( + struct mcumgr_serial_rx_ctxt *rx_ctxt, + const u8_t *frag, int frag_len) +{ + struct net_buf *nb; + u16_t crc; + u16_t op; + int rc; + + if (rx_ctxt->nb == NULL) { + rx_ctxt->nb = mcumgr_buf_alloc(); + if (rx_ctxt->nb == NULL) { + return NULL; + } + } + + op = mcumgr_serial_parse_op(frag, frag_len); + switch (op) { + case MCUMGR_SERIAL_HDR_PKT: + net_buf_reset(rx_ctxt->nb); + break; + + case MCUMGR_SERIAL_HDR_FRAG: + if (rx_ctxt->nb->len == 0) { + mcumgr_serial_free_rx_ctxt(rx_ctxt); + return NULL; + } + break; + + default: + return NULL; + } + + rc = mcumgr_serial_decode_frag(rx_ctxt, + frag + sizeof(op), + frag_len - sizeof(op)); + if (rc != 0) { + mcumgr_serial_free_rx_ctxt(rx_ctxt); + return NULL; + } + + if (op == MCUMGR_SERIAL_HDR_PKT) { + rc = mcumgr_serial_extract_len(rx_ctxt); + if (rc < 0) { + mcumgr_serial_free_rx_ctxt(rx_ctxt); + return NULL; + } + } + + if (rx_ctxt->nb->len < rx_ctxt->pkt_len) { + /* More fragments expected. */ + return NULL; + } + + if (rx_ctxt->nb->len > rx_ctxt->pkt_len) { + /* Payload longer than indicated in header. */ + mcumgr_serial_free_rx_ctxt(rx_ctxt); + return NULL; + } + + crc = mcumgr_serial_calc_crc(rx_ctxt->nb->data, rx_ctxt->nb->len); + if (crc != 0) { + mcumgr_serial_free_rx_ctxt(rx_ctxt); + return NULL; + } + + /* Packet is complete; strip the CRC. */ + rx_ctxt->nb->len -= 2; + + nb = rx_ctxt->nb; + rx_ctxt->nb = NULL; + return nb; +} + +/** + * Base64-encodes a small chunk of data and transmits it. The data must be no + * larger than three bytes. + */ +static int mcumgr_serial_tx_small(const void *data, int len, + mcumgr_serial_tx_cb cb, void *arg) +{ + u8_t b64[4 + 1]; /* +1 required for null terminator. */ + size_t dst_len; + int rc; + + rc = mbedtls_base64_encode(b64, sizeof(b64), &dst_len, data, len); + assert(rc == 0); + assert(dst_len == 4); + + return cb(b64, 4, arg); +} + +/** + * @brief Transmits a single mcumgr frame over serial. + * + * @param data The frame payload to transmit. This does not + * include a header or CRC. + * @param first Whether this is the first frame in the packet. + * @param len The number of untransmitted data bytes in the + * packet. + * @param crc The 16-bit CRC of the entire packet. + * @param cb A callback used for transmitting raw data. + * @param arg An optional argument that gets passed to the + * callback. + * @param out_data_bytes_txed On success, the number of data bytes + * transmitted gets written here. + * + * @return 0 on success; negative error code on failure. + */ +int mcumgr_serial_tx_frame(const u8_t *data, bool first, int len, + u16_t crc, mcumgr_serial_tx_cb cb, void *arg, + int *out_data_bytes_txed) +{ + u8_t raw[3]; + u16_t u16; + int dst_off; + int src_off; + int rem; + int rc; + + src_off = 0; + dst_off = 0; + + if (first) { + u16 = sys_cpu_to_be16(MCUMGR_SERIAL_HDR_PKT); + } else { + u16 = sys_cpu_to_be16(MCUMGR_SERIAL_HDR_FRAG); + } + + rc = cb(&u16, sizeof(u16), arg); + if (rc != 0) { + return rc; + } + dst_off += 2; + + /* Only the first fragment contains the packet length. */ + if (first) { + u16 = sys_cpu_to_be16(len); + memcpy(raw, &u16, sizeof(u16)); + raw[2] = data[0]; + + rc = mcumgr_serial_tx_small(raw, 3, cb, arg); + if (rc != 0) { + return rc; + } + + src_off++; + dst_off += 4; + } + + while (1) { + if (dst_off >= MCUMGR_SERIAL_MAX_FRAME - 4) { + /* Can't fit any more data in this frame. */ + break; + } + + /* If we have reached the end of the packet, we need to encode + * and send the CRC. + */ + rem = len - src_off; + if (rem == 0) { + raw[0] = (crc & 0xff00) >> 8; + raw[1] = crc & 0x00ff; + rc = mcumgr_serial_tx_small(raw, 2, cb, arg); + if (rc != 0) { + return rc; + } + break; + } + + if (rem == 1) { + raw[0] = data[src_off]; + src_off++; + + raw[1] = (crc & 0xff00) >> 8; + raw[2] = crc & 0x00ff; + rc = mcumgr_serial_tx_small(raw, 3, cb, arg); + if (rc != 0) { + return rc; + } + break; + } + + if (rem == 2) { + raw[0] = data[src_off]; + raw[1] = data[src_off + 1]; + src_off += 2; + + raw[2] = (crc & 0xff00) >> 8; + rc = mcumgr_serial_tx_small(raw, 3, cb, arg); + if (rc != 0) { + return rc; + } + + raw[0] = crc & 0x00ff; + rc = mcumgr_serial_tx_small(raw, 1, cb, arg); + if (rc != 0) { + return rc; + } + break; + } + + /* Otherwise, just encode payload data. */ + memcpy(raw, data + src_off, 3); + rc = mcumgr_serial_tx_small(raw, 3, cb, arg); + if (rc != 0) { + return rc; + } + src_off += 3; + dst_off += 4; + } + + rc = cb("\n", 1, arg); + if (rc != 0) { + return rc; + } + + *out_data_bytes_txed = src_off; + return 0; +} + +int mcumgr_serial_tx_pkt(const u8_t *data, int len, mcumgr_serial_tx_cb cb, + void *arg) +{ + u16_t crc; + int data_bytes_txed; + int src_off; + int rc; + + /* Calculate CRC of entire packet. */ + crc = mcumgr_serial_calc_crc(data, len); + + /* Transmit packet as a sequence of frames. */ + src_off = 0; + while (src_off < len) { + rc = mcumgr_serial_tx_frame(data + src_off, + src_off == 0, + len - src_off, + crc, cb, arg, + &data_bytes_txed); + if (rc != 0) { + return rc; + } + + src_off += data_bytes_txed; + } + + return 0; +} diff --git a/subsys/mgmt/smp_shell.c b/subsys/mgmt/smp_shell.c new file mode 100644 index 00000000000..ec999564e8a --- /dev/null +++ b/subsys/mgmt/smp_shell.c @@ -0,0 +1,80 @@ +/* + * Copyright Runtime.io 2018. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** @file + * @brief Shell transport for the mcumgr SMP protocol. + */ + +#include +#include +#include +#include "net/buf.h" +#include "shell/shell.h" +#include "mgmt/mgmt.h" +#include "mgmt/serial.h" +#include "mgmt/buf.h" +#include "mgmt/smp.h" + +struct device; + +static struct zephyr_smp_transport smp_shell_transport; + +static struct mcumgr_serial_rx_ctxt smp_shell_rx_ctxt; + +/** + * Processes a single line (i.e., a single SMP frame) + */ +static int smp_shell_rx_line(const char *line, void *arg) +{ + struct net_buf *nb; + int line_len; + + /* Strip the trailing newline. */ + line_len = strlen(line) - 1; + + nb = mcumgr_serial_process_frag(&smp_shell_rx_ctxt, line, line_len); + if (nb != NULL) { + zephyr_smp_rx_req(&smp_shell_transport, nb); + } + + return 0; +} + +static u16_t smp_shell_get_mtu(const struct net_buf *nb) +{ + return CONFIG_MCUMGR_SMP_SHELL_MTU; +} + +static int smp_shell_tx_raw(const void *data, int len, void *arg) +{ + /* Cast away const. */ + k_str_out((void *)data, len); + return 0; +} + +static int smp_shell_tx_pkt(struct zephyr_smp_transport *zst, + struct net_buf *nb) +{ + int rc; + + rc = mcumgr_serial_tx_pkt(nb->data, nb->len, smp_shell_tx_raw, NULL); + mcumgr_buf_free(nb); + + return rc; +} + +static int smp_shell_init(struct device *dev) +{ + ARG_UNUSED(dev); + + zephyr_smp_transport_init(&smp_shell_transport, smp_shell_tx_pkt, + smp_shell_get_mtu); + shell_register_mcumgr_handler(smp_shell_rx_line, NULL); + + return 0; +} + +SYS_INIT(smp_shell_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/subsys/shell/shell.c b/subsys/shell/shell.c index 7f971aeba16..c0934e422dc 100644 --- a/subsys/shell/shell.c +++ b/subsys/shell/shell.c @@ -9,7 +9,6 @@ * @brief Console handler implementation of shell.h API */ - #include #include #include @@ -18,6 +17,7 @@ #include #include #include +#include "mgmt/serial.h" #ifdef CONFIG_UART_CONSOLE #include @@ -66,6 +66,9 @@ static struct k_fifo cmds_queue; static shell_cmd_function_t app_cmd_handler; static shell_prompt_function_t app_prompt_handler; +static shell_mcumgr_function_t mcumgr_cmd_handler; +static void *mcumgr_arg; + static const char *get_prompt(void) { if (app_prompt_handler) { @@ -361,6 +364,12 @@ static const struct shell_cmd *get_internal(const char *command) return get_cmd(internal_commands, command); } +void shell_register_mcumgr_handler(shell_mcumgr_function_t handler, void *arg) +{ + mcumgr_cmd_handler = handler; + mcumgr_arg = arg; +} + int shell_exec(char *line) { char *argv[ARGC_MAX + 1], **argv_start = argv; @@ -425,6 +434,8 @@ done: static void shell(void *p1, void *p2, void *p3) { + bool skip_prompt = false; + ARG_UNUSED(p1); ARG_UNUSED(p2); ARG_UNUSED(p3); @@ -434,7 +445,7 @@ static void shell(void *p1, void *p2, void *p3) while (1) { struct console_input *cmd; - if (!no_promt) { + if (!no_promt && !skip_prompt) { printk("%s", get_prompt()); #if defined(CONFIG_NATIVE_POSIX_CONSOLE) /* The native printk driver is line buffered */ @@ -444,7 +455,17 @@ static void shell(void *p1, void *p2, void *p3) cmd = k_fifo_get(&cmds_queue, K_FOREVER); - shell_exec(cmd->line); + /* If the received line is an mcumgr frame, divert it to the + * mcumgr handler. Don't print the shell prompt this time, as + * that will interfere with the mcumgr response. + */ + if (mcumgr_cmd_handler != NULL && cmd->is_mcumgr) { + mcumgr_cmd_handler(cmd->line, mcumgr_arg); + skip_prompt = true; + } else { + shell_exec(cmd->line); + skip_prompt = false; + } k_fifo_put(&avail_queue, cmd); }