You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
12 KiB
418 lines
12 KiB
//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
|
|
|
|
#include <utils.h>
|
|
#include <stdlib.h>
|
|
#include <esp_log.h>
|
|
#include <driver/uart.h>
|
|
#include <errno.h>
|
|
#include <esp_vfs_dev.h>
|
|
#include <fcntl.h>
|
|
#include <settings.h>
|
|
#include "console_ioimpl.h"
|
|
#include "tasks.h"
|
|
#include "telnet_parser.h"
|
|
#include "cmd_common.h"
|
|
|
|
static const char *TAG = "console-io";
|
|
|
|
void console_internal_error_print(const char *msg) {
|
|
ESP_LOGE(TAG, "CONSOLE ERR: %s", msg);
|
|
}
|
|
|
|
void console_setup_uart_stdio(void)
|
|
{
|
|
assert(CONFIG_ESP_CONSOLE_UART_NUM == UART_NUM_0);
|
|
|
|
/* Minicom, screen, idf_monitor send CR when ENTER key is pressed */
|
|
esp_vfs_dev_uart_port_set_rx_line_endings(UART_NUM_0, ESP_LINE_ENDINGS_CR);
|
|
/* Move the caret to the beginning of the next line on '\n' */
|
|
esp_vfs_dev_uart_port_set_tx_line_endings(UART_NUM_0, ESP_LINE_ENDINGS_CRLF); // this is the default anyway
|
|
|
|
/* Disable buffering on stdin and stdout */
|
|
setvbuf(stdin, NULL, _IONBF, 0);
|
|
setvbuf(stdout, NULL, _IONBF, 0);
|
|
|
|
// fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK); // make input non-blocking
|
|
|
|
#if 0
|
|
/* Configure UART. Note that REF_TICK is used so that the baud rate remains
|
|
* correct while APB frequency is changing in light sleep mode.
|
|
*/
|
|
const uart_config_t uart_config = {
|
|
.baud_rate = CONFIG_CONSOLE_UART_BAUDRATE,
|
|
.data_bits = UART_DATA_8_BITS,
|
|
.parity = UART_PARITY_DISABLE,
|
|
.stop_bits = UART_STOP_BITS_1,
|
|
.use_ref_tick = true
|
|
};
|
|
ESP_ERROR_CHECK( uart_param_config(CONFIG_CONSOLE_UART_NUM, &uart_config) );
|
|
#endif
|
|
|
|
/* Install UART driver for interrupt-driven reads and writes */
|
|
ESP_ERROR_CHECK(uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM,
|
|
/* rxbuf */ 256, /* txbuf */ 0, /* que */ 0, /* uart que */ NULL, /* alloc flags */ 0));
|
|
|
|
uart_flush(CONFIG_ESP_CONSOLE_UART_NUM);
|
|
|
|
/* Tell VFS to use UART driver */
|
|
esp_vfs_dev_uart_use_driver(CONFIG_ESP_CONSOLE_UART_NUM);
|
|
}
|
|
|
|
static void my_console_task_freertos(void *param) {
|
|
console_ctx_t *ctx = param;
|
|
assert(CONSOLE_CTX_MAGIC == ctx->__internal_magic); // just make sure it's OK
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(50)); // ??
|
|
|
|
bool logged_in = true;
|
|
{
|
|
struct console_ioimpl *io = ctx->ioctx;
|
|
assert(CONSOLE_IOIMPL_MAGIC == io->__magic);
|
|
|
|
if (io->kind == CONSOLE_IO_TELNET) {
|
|
const size_t pwlen = strnlen(gSettings.console_pw, CONSOLE_PW_LEN);
|
|
if (pwlen != 0) {
|
|
ESP_LOGE(TAG, "Pw=\"%.*s\"", pwlen, gSettings.console_pw);
|
|
|
|
console_print_ctx(&io->ctx, "Password: ");
|
|
|
|
// Make the prompt fancy with asterisks and stuff. Backspace is not supported.
|
|
int pos = 0;
|
|
char buf[CONSOLE_PW_LEN] = {/*zeros*/};
|
|
while (1) {
|
|
char ch = 0;
|
|
console_read_ctx(ctx, &ch, 1);
|
|
if (ch == 10 || ch == 13) {
|
|
console_write_ctx(ctx, "\n", 1);
|
|
break;
|
|
}
|
|
if (ch >= 32 && ch <= 126) {
|
|
if (pos < CONSOLE_PW_LEN) {
|
|
buf[pos++] = ch;
|
|
}
|
|
console_write_ctx(ctx, "*", 1);
|
|
}
|
|
}
|
|
|
|
if (0 == strncmp(buf, gSettings.console_pw, CONSOLE_PW_LEN)) {
|
|
console_print_ctx(&io->ctx, "Login OK!\n");
|
|
} else {
|
|
console_print_ctx(&io->ctx, "Login failed!\n");
|
|
logged_in = false;
|
|
if (io->telnet.tcpcli) {
|
|
tcpd_kick(io->telnet.tcpcli);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (logged_in) {
|
|
console_print_motd(ctx);
|
|
console_task(param);
|
|
}
|
|
|
|
ESP_LOGD(TAG, "Console task ended");
|
|
|
|
// This delay should ensure the TCP client is shut down completely
|
|
// before we proceed to free stuff. The delay is deliberately very generous,
|
|
// we are in no rush here.
|
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
|
|
|
// Deallocate what console allocated inside ctx, ctx is part of ioimpl so it will NOT be freed
|
|
ESP_LOGD(TAG, "Clear console context");
|
|
console_ctx_destroy(ctx);
|
|
|
|
ESP_LOGD(TAG, "Clear IO context");
|
|
struct console_ioimpl *io = ctx->ioctx;
|
|
assert(CONSOLE_IOIMPL_MAGIC == io->__magic);
|
|
|
|
// Tear down IO
|
|
if (io->kind == CONSOLE_IO_TELNET) {
|
|
vRingbufferDelete(io->telnet.console_stdin_ringbuf);
|
|
}
|
|
|
|
// Free ioimpl
|
|
free(io);
|
|
|
|
ESP_LOGI(TAG, "Console task shutdown.");
|
|
// suicide the task
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
static void telnet_shutdown_handler(console_ctx_t *ctx) {
|
|
assert(ctx);
|
|
assert(CONSOLE_CTX_MAGIC == ctx->__internal_magic); // just make sure it's OK
|
|
struct console_ioimpl *io = ctx->ioctx;
|
|
assert(io);
|
|
assert(CONSOLE_IOIMPL_MAGIC == io->__magic);
|
|
|
|
tcpd_kick(io->telnet.tcpcli);
|
|
}
|
|
|
|
/**
|
|
* Start console working with stdin and stdout
|
|
*/
|
|
static esp_err_t console_start_io(struct console_ioimpl **pIo, TaskHandle_t * hdl, enum console_iokind kind, TcpdClient_t client) {
|
|
struct console_ioimpl * io = calloc(1, sizeof(struct console_ioimpl));
|
|
if (io == NULL) {
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
if (pIo != NULL) {
|
|
*pIo = io;
|
|
}
|
|
|
|
io->__magic = CONSOLE_IOIMPL_MAGIC;
|
|
|
|
io->kind = kind;
|
|
|
|
if (kind == CONSOLE_IO_TELNET) {
|
|
io->telnet.console_stdin_ringbuf = xRingbufferCreate(CONSOLE_BUFSIZE, RINGBUF_TYPE_BYTEBUF);
|
|
if (NULL == io->telnet.console_stdin_ringbuf) {
|
|
ESP_LOGE(TAG, "Failed to create RB!");
|
|
free(io);
|
|
if (pIo != NULL) {
|
|
*pIo = NULL;
|
|
}
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
io->telnet.tcpcli = client;
|
|
|
|
// Store the "io" reference as "cctx" in the TCP client, so we know
|
|
// where to write received data in the callback
|
|
tcpd_set_client_ctx(client, io);
|
|
|
|
} else {
|
|
io->files.inf = stdin;
|
|
io->files.outf = stdout;
|
|
}
|
|
|
|
// using "static" allocation - context is part of the io struct
|
|
// Here "io" is stored as "ioctx" in "ctx" and then passed to the read/write functions
|
|
if (NULL == console_ctx_init(&io->ctx, io)) {
|
|
ESP_LOGE(TAG, "Console init failed!");
|
|
goto fail;
|
|
}
|
|
|
|
if (kind == CONSOLE_IO_FILES) {
|
|
io->ctx.exit_allowed = false;
|
|
} else {
|
|
/* TELNET */
|
|
io->ctx.shutdown_handler = telnet_shutdown_handler;
|
|
}
|
|
|
|
assert(io->ctx.__internal_magic == CONSOLE_CTX_MAGIC);
|
|
|
|
snprintf(io->ctx.prompt, CONSOLE_PROMPT_MAX_LEN, "\x1b[36;1m> \x1b[m");
|
|
|
|
if (pdPASS != xTaskCreate(
|
|
my_console_task_freertos, // func
|
|
"console", // name
|
|
CONSOLE_TASK_STACK, // stack
|
|
&io->ctx, // param
|
|
CONSOLE_TASK_PRIO, // prio
|
|
hdl // handle dest
|
|
)) {
|
|
ESP_LOGE(TAG, "Err create console task!");
|
|
goto fail;
|
|
}
|
|
|
|
return ESP_OK;
|
|
|
|
fail:
|
|
ESP_LOGE(TAG, "console_start_io FAILED, freeing resources!");
|
|
console_ctx_destroy(&io->ctx);
|
|
|
|
if (kind == CONSOLE_IO_TELNET) {
|
|
vRingbufferDelete(io->telnet.console_stdin_ringbuf);
|
|
io->telnet.console_stdin_ringbuf = NULL;
|
|
}
|
|
|
|
free(io);
|
|
if (pIo != NULL) {
|
|
*pIo = NULL;
|
|
}
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
esp_err_t console_start_stdio(struct console_ioimpl **pIo, TaskHandle_t * hdl) {
|
|
ESP_LOGI(TAG, "Start STDIO console task");
|
|
|
|
return console_start_io(pIo, hdl, CONSOLE_IO_FILES, NULL);
|
|
}
|
|
|
|
esp_err_t console_start_tcp(struct console_ioimpl **pIo, TaskHandle_t * hdl, TcpdClient_t client) {
|
|
ESP_LOGI(TAG, "Start TCP console task");
|
|
|
|
return console_start_io(pIo, hdl, CONSOLE_IO_TELNET, client);
|
|
}
|
|
|
|
/**
|
|
* Write to console context.
|
|
*
|
|
* Return number of characters written, -1 on error.
|
|
*/
|
|
int console_write_ctx(console_ctx_t *ctx, const char *text, size_t len) {
|
|
struct console_ioimpl *io = ctx->ioctx;
|
|
assert(CONSOLE_IOIMPL_MAGIC == io->__magic);
|
|
|
|
if (io->kind == CONSOLE_IO_TELNET) {
|
|
const char *wp = (const char *)text;
|
|
const char *sp = (const char *)text;
|
|
int towrite = 0;
|
|
|
|
// hack to allow using bare \n
|
|
for (size_t i = 0; i < len; i++) {
|
|
char c = *sp++;
|
|
if (c == '\n') {
|
|
// LF: print the chunk before it and a CR.
|
|
if (towrite > 0) {
|
|
if (ESP_OK != tcpd_send(io->telnet.tcpcli, (uint8_t *) wp, towrite)) {
|
|
return -1;
|
|
}
|
|
}
|
|
if (ESP_OK != tcpd_send(io->telnet.tcpcli, (uint8_t*) "\r", 1)) {
|
|
return -1;
|
|
}
|
|
// The LF gets rolled into the next chunk.
|
|
wp = sp - 1;
|
|
towrite = 1;
|
|
} else {
|
|
// Non-LF character is printed as is
|
|
towrite++;
|
|
}
|
|
}
|
|
|
|
// Send the leftovers (chars from last LF or from the start)
|
|
if (towrite > 0) {
|
|
if (ESP_OK != tcpd_send(io->telnet.tcpcli, (uint8_t*) wp, towrite)) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return len;
|
|
} else {
|
|
// File IO
|
|
errno = 0;
|
|
// the UART driver takes care of encoding \n as \r\n
|
|
size_t n = fwrite(text, 1, len, io->files.outf);
|
|
if (n != len) {
|
|
if (errno || ferror(io->files.outf)) {
|
|
return -1;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read from console context's input stream.
|
|
*
|
|
* Return number of characters read, -1 on error
|
|
*/
|
|
int console_read_ctx(console_ctx_t *ctx, char *dest, size_t count) {
|
|
if (!console_have_stdin_ctx(ctx)) {
|
|
ESP_LOGW(TAG, "Console stream has no stdin!");
|
|
return -1;
|
|
}
|
|
|
|
struct console_ioimpl *io = ctx->ioctx;
|
|
assert(CONSOLE_IOIMPL_MAGIC == io->__magic);
|
|
|
|
if (io->kind == CONSOLE_IO_TELNET) {
|
|
size_t remain = count;
|
|
char *wp = dest;
|
|
do {
|
|
size_t rcount = 0;
|
|
uint8_t *chunk = xRingbufferReceiveUpTo(io->telnet.console_stdin_ringbuf, &rcount, portMAX_DELAY, remain);
|
|
|
|
// telnet options negotiation
|
|
rcount = telnet_middleware_read(ctx, chunk, rcount);
|
|
|
|
if (rcount > 0) {
|
|
memcpy(wp, chunk, rcount);
|
|
wp += rcount;
|
|
remain -= rcount;
|
|
}
|
|
|
|
vRingbufferReturnItem(io->telnet.console_stdin_ringbuf, chunk);
|
|
} while (remain > 0);
|
|
|
|
return count;
|
|
} else {
|
|
// File IO
|
|
errno = 0;
|
|
// clearerr(io->files.inf);
|
|
size_t r = fread(dest, 1, count, io->files.inf);
|
|
|
|
if (errno != 0 || feof(io->files.inf) /*|| ferror(io->files.inf)*/) {
|
|
ESP_LOGW(TAG, "Console stream EOF or error.");
|
|
perror("Read err");
|
|
return -1;
|
|
}
|
|
|
|
return r;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if console input stream has bytes ready.
|
|
*
|
|
* @return number of queued bytes, 0 if none, -1 on error.
|
|
*/
|
|
int console_can_read_ctx(console_ctx_t *ctx) {
|
|
if (!console_have_stdin_ctx(ctx)) {
|
|
ESP_LOGW(TAG, "Console stream has no stdin!");
|
|
return -1;
|
|
}
|
|
|
|
struct console_ioimpl *io = ctx->ioctx;
|
|
assert(CONSOLE_IOIMPL_MAGIC == io->__magic);
|
|
|
|
if (io->kind == CONSOLE_IO_TELNET) {
|
|
uint32_t nitems = 0;
|
|
vRingbufferGetInfo(io->telnet.console_stdin_ringbuf, NULL, NULL, NULL, NULL, &nitems);
|
|
return nitems;
|
|
} else {
|
|
// File IO
|
|
|
|
// a hack with select - this is used rarely, we can afford the overhead
|
|
fd_set readfds;
|
|
FD_ZERO(&readfds);
|
|
FD_SET(fileno(io->files.inf), &readfds);
|
|
struct timeval timeout = {0, 0};
|
|
int sel_rv = select(1, &readfds, NULL, NULL, &timeout);
|
|
if (sel_rv > 0) {
|
|
return 1; // at least one
|
|
} else if (sel_rv == -1) {
|
|
ESP_LOGW(TAG, "Console stream EOF or error.");
|
|
return -1; // error
|
|
} else {
|
|
return 0; // nothing
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test if console context is not NULL and has stdin stream available
|
|
*
|
|
* @return have stdin
|
|
*/
|
|
bool console_have_stdin_ctx(console_ctx_t *ctx) {
|
|
if (!ctx->ioctx) return false;
|
|
struct console_ioimpl *io = ctx->ioctx;
|
|
assert(CONSOLE_IOIMPL_MAGIC == io->__magic);
|
|
|
|
if (io->kind == CONSOLE_IO_TELNET) {
|
|
return io->telnet.tcpcli != NULL &&
|
|
io->telnet.console_stdin_ringbuf != NULL;
|
|
} else {
|
|
// File IO
|
|
// ESP_LOGW(TAG, "%p, %d, %d", io->files.inf,
|
|
// feof(io->files.inf),
|
|
// ferror(io->files.inf));
|
|
|
|
return io->files.inf != NULL &&
|
|
!feof(io->files.inf);
|
|
}
|
|
}
|
|
|