nodemcu-firmware/components/modules/console.c

264 lines
7.5 KiB
C

#include "module.h"
#include "platform.h"
#include "lauxlib.h"
#include "linput.h"
#include "serial_common.h"
#include "task/task.h"
#include "esp_vfs_dev.h"
#include "esp_vfs_cdcacm.h"
#include "esp_vfs_usb_serial_jtag.h"
#include "driver/usb_serial_jtag.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
// Line ending config from Kconfig
#if CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF
# define RX_LINE_ENDINGS_CFG ESP_LINE_ENDINGS_CRLF
#elif CONFIG_NEWLIB_STDIN_LINE_ENDING_CR
# define RX_LINE_ENDINGS_CFG ESP_LINE_ENDINGS_CR
#else
# define RX_LINE_ENDINGS_CFG ESP_LINE_ENDINGS_LF
#endif
#if CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF
# define TX_LINE_ENDINGS_CFG ESP_LINE_ENDINGS_CRLF
#elif CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR
# define TX_LINE_ENDINGS_CFG ESP_LINE_ENDINGS_CR
#else
# define TX_LINE_ENDINGS_CFG ESP_LINE_ENDINGS_LF
#endif
typedef enum { NONINTERACTIVE, INTERACTIVE } console_mode_t;
static serial_input_cfg_t *cb_cfg;
static task_handle_t feed_lua_task;
// --- Console input task related -----------------------------------
static void console_feed_lua(task_param_t param, task_prio_t prio)
{
(void)prio;
char c = (char)param;
if (run_input)
feed_lua_input(&c, 1);
if (serial_input_has_data_cb(cb_cfg))
serial_input_feed_data(cb_cfg, &c, 1);
// The IDF doesn't seem to honor setvbuf(stdout, NULL, _IONBF, 0) :(
fflush(stdout);
fsync(fileno(stdout));
}
static void console_task(void *)
{
for (;;)
{
// TODO: Support linenoise editing here as an option?
// The run_input switch would need to also control whether we do
// linenoise or raw byte input, to allow for binary xfers.
// But, we would have a big race condition here as the execution
// of the last line happens after we've already started reading the
// next one. We'd have to use a newer version of linenoise than what
// the IDF has, so we get the async interface. Plus switch everything to
// using select() before picking which input method we're using.
// For the race condition, would it be sufficient to wait for the next
// next prompt display to be reasonably certain it's switched?
// But even the prompt handling would be problematic with linenoise as
// that's fixed on linenoiseEditStart(). To solve that we'd need to
// be running the console within the LVM task, synchronously, but we
// can't do that because we need the LVM accessible to handle events.
// These are incompatible design constraints, sigh.
/* We can't use a large read buffer here as some console choices
* (e.g. usb-serial-jtag) don't support read timeouts/partial reads,
* which breaks the echo support and makes for a bad user experience.
*/
char c;
ssize_t n = read(fileno(stdin), &c, 1);
if (n > 0 && (run_input || serial_input_has_data_cb(cb_cfg)))
{
if (!task_post_block_high(feed_lua_task, (task_param_t)c))
{
NODE_ERR("Lost console input data?!\n");
}
}
}
}
static void console_init(void)
{
fflush(stdout);
fsync(fileno(stdout));
/* Disable buffering */
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
/* Disable non-blocking mode */
fcntl(fileno(stdin), F_SETFL, 0);
fcntl(fileno(stdout), F_SETFL, 0);
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
/* Based on console/advanced example */
esp_vfs_dev_uart_port_set_rx_line_endings(
CONFIG_ESP_CONSOLE_UART_NUM, RX_LINE_ENDINGS_CFG);
esp_vfs_dev_uart_port_set_tx_line_endings(
CONFIG_ESP_CONSOLE_UART_NUM, TX_LINE_ENDINGS_CFG);
/* 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_ESP_CONSOLE_UART_BAUDRATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
#if SOC_UART_SUPPORT_REF_TICK
.source_clk = UART_SCLK_REF_TICK,
#elif SOC_UART_SUPPORT_XTAL_CLK
.source_clk = UART_SCLK_XTAL,
#endif
};
/* Install UART driver for interrupt-driven reads and writes */
uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, 256, 0, 0, NULL, 0);
uart_param_config(CONFIG_ESP_CONSOLE_UART_NUM, &uart_config);
/* Tell VFS to use UART driver */
esp_vfs_dev_uart_use_driver(CONFIG_ESP_CONSOLE_UART_NUM);
#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG
/* Based on @pjsg's work */
esp_vfs_dev_usb_serial_jtag_set_rx_line_endings(RX_LINE_ENDINGS_CFG);
esp_vfs_dev_usb_serial_jtag_set_tx_line_endings(TX_LINE_ENDINGS_CFG);
usb_serial_jtag_driver_config_t usb_serial_jtag_config =
USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT();
/* Install USB-SERIAL-JTAG driver for interrupt-driven reads and write */
usb_serial_jtag_driver_install(&usb_serial_jtag_config);
esp_vfs_usb_serial_jtag_use_driver();
#elif CONFIG_ESP_CONSOLE_USB_CDC
/* Based on console/advanced_usb_cdc */
esp_vfs_dev_cdcacm_set_rx_line_endings(RX_LINE_ENDINGS_CFG);
esp_vfs_dev_cdcacm_set_tx_line_endings(TX_LINE_ENDINGS_CFG);
#else
# error "Unsupported console type"
#endif
xTaskCreate(
console_task, "console", configMINIMAL_STACK_SIZE,
NULL, ESP_TASK_MAIN_PRIO+1, NULL);
}
// --- Lua interface related ----------------------------------------
static int retrying_write(const char *buf, size_t len)
{
size_t written = 0;
while (written < len)
{
// At least the USB-Serial-JTAG appears to silently drop characters
// sometimes when writing more than 255 bytes, so we break such strings
// up into multiple calls as a workaround.
const size_t MAX_LEN = 255;
size_t left = len - written;
size_t to_write = left > MAX_LEN ? MAX_LEN : left;
size_t n = fwrite(buf + written, 1, to_write, stdout);
// Additionally, we have to explicitly flush after each chunk we've written.
fflush(stdout);
fsync(fileno(stdout));
if (n > 0)
written += n;
else if (ferror(stdout))
break;
else
vTaskDelay(1);
}
return written;
}
// Lua: console.on("method", [number/char], function)
static int console_on(lua_State *L)
{
return serial_input_register(L, cb_cfg);
}
// Lua: console.mode(onoff)
static int console_mode(lua_State *L)
{
switch (luaL_checkint(L, 1))
{
case NONINTERACTIVE: run_input = false; break;
case INTERACTIVE: run_input = true; break;
default: luaL_error(L, "invalid mode");
}
return 0;
}
// Lua: console.write(str_or_num [, str_or_num2 ... ])
static int console_write(lua_State *L)
{
int total = lua_gettop(L);
for (int s = 1; s <= total; ++s)
{
if (lua_type(L, s) == LUA_TSTRING)
{
size_t len = 0;
const char *buf = lua_tolstring(L, s, &len);
retrying_write(buf, len);
}
else if (lua_isnumber(L, s))
{
int n = lua_tointeger(L, s);
if (n < 0 || n > 255)
return luaL_error(L, "invalid number");
char ch = n;
retrying_write(&ch, 1);
}
}
return 0;
}
// Module function map
LROT_BEGIN(console, NULL, 0)
LROT_FUNCENTRY( mode, console_mode )
LROT_FUNCENTRY( on, console_on )
LROT_FUNCENTRY( write, console_write )
LROT_NUMENTRY( INTERACTIVE, INTERACTIVE )
LROT_NUMENTRY( NONINTERACTIVE, NONINTERACTIVE )
LROT_END(console, NULL, 0)
int luaopen_console( lua_State *L ) {
cb_cfg = serial_input_new();
if (!cb_cfg)
return luaL_error(L, "out of mem");
feed_lua_task = task_get_id(console_feed_lua);
console_init();
return 0;
}
NODEMCU_MODULE(CONSOLE, "console", console, luaopen_console);