//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG #include #include #include #include #include #include #include #include #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); } }