net: websocket: Add console support

Add console driver that allows console session to be transferred
over a websocket connection.

Signed-off-by: Jukka Rissanen <jukka.rissanen@linux.intel.com>
This commit is contained in:
Jukka Rissanen 2017-08-31 12:28:40 +03:00
commit ead9cd409c
7 changed files with 556 additions and 0 deletions

View file

@ -8,3 +8,4 @@ zephyr_sources_if_kconfig(uart_pipe.c)
zephyr_sources_if_kconfig(telnet_console.c)
zephyr_sources_if_kconfig(xtensa_sim_console.c)
zephyr_sources_if_kconfig(native_posix_console.c)
zephyr_sources_if_kconfig(websocket_console.c)

View file

@ -312,4 +312,5 @@ config NATIVE_POSIX_CONSOLE_INIT_PRIORITY
Device driver initialization priority.
source "drivers/console/Kconfig.telnet"
source "drivers/console/Kconfig.ws"
endif

112
drivers/console/Kconfig.ws Normal file
View file

@ -0,0 +1,112 @@
# Kconfig - console driver configuration options
#
# Copyright (c) 2017 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
menuconfig WEBSOCKET_CONSOLE
bool "Enable websocket console service"
default n
select NETWORKING
select NET_TCP
select HTTP_PARSER
select HTTP_SERVER
select WEBSOCKET
help
This option enables console over a websocket link. Currently,
it is basically just a redirection of the Zephyr console through
websocket. It nicely works along with another console driver (like
uart), twist being that it will take over the output only if a
successful connection to its HTTP service is done.
if WEBSOCKET_CONSOLE
config WEBSOCKET_CONSOLE_LINE_BUF_SIZE
int "WS console line buffer size"
default 128
help
This option can be used to modify the size of the buffer storing
console output line, prior to sending it through the network.
Of course an output line can be longer than such size, it just
means sending it will start as soon as it reaches this size.
It really depends on what type of output is expected.
If there is a lot of short lines, then lower this value. If there
are longer lines, then raise this value.
config WEBSOCKET_CONSOLE_LINE_BUF_NUMBERS
int "WS console line buffers"
default 4
help
This option can be used to modify the amount of line buffers the
driver can use. It really depends on how much output is meant to be
sent, depending on the system load etc. You can play on both
WEBSOCKET_CONSOLE_LINE_BUF_SIZE and this current option to get the
best possible buffer settings you need.
config WEBSOCKET_CONSOLE_SEND_TIMEOUT
int "WS console line send timeout"
default 100
help
This option can be used to modify the duration of the timer that kick
in when a line buffer is not empty but did not yet meet the line feed.
config WEBSOCKET_CONSOLE_SEND_THRESHOLD
int "WS console line send threshold"
default 5
help
This option can be used to modify the minimal amount of a line buffer
that can be sent by the WS server when nothing has happened for
a little while (see WEBSOCKET_CONSOLE_SEND_TIMEOUT) and when the line
buffer did not meet the line feed yet.
config WEBSOCKET_CONSOLE_STACK_SIZE
int "WS console inner thread stack size"
default 1500
help
This option helps to fine-tune WS console inner thread stack size.
config WEBSOCKET_CONSOLE_PRIO
int "WS console inner thread priority"
default 7
help
This option helps to fine-tune WS console inner thread priority.
config SYS_LOG_WEBSOCKET_CONSOLE_LEVEL
int "WS console log level"
default 0
depends on SYS_LOG
help
Sets log level for websocket console (for WS console dev only)
Levels are:
- 0 OFF, do not write
- 1 ERROR, only write SYS_LOG_ERR
- 2 WARNING, write SYS_LOG_WRN in addition to previous level
- 3 INFO, write SYS_LOG_INF in addition to previous levels
- 4 DEBUG, write SYS_LOG_DBG in addition to previous levels
config WEBSOCKET_CONSOLE_DEBUG_DEEP
bool "Forward output to original console handler"
depends on UART_CONSOLE
default n
help
For WS console developers only, this will forward each output to
original console handler. So if by chance WS console seems silent,
at least things will be printed to original handler, usually
UART console.
config WEBSOCKET_CONSOLE_INIT_PRIORITY
int "WS console init priority"
default 99
help
WS console driver initialization priority. Note that WS works
on application level. Usually, you won't have to tweak this.
endif

View file

@ -0,0 +1,339 @@
/*
* Copyright (c) 2017 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief Websocket console
*
*
* Websocket console driver. The console is provided over
* a websocket connection.
*/
#define SYS_LOG_LEVEL CONFIG_SYS_LOG_WEBSOCKET_CONSOLE_LEVEL
#define SYS_LOG_DOMAIN "ws/console"
#include <logging/sys_log.h>
#include <zephyr.h>
#include <init.h>
#include <misc/printk.h>
#include <console/console.h>
#include <net/buf.h>
#include <net/net_pkt.h>
#include <net/websocket_console.h>
#define NVT_NUL 0
#define NVT_LF 10
#define NVT_CR 13
#define WS_CONSOLE_STACK_SIZE CONFIG_WEBSOCKET_CONSOLE_STACK_SIZE
#define WS_CONSOLE_PRIORITY CONFIG_WEBSOCKET_CONSOLE_PRIO
#define WS_CONSOLE_TIMEOUT K_MSEC(CONFIG_WEBSOCKET_CONSOLE_SEND_TIMEOUT)
#define WS_CONSOLE_LINES CONFIG_WEBSOCKET_CONSOLE_LINE_BUF_NUMBERS
#define WS_CONSOLE_LINE_SIZE CONFIG_WEBSOCKET_CONSOLE_LINE_BUF_SIZE
#define WS_CONSOLE_TIMEOUT K_MSEC(CONFIG_WEBSOCKET_CONSOLE_SEND_TIMEOUT)
#define WS_CONSOLE_THRESHOLD CONFIG_WEBSOCKET_CONSOLE_SEND_THRESHOLD
#define WS_CONSOLE_MIN_MSG 2
/* These 2 structures below are used to store the console output
* before sending it to the client. This is done to keep some
* reactivity: the ring buffer is non-protected, if first line has
* not been sent yet, and if next line is reaching the same index in rb,
* the first one will be replaced. In a perfect world, this should
* not happen. However on a loaded system with a lot of debug output
* this is bound to happen eventualy, moreover if it does not have
* the luxury to bufferize as much as it wants to. Just raise
* CONFIG_WEBSOCKET_CONSOLE_LINE_BUF_NUMBERS if possible.
*/
struct line_buf {
char buf[WS_CONSOLE_LINE_SIZE];
u16_t len;
};
struct line_buf_rb {
struct line_buf l_bufs[WS_CONSOLE_LINES];
u16_t line_in;
u16_t line_out;
};
static struct line_buf_rb ws_rb;
NET_STACK_DEFINE(WS_CONSOLE, ws_console_stack,
WS_CONSOLE_STACK_SIZE, WS_CONSOLE_STACK_SIZE);
static struct k_thread ws_thread_data;
static K_SEM_DEFINE(send_lock, 0, UINT_MAX);
/* The timer is used to send non-lf terminated output that has
* been around for "tool long". This will prove to be useful
* to send the shell prompt for instance.
* ToDo: raise the time, incrementaly, when no output is coming
* so the timer will kick in less and less.
*/
static void ws_send_prematurely(struct k_timer *timer);
static K_TIMER_DEFINE(send_timer, ws_send_prematurely, NULL);
static int (*orig_printk_hook)(int);
static struct k_fifo *avail_queue;
static struct k_fifo *input_queue;
/* Websocket context that this console is related to */
static struct http_ctx *ws_console;
extern void __printk_hook_install(int (*fn)(int));
extern void *__printk_get_hook(void);
void ws_register_input(struct k_fifo *avail, struct k_fifo *lines,
u8_t (*completion)(char *str, u8_t len))
{
ARG_UNUSED(completion);
avail_queue = avail;
input_queue = lines;
}
static void ws_rb_init(void)
{
int i;
ws_rb.line_in = 0;
ws_rb.line_out = 0;
for (i = 0; i < WS_CONSOLE_LINES; i++) {
ws_rb.l_bufs[i].len = 0;
}
}
static void ws_end_client_connection(struct http_ctx *console)
{
__printk_hook_install(orig_printk_hook);
orig_printk_hook = NULL;
k_timer_stop(&send_timer);
ws_send_msg(console, NULL, 0, WS_OPCODE_CLOSE, false, true,
NULL, NULL);
ws_rb_init();
}
static void ws_rb_switch(void)
{
ws_rb.line_in++;
if (ws_rb.line_in == WS_CONSOLE_LINES) {
ws_rb.line_in = 0;
}
ws_rb.l_bufs[ws_rb.line_in].len = 0;
/* Unfortunately, we don't have enough line buffer,
* so we eat the next to be sent.
*/
if (ws_rb.line_in == ws_rb.line_out) {
ws_rb.line_out++;
if (ws_rb.line_out == WS_CONSOLE_LINES) {
ws_rb.line_out = 0;
}
}
k_timer_start(&send_timer, WS_CONSOLE_TIMEOUT, WS_CONSOLE_TIMEOUT);
k_sem_give(&send_lock);
}
static inline struct line_buf *ws_rb_get_line_out(void)
{
u16_t out = ws_rb.line_out;
ws_rb.line_out++;
if (ws_rb.line_out == WS_CONSOLE_LINES) {
ws_rb.line_out = 0;
}
if (!ws_rb.l_bufs[out].len) {
return NULL;
}
return &ws_rb.l_bufs[out];
}
static inline struct line_buf *ws_rb_get_line_in(void)
{
return &ws_rb.l_bufs[ws_rb.line_in];
}
/* The actual printk hook */
static int ws_console_out(int c)
{
int key = irq_lock();
struct line_buf *lb = ws_rb_get_line_in();
bool yield = false;
lb->buf[lb->len++] = (char)c;
if (c == '\n' || lb->len == WS_CONSOLE_LINE_SIZE - 1) {
lb->buf[lb->len-1] = NVT_CR;
lb->buf[lb->len++] = NVT_LF;
ws_rb_switch();
yield = true;
}
irq_unlock(key);
#ifdef CONFIG_WEBSOCKET_CONSOLE_DEBUG_DEEP
/* This is ugly, but if one wants to debug websocket console, it
* will also output the character to original console
*/
orig_printk_hook(c);
#endif
if (yield) {
k_yield();
}
return c;
}
static void ws_send_prematurely(struct k_timer *timer)
{
struct line_buf *lb = ws_rb_get_line_in();
if (lb->len >= WS_CONSOLE_THRESHOLD) {
ws_rb_switch();
}
}
static inline void ws_handle_input(struct net_pkt *pkt)
{
struct console_input *input;
u16_t len, offset, pos;
len = net_pkt_appdatalen(pkt);
if (len > CONSOLE_MAX_LINE_LEN || len < WS_CONSOLE_MIN_MSG) {
return;
}
if (!avail_queue || !input_queue) {
return;
}
input = k_fifo_get(avail_queue, K_NO_WAIT);
if (!input) {
return;
}
offset = net_pkt_get_len(pkt) - len;
net_frag_read(pkt->frags, offset, &pos, len, (u8_t *)input->line);
/* The data from websocket does not contain \n or NUL, so insert
* it here.
*/
input->line[len] = NVT_NUL;
/* LF/CR will be removed if only the line is not NUL terminated */
if (input->line[len-1] != NVT_NUL) {
if (input->line[len-1] == NVT_LF) {
input->line[len-1] = NVT_NUL;
}
if (input->line[len-2] == NVT_CR) {
input->line[len-2] = NVT_NUL;
}
}
k_fifo_put(input_queue, input);
}
/* The data is coming from outside system and going into zephyr */
int ws_console_recv(struct http_ctx *ctx, struct net_pkt *pkt)
{
if (ctx != ws_console) {
return -ENOENT;
}
ws_handle_input(pkt);
net_pkt_unref(pkt);
return 0;
}
/* This is for transferring data from zephyr to outside system */
static bool ws_console_send(struct http_ctx *console)
{
struct line_buf *lb = ws_rb_get_line_out();
if (lb) {
(void)ws_send_msg(console, (u8_t *)lb->buf, lb->len,
WS_OPCODE_DATA_TEXT, false, true,
NULL, NULL);
/* We reinitialize the line buffer */
lb->len = 0;
}
return true;
}
/* WS console loop, used to send buffered output in the RB */
static void ws_console_run(void)
{
while (true) {
k_sem_take(&send_lock, K_FOREVER);
if (!ws_console_send(ws_console)) {
ws_end_client_connection(ws_console);
}
}
}
int ws_console_enable(struct http_ctx *ctx)
{
orig_printk_hook = __printk_get_hook();
__printk_hook_install(ws_console_out);
k_timer_start(&send_timer, WS_CONSOLE_TIMEOUT, WS_CONSOLE_TIMEOUT);
ws_console = ctx;
return 0;
}
int ws_console_disable(struct http_ctx *ctx)
{
if (!ws_console) {
return 0;
}
if (ws_console != ctx) {
return -ENOENT;
}
ws_end_client_connection(ws_console);
ws_console = NULL;
return 0;
}
static int ws_console_init(struct device *arg)
{
k_thread_create(&ws_thread_data, ws_console_stack,
K_THREAD_STACK_SIZEOF(ws_console_stack),
(k_thread_entry_t)ws_console_run,
NULL, NULL, NULL,
K_PRIO_COOP(WS_CONSOLE_PRIORITY), 0, K_MSEC(10));
SYS_LOG_INF("Websocket console initialized");
return 0;
}
/* Websocket console is initialized as an application directly, as it requires
* the whole network stack to be ready.
*/
SYS_INIT(ws_console_init, APPLICATION, CONFIG_WEBSOCKET_CONSOLE_INIT_PRIORITY);

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2017 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __WS_CONSOLE_H__
#define __WS_CONSOLE_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <kernel.h>
/** @brief Register websocket input processing
*
* Input processing is started when string is received in WS server.
* Carriage return is translated to NULL making string always NULL
* terminated. Application before calling register function need to
* initialize two fifo queues mentioned below.
*
* @param avail k_fifo queue keeping available input slots
* @param lines k_fifo queue of entered lines which to be processed
* in the application code.
* @param completion callback for tab completion of entered commands
*
* @return N/A
*/
void ws_register_input(struct k_fifo *avail, struct k_fifo *lines,
u8_t (*completion)(char *str, u8_t len));
#ifdef __cplusplus
}
#endif
#endif /* __WS_CONSOLE_H__ */

View file

@ -0,0 +1,60 @@
/*
* Copyright (c) 2017 Intel Corporation.
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __WEBSOCKET_CONSOLE_H__
#define __WEBSOCKET_CONSOLE_H__
#include <net/websocket.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Websocket console library
* @defgroup websocket_console Websocket Console Library
* @{
*/
/**
* @brief Enable websocket console.
*
* @details The console can be sent over websocket to browser.
*
* @param ctx HTTP context
*
* @return 0 if ok, <0 if error
*/
int ws_console_enable(struct http_ctx *ctx);
/**
* @brief Disable websocket console.
*
* @param ctx HTTP context
*
* @return 0 if ok, <0 if error
*/
int ws_console_disable(struct http_ctx *ctx);
/**
* @brief Receive data from outside system and feed it into Zephyr.
*
* @param ctx HTTP context
* @param pkt Network packet containing the received data.
*
* @return 0 if ok, <0 if error
*/
int ws_console_recv(struct http_ctx *ctx, struct net_pkt *pkt);
#ifdef __cplusplus
}
#endif
/**
* @}
*/
#endif /* __WEBSOCKET_CONSOLE_H__ */

View file

@ -28,6 +28,9 @@
#ifdef CONFIG_NATIVE_POSIX_CONSOLE
#include <console/native_posix_console.h>
#endif
#ifdef CONFIG_WEBSOCKET_CONSOLE
#include <console/websocket_console.h>
#endif
#include <shell/shell.h>
@ -638,6 +641,9 @@ void shell_init(const char *str)
#ifdef CONFIG_NATIVE_POSIX_STDIN_CONSOLE
native_stdin_register_input(&avail_queue, &cmds_queue, completion);
#endif
#ifdef CONFIG_WEBSOCKET_CONSOLE
ws_register_input(&avail_queue, &cmds_queue, completion);
#endif
}
/** @brief Optionally register an app default cmd handler.