Refactor into new 'console' module.

A breaking change, but should finally see us move away from the chronic edge
cases and inconsistent behaviour we have while trying to shoe-horn the
usb-serial-jtag and cdc-acm consoles into uart behaviour and assumptions.
This commit is contained in:
Jade Mattsson 2024-10-22 10:23:17 +11:00
parent 6be07fbbf2
commit 13553ddf76
12 changed files with 753 additions and 389 deletions

View File

@ -11,47 +11,18 @@
#include "lua.h"
#include "linput.h"
#include "platform.h"
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include "sdkconfig.h"
#include "esp_system.h"
#include "esp_event.h"
#include "esp_spiffs.h"
#include "esp_netif.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 "nvs_flash.h"
#include "task/task.h"
#include "sections.h"
#include "nodemcu_esp_event.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#define SIG_LUA 0
#define SIG_UARTINPUT 1
// 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
#include "freertos/semphr.h"
// We don't get argument size data from the esp_event dispatch, so it's
@ -71,8 +42,6 @@ typedef struct {
static task_handle_t relayed_event_task;
static SemaphoreHandle_t relayed_event_handled;
static task_handle_t lua_feed_task;
// This function runs in the context of the system default event loop RTOS task
static void relay_default_loop_events(
@ -166,129 +135,10 @@ static void nodemcu_init(void)
}
static bool have_console_on_data_cb(void)
{
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
return uart_has_on_data_cb(CONFIG_ESP_CONSOLE_UART_NUM);
#else
return false;
#endif
}
static void console_nodemcu_task(task_param_t param, task_prio_t prio)
{
(void)prio;
char c = (char)param;
if (run_input)
feed_lua_input(&c, 1);
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
if (have_console_on_data_cb())
uart_feed_data(CONFIG_ESP_CONSOLE_UART_NUM, &c, 1);
#endif
// The IDF doesn't seem to honor setvbuf(stdout, NULL, _IONBF, 0) :(
fsync(fileno(stdout));
}
static void console_task(void *)
{
for (;;)
{
/* 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 || have_console_on_data_cb()))
{
if (!task_post_block_high(lua_feed_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", 1024, NULL, ESP_TASK_MAIN_PRIO+1, NULL);
}
void __attribute__((noreturn)) app_main(void)
{
task_init();
lua_feed_task = task_get_id(console_nodemcu_task);
relayed_event_handled = xSemaphoreCreateBinary();
relayed_event_task = task_get_id(handle_default_loop_event);
@ -304,8 +154,6 @@ void __attribute__((noreturn)) app_main(void)
nvs_flash_init ();
esp_netif_init ();
console_init();
start_lua ();
task_pump_messages ();
__builtin_unreachable ();

View File

@ -129,7 +129,7 @@ menu "Lua configuration"
bool
default y
select NODEMCU_CMODULE_PIPE
select NODEMCU_CMODULE_UART
select NODEMCU_CMODULE_CONSOLE
select LUA_BUILTIN_DEBUG
choice LUA_INIT_STRING

View File

@ -11,6 +11,7 @@ set(module_srcs
"bit.c"
"bthci.c"
"common.c"
"console.c"
"crypto.c"
"dht.c"
"encoder.c"
@ -35,6 +36,7 @@ set(module_srcs
"rmt.c"
"rtcmem.c"
"qrcodegen.c"
"serial_common.c"
"sigma_delta.c"
"sjson.c"
"sodium.c"

View File

@ -28,6 +28,12 @@ menu "NodeMCU modules"
help
Includes the can module.
config NODEMCU_CMODULE_CONSOLE
bool "Console module"
default y
help
Includes the console module (required by our Lua VM).
config NODEMCU_CMODULE_CRYPTO
bool "Crypto module"
default "n"
@ -339,6 +345,6 @@ menu "NodeMCU modules"
bool "UART module"
default y
help
Includes the UART module (required by our Lua VM).
Includes the UART module.
endmenu

View File

@ -0,0 +1,256 @@
#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)
{
size_t n = fwrite(buf + written, 1, len - written, 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(string1, [string2], ..., [stringn])
static int console_write(lua_State *L)
{
int total = lua_gettop(L);
for (int s = 1; s <= total; ++s)
{
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);
}
else
{
luaL_checktype(L, s, LUA_TSTRING);
size_t len = 0;
const char *buf = lua_tolstring(L, s, &len);
retrying_write(buf, len);
}
}
fflush(stdout);
fsync(fileno(stdout));
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(UART, "console", console, luaopen_console);

View File

@ -0,0 +1,197 @@
// Common routines for handling serial input data
#include "serial_common.h"
#include "lauxlib.h"
#include <string.h>
// This is the historical max value
#define MAX_SERIAL_INPUT 255
struct serial_input_cfg {
int receive_ref;
int error_ref;
char *line_buffer;
size_t line_buffer_size;
size_t line_position;
uint16_t need_len;
int16_t end_char;
};
static const char nostack[] = "out of stack";
static bool serial_input_invoke(int ref, const char *buf, size_t len)
{
if(ref == LUA_NOREF || !buf || len == 0)
return false;
lua_State *L = lua_getstate();
int top = lua_gettop(L);
luaL_checkstack(L, 2, nostack);
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
lua_pushlstring(L, buf, len);
luaL_pcallx(L, 1, 0);
lua_settop(L, top);
return true;
}
serial_input_cfg_t *serial_input_new(void)
{
serial_input_cfg_t *cfg = calloc(1, sizeof(serial_input_cfg_t));
if (!cfg)
return NULL;
cfg->receive_ref = cfg->error_ref = LUA_NOREF;
cfg->end_char = -1;
return cfg;
}
void serial_input_free(lua_State *L, serial_input_cfg_t *cfg)
{
if (cfg->receive_ref != LUA_NOREF)
luaL_unref(L, LUA_REGISTRYINDEX, cfg->receive_ref);
if (cfg->error_ref != LUA_NOREF)
luaL_unref(L, LUA_REGISTRYINDEX, cfg->error_ref);
free(cfg->line_buffer);
free(cfg);
}
bool serial_input_dispatch_data(serial_input_cfg_t *cfg, const char *buf, size_t len)
{
if (!cfg)
return false;
else
return serial_input_invoke(cfg->receive_ref, buf, len);
}
bool serial_input_report_error(serial_input_cfg_t *cfg, const char *buf, size_t len)
{
if (!cfg)
return false;
else
return serial_input_invoke(cfg->error_ref, buf, len);
}
void serial_input_feed_data(serial_input_cfg_t *cfg, const char *buf, size_t len)
{
if (!cfg || !cfg->line_buffer || !buf || !len)
return;
const uint16_t need_len = cfg->need_len;
const int16_t end_char = cfg->end_char;
const size_t max_wanted =
(end_char >= 0 && need_len == 0) ? cfg->line_buffer_size : need_len;
for (unsigned i = 0; i < len; ++i)
{
char ch = buf[i];
cfg->line_buffer[cfg->line_position] = ch;
cfg->line_position++;
bool at_end = (cfg->line_position >= max_wanted);
bool end_char_found =
(end_char >= 0 && (uint8_t)ch == (uint8_t)end_char);
if (at_end || end_char_found) {
// Reset line position early so callback can resize line_buffer if desired
int n = cfg->line_position;
cfg->line_position = 0;
serial_input_dispatch_data(cfg, cfg->line_buffer, n);
}
}
}
bool serial_input_has_data_cb(serial_input_cfg_t *cfg)
{
return cfg && cfg->receive_ref != LUA_NOREF;
}
// on("method", [number/char], function)
int serial_input_register(lua_State *L, serial_input_cfg_t *cfg)
{
const char *method = luaL_checkstring(L, 1);
const bool is_data = (strcmp(method, "data") == 0);
const bool is_error = (strcmp(method, "error") == 0);
if (!is_data && !is_error)
return luaL_error(L, "method not supported");
int fn_idx = -1;
if (lua_isnumber(L, 2))
{
cfg->need_len = luaL_checkinteger(L, 2);
cfg->end_char = -1;
}
else if (lua_isstring(L, 2))
{
size_t len;
const char *end = luaL_checklstring(L, 2, &len);
if (len != 1)
return luaL_error(L, "only single byte end marker supported");
cfg->need_len = 0;
cfg->end_char = end[0];
}
else if (lua_isfunction(L, 2))
{
fn_idx = 2;
}
if (fn_idx == -1 && lua_isfunction(L, 3))
{
fn_idx = 3;
}
if (is_data)
{
if (cfg->receive_ref != LUA_NOREF)
luaL_unref2(L, LUA_REGISTRYINDEX, cfg->receive_ref); // unref & clear
if (fn_idx != -1) // Register and (re)alloc resources
{
luaL_checkstack(L, 1, nostack);
lua_pushvalue(L, fn_idx);
cfg->receive_ref = luaL_ref(L, LUA_REGISTRYINDEX);
size_t min_size = (cfg->need_len > 0) ? cfg->need_len : MAX_SERIAL_INPUT;
// Prevent dropping input; this should be an exceedingly rare condition
if (cfg->line_position >= min_size)
min_size = cfg->line_position + 1;
if (cfg->line_buffer_size < min_size)
{
cfg->line_buffer = realloc(cfg->line_buffer, min_size);
cfg->line_buffer_size = (cfg->line_buffer) ? min_size : 0;
if (!cfg->line_buffer)
return luaL_error(L, "out of mem");
}
}
else // Free resources
{
free(cfg->line_buffer);
cfg->line_buffer = NULL;
cfg->line_buffer_size = 0;
cfg->line_position = 0;
}
}
else if (is_error)
{
if (cfg->error_ref != LUA_NOREF)
luaL_unref2(L, LUA_REGISTRYINDEX, cfg->error_ref); // unref & clear
if (fn_idx != -1)
{
luaL_checkstack(L, 1, nostack);
lua_pushvalue(L, fn_idx);
cfg->error_ref = luaL_ref(L, LUA_REGISTRYINDEX);
}
}
return 0;
}

View File

@ -0,0 +1,91 @@
#ifndef SERIAL_COMMON_H
#define SERIAL_COMMON_H
#include "lua.h"
#include <stdint.h>
#include <stdbool.h>
struct serial_input_cfg;
typedef struct serial_input_cfg serial_input_cfg_t;
/**
* Instantiate a new serial_input object.
* @returns a freshly allocated serial input object, with no further resources
* associated with it.
*/
serial_input_cfg_t *serial_input_new(void);
/**
* Free a serial_input_cfg_t object.
* Releases all associated resources. The object may not be passed to any
* serial_input_xxx functions after this.
*
* Must only be called from the Lua VM task context.
*/
void serial_input_free(lua_State *L, serial_input_cfg_t *cfg);
/**
* Helper function to hand registration of "data" and "error" callbacks.
* Expects the following calling signature:
* on("method", [number/char], function)
*
* Must only be called from the Lua VM task context.
*
* @param L The current Lua VM.
* @param cfg Instance to un/register with. Must've been initialised with
* @c serial_input_init() originally.
* @return Zero. Will luaL_error() on invalid args.
*/
int serial_input_register(lua_State *L, serial_input_cfg_t *cfg);
/**
* Feed data into a serial_input stream for processing.
*
* Must only be called from the Lua VM task context, as it will invoke
* Lua callbacks as necessary.
* Uses lua_getstate() to obtain the LVM instance.
*
* @param cfg The serial_input instance.
* @param buf The data buffer from which to feed bytes.
* @param len The number of bytes available in the buffer.
*/
void serial_input_feed_data(serial_input_cfg_t *cfg, const char *buf, size_t len);
/**
* Checks whether a "data" callback is registered.
*
* @param cfg The serial_input instance.
* @return Whether a "data" callback is currently registered.
*/
bool serial_input_has_data_cb(serial_input_cfg_t *cfg);
/**
* Direct access to invoking a configured "data" callback.
*
* Must only be called from the Lua VM task context.
* Uses lua_getstate() to obtain the LVM instance.
*
* @param cfg The serial_input instance.
* @param buf The data which to pass to the callback.
* @param len The number of bytes available in the buffer.
* @return True if the callback was successfully invoked (registered, and valid
* non-empty data passed).
*/
bool serial_input_dispatch_data(serial_input_cfg_t *cfg, const char *buf, size_t len);
/**
* Direct access to invoking a configured "error" callback.
* Must only be called from the Lua VM task context.
* Uses lua_getstate() to obtain the LVM instance.
*
* @param cfg The serial_input instance.
* @param msg The message to pass to the error callback.
* @param len The length of the message, in bytes.
* @return True if the callback was successfully invoked (registered, and valid
* non-empty data passed).
*/
bool serial_input_report_error(serial_input_cfg_t *cfg, const char *msg, size_t len);
#endif

View File

@ -3,61 +3,23 @@
#include "module.h"
#include "lauxlib.h"
#include "platform.h"
#include "serial_common.h"
#include "linput.h"
#include "lmem.h"
#include <stdint.h>
#include <string.h>
typedef struct {
int receive_rf;
int error_rf;
char *line_buffer;
size_t line_position;
uint16_t need_len;
int16_t end_char;
} uart_cb_cfg_t;
static lua_State *gL = NULL;
static uart_cb_cfg_t uart_cb_cfg[NUM_UART];
static bool uart_on_data_cb(unsigned id, const char *buf, size_t len){
if(!buf || len==0)
return false;
if(uart_cb_cfg[id].receive_rf == LUA_NOREF)
return false;
if(!gL)
return false;
int top = lua_gettop(gL);
lua_rawgeti(gL, LUA_REGISTRYINDEX, uart_cb_cfg[id].receive_rf);
lua_pushlstring(gL, buf, len);
luaL_pcallx(gL, 1, 0);
lua_settop(gL, top);
return !run_input;
}
static serial_input_cfg_t *uart_cb_cfg[NUM_UART];
bool uart_on_error_cb(unsigned id, const char *buf, size_t len){
if(!buf || len==0)
return false;
if(uart_cb_cfg[id].error_rf == LUA_NOREF)
return false;
if(!gL)
return false;
int top = lua_gettop(gL);
lua_rawgeti(gL, LUA_REGISTRYINDEX, uart_cb_cfg[id].error_rf);
lua_pushlstring(gL, buf, len);
luaL_pcallx(gL, 1, 0);
lua_settop(gL, top);
return true;
return serial_input_report_error(uart_cb_cfg[id], buf, len);
}
bool uart_has_on_data_cb(unsigned id){
return uart_cb_cfg[id].receive_rf != LUA_NOREF;
return serial_input_has_data_cb(uart_cb_cfg[id]);
}
@ -66,116 +28,43 @@ void uart_feed_data(unsigned id, const char *buf, size_t len)
if (id >= NUM_UART)
return;
uart_cb_cfg_t *cfg = &uart_cb_cfg[id];
if (!cfg->line_buffer)
return;
serial_input_feed_data(uart_cb_cfg[id], buf, len);
}
for (unsigned i = 0; i < len; ++i)
{
char ch = buf[i];
cfg->line_buffer[cfg->line_position] = ch;
cfg->line_position++;
uint16_t need_len = cfg->need_len;
int16_t end_char = cfg->end_char;
size_t max_wanted =
(end_char >= 0 && need_len == 0) ? LUA_MAXINPUT : need_len;
bool at_end = (cfg->line_position >= max_wanted);
bool end_char_found =
(end_char >= 0 && (uint8_t)ch == (uint8_t)end_char);
if (at_end || end_char_found) {
uart_on_data_cb(id, cfg->line_buffer, cfg->line_position);
cfg->line_position = 0;
}
}
static int ensure_valid_id(lua_State *L, int id)
{
MOD_CHECK_ID(uart, id);
int console = -1;
#if defined(CONFIG_ESP_CONSOLE_UART_DEFAULT) || defined(CONFIG_ESP_CONSOLE_UART_CUSTOM)
console = CONFIG_ESP_CONSOLE_UART_NUM;
#endif
if (id == console)
return luaL_error(L,
"uart in use by system console; use the 'console' module instead");
return 0;
}
// Lua: uart.on([id], "method", [number/char], function, [run_input])
static int uart_on( lua_State* L )
{
unsigned id = CONFIG_ESP_CONSOLE_UART_NUM;
size_t sl, el;
int32_t run = 1;
uint8_t stack = 1;
const char *method;
if( lua_isnumber( L, stack ) ) {
id = ( unsigned )luaL_checkinteger( L, stack );
MOD_CHECK_ID( uart, id );
stack++;
}
uart_cb_cfg_t *cfg = &uart_cb_cfg[id];
method = luaL_checklstring( L, stack, &sl );
stack++;
if (method == NULL)
return luaL_error( L, "wrong arg type" );
if( lua_type( L, stack ) == LUA_TNUMBER )
int id = 0;
if (lua_isnumber(L, 1))
{
cfg->need_len = (uint16_t)luaL_checkinteger(L, stack);
stack++;
cfg->end_char = -1;
if(cfg->need_len > 255)
{
cfg->need_len = 255;
return luaL_error( L, "wrong arg range" );
}
}
else if(lua_isstring(L, stack))
{
const char *end = luaL_checklstring( L, stack, &el );
stack++;
if(el!=1){
return luaL_error( L, "wrong arg range" );
}
cfg->end_char = (int16_t)end[0];
cfg->need_len = 0;
id = luaL_checkinteger(L, 1);
lua_remove(L, 1);
}
if (lua_isfunction(L, stack)) {
if ( lua_isnumber(L, stack+1) ){
run = lua_tointeger(L, stack+1);
}
lua_pushvalue(L, stack); // copy argument (func) to the top of stack
} else {
lua_pushnil(L);
}
if(sl == 4 && strcmp(method, "data") == 0){
if(id == CONFIG_ESP_CONSOLE_UART_NUM)
run_input = true;
if(cfg->receive_rf != LUA_NOREF){
luaL_unref(L, LUA_REGISTRYINDEX, cfg->receive_rf);
cfg->receive_rf = LUA_NOREF;
}
if(!lua_isnil(L, -1)){
cfg->receive_rf = luaL_ref(L, LUA_REGISTRYINDEX);
gL = L;
if(id == CONFIG_ESP_CONSOLE_UART_NUM && run==0)
run_input = false;
} else {
lua_pop(L, 1);
}
} else if(sl == 5 && strcmp(method, "error") == 0){
if(cfg->error_rf != LUA_NOREF){
luaL_unref(L, LUA_REGISTRYINDEX, cfg->error_rf);
cfg->error_rf = LUA_NOREF;
}
if(!lua_isnil(L, -1)){
cfg->error_rf = luaL_ref(L, LUA_REGISTRYINDEX);
gL = L;
} else {
lua_pop(L, 1);
}
} else {
lua_pop(L, 1);
return luaL_error( L, "method not supported" );
}
return 0;
ensure_valid_id(L, id);
return serial_input_register(L, uart_cb_cfg[id]);
}
// Lua: actualbaud = setup( id, baud, databits, parity, stopbits, echo )
static int uart_setup( lua_State* L )
{
@ -186,40 +75,36 @@ static int uart_setup( lua_State* L )
memset(&pins, 0, sizeof(pins));
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( uart, id );
ensure_valid_id(L, id);
baud = luaL_checkinteger( L, 2 );
databits = luaL_checkinteger( L, 3 );
parity = luaL_checkinteger( L, 4 );
stopbits = luaL_checkinteger( L, 5 );
if (!lua_isnoneornil(L, 6)) {
if(id == CONFIG_ESP_CONSOLE_UART_NUM){
input_echo = luaL_checkinteger(L, 6) > 0;
} else {
luaL_checktable(L, 6);
luaL_checktable(L, 6);
lua_getfield (L, 6, "tx");
pins.tx_pin = luaL_checkint(L, -1);
lua_getfield (L, 6, "rx");
pins.rx_pin = luaL_checkint(L, -1);
lua_getfield (L, 6, "cts");
pins.cts_pin = luaL_optint(L, -1, -1);
lua_getfield (L, 6, "rts");
pins.rts_pin = luaL_optint(L, -1, -1);
lua_getfield (L, 6, "tx_inverse");
pins.tx_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "rx_inverse");
pins.rx_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "cts_inverse");
pins.cts_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "rts_inverse");
pins.rts_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "flow_control");
pins.flow_control = luaL_optint(L, -1, PLATFORM_UART_FLOW_NONE);
lua_getfield (L, 6, "tx");
pins.tx_pin = luaL_checkint(L, -1);
lua_getfield (L, 6, "rx");
pins.rx_pin = luaL_checkint(L, -1);
lua_getfield (L, 6, "cts");
pins.cts_pin = luaL_optint(L, -1, -1);
lua_getfield (L, 6, "rts");
pins.rts_pin = luaL_optint(L, -1, -1);
pins_to_use = &pins;
}
lua_getfield (L, 6, "tx_inverse");
pins.tx_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "rx_inverse");
pins.rx_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "cts_inverse");
pins.cts_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "rts_inverse");
pins.rts_inverse = lua_toboolean(L, -1);
lua_getfield (L, 6, "flow_control");
pins.flow_control = luaL_optint(L, -1, PLATFORM_UART_FLOW_NONE);
pins_to_use = &pins;
}
res = platform_uart_setup( id, baud, databits, parity, stopbits, pins_to_use );
@ -232,7 +117,7 @@ static int uart_setmode(lua_State* L)
unsigned id, mode;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( uart, id );
ensure_valid_id(L, id);
mode = luaL_checkinteger( L, 2 );
platform_uart_setmode(id, mode);
@ -249,7 +134,7 @@ static int uart_write( lua_State* L )
int total = lua_gettop( L ), s;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( uart, id );
ensure_valid_id(L, id);
for( s = 2; s <= total; s ++ )
{
if( lua_type( L, s ) == LUA_TNUMBER )
@ -275,13 +160,8 @@ static int uart_stop( lua_State* L )
{
unsigned id;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( uart, id );
ensure_valid_id(L, id);
platform_uart_stop( id );
if (uart_cb_cfg[id].line_buffer)
{
luaM_freemem(L, uart_cb_cfg[id].line_buffer, LUA_MAXINPUT);
uart_cb_cfg[id].line_buffer = NULL;
}
return 0;
}
@ -291,9 +171,7 @@ static int uart_start( lua_State* L )
unsigned id;
int err;
id = luaL_checkinteger( L, 1 );
MOD_CHECK_ID( uart, id );
if (!uart_cb_cfg[id].line_buffer)
uart_cb_cfg[id].line_buffer = luaM_malloc(L, LUA_MAXINPUT);
ensure_valid_id(L, id);
err = platform_uart_start( id );
lua_pushboolean( L, err == 0 );
return 1;
@ -303,7 +181,7 @@ static int uart_getconfig(lua_State* L) {
uint32_t id, baud, databits, parity, stopbits;
id = luaL_checkinteger(L, 1);
MOD_CHECK_ID(uart, id);
ensure_valid_id(L, id);
int err = platform_uart_get_config(id, &baud, &databits, &parity, &stopbits);
if (err) {
@ -320,7 +198,7 @@ static int uart_getconfig(lua_State* L) {
static int uart_wakeup (lua_State *L)
{
uint32_t id = luaL_checkinteger(L, 1);
MOD_CHECK_ID(uart, id);
ensure_valid_id(L, id);
int threshold = luaL_checkinteger(L, 2);
int err = platform_uart_set_wakeup_threshold(id, threshold);
if (err) {
@ -332,7 +210,7 @@ static int uart_wakeup (lua_State *L)
static int luart_tx_flush (lua_State *L)
{
uint32_t id = luaL_checkinteger(L, 1);
MOD_CHECK_ID(uart, id);
ensure_valid_id(L, id);
platform_uart_flush(id);
return 0;
}
@ -367,13 +245,9 @@ LROT_END(uart, NULL, 0)
int luaopen_uart( lua_State *L ) {
for(int id = 0; id < sizeof(uart_cb_cfg)/sizeof(uart_cb_cfg[0]); id++)
{
uart_cb_cfg_t *cfg = &uart_cb_cfg[id];
cfg->receive_rf = LUA_NOREF;
cfg->error_rf = LUA_NOREF;
cfg->line_buffer = NULL;
cfg->line_position = 0;
cfg->need_len = 0;
cfg->end_char = -1;
uart_cb_cfg[id] = serial_input_new();
if (!uart_cb_cfg[id])
return luaL_error(L, "out of mem");
}
return 0;
}

View File

@ -88,14 +88,6 @@ void uart_event_task( task_param_t param, task_prio_t prio ) {
unsigned id = post->id;
xSemaphoreGive(sem);
if(post->type == PLATFORM_UART_EVENT_DATA) {
if (id == CONFIG_ESP_CONSOLE_UART_NUM && run_input) {
size_t i = 0;
while (i < post->size)
{
unsigned used = feed_lua_input(post->data + i, post->size - i);
i += used;
}
}
if (uart_has_on_data_cb(id))
uart_feed_data(id, post->data, post->size);
@ -206,6 +198,10 @@ static void task_uart( void *pvParameters ){
// pins must not be null for non-console uart
uint32_t platform_uart_setup( unsigned id, uint32_t baud, int databits, int parity, int stopbits, uart_pins_t* pins )
{
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
if (id == CONFIG_ESP_CONSOLE_UART_NUM)
return 0;
#endif
int flow_control = UART_HW_FLOWCTRL_DISABLE;
if (pins != NULL) {
if(pins->flow_control & PLATFORM_UART_FLOW_CTS) flow_control |= UART_HW_FLOWCTRL_CTS;
@ -286,30 +282,29 @@ void platform_uart_setmode(unsigned id, unsigned mode)
void platform_uart_send_multi( unsigned id, const char *data, size_t len )
{
size_t i;
if (id == CONFIG_ESP_CONSOLE_UART_NUM) {
for( i = 0; i < len; i ++ ) {
putchar (data[ i ]);
}
} else {
uart_write_bytes(id, data, len);
}
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
if (id == CONFIG_ESP_CONSOLE_UART_NUM)
return;
#endif
uart_write_bytes(id, data, len);
}
void platform_uart_send( unsigned id, uint8_t data )
{
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
if (id == CONFIG_ESP_CONSOLE_UART_NUM)
putchar (data);
else
uart_write_bytes(id, (const char *)&data, 1);
return;
#endif
uart_write_bytes(id, (const char *)&data, 1);
}
void platform_uart_flush( unsigned id )
{
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
if (id == CONFIG_ESP_CONSOLE_UART_NUM)
fflush (stdout);
else
uart_tx_flush(id);
return;
#endif
uart_tx_flush(id);
}
@ -354,9 +349,12 @@ void platform_uart_stop( unsigned id )
}
int platform_uart_get_config(unsigned id, uint32_t *baudp, uint32_t *databitsp, uint32_t *parityp, uint32_t *stopbitsp) {
int err;
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
if (id == CONFIG_ESP_CONSOLE_UART_NUM)
return -1;
#endif
err = uart_get_baudrate(id, baudp);
int err = uart_get_baudrate(id, baudp);
if (err != ESP_OK) return -1;
*baudp &= 0xFFFFFFFE; // round down
@ -405,6 +403,10 @@ int platform_uart_get_config(unsigned id, uint32_t *baudp, uint32_t *databitsp,
int platform_uart_set_wakeup_threshold(unsigned id, unsigned threshold)
{
#if CONFIG_ESP_CONSOLE_UART_DEFAULT || CONFIG_ESP_CONSOLE_UART_CUSTOM
if (id == CONFIG_ESP_CONSOLE_UART_NUM)
return -1;
#endif
esp_err_t err = uart_set_wakeup_threshold(id, threshold);
return (err == ESP_OK) ? 0 : -1;
}

88
docs/modules/console.md Normal file
View File

@ -0,0 +1,88 @@
# Console Module
| Since | Origin / Contributor | Maintainer | Source |
| :----- | :-------------------- | :---------- | :------ |
| 2024-10-22 | [jmattsson](https://github.com/jmattsson) | [jmattsson](https://github.com/jmattsson) | [console.c](../../components/modules/console.c)|
The `console` modules allows direct access to the system console. The system
console has typically been a UART, but by now several options are available
across the ESP32 range of SoCs, including UART, USB-Serial-JTAG and USB CDC-ACM.
By default the system console is linked up to provide an interactive Lua
shell (REPL — Read-Execute-Print Loop). It also provides a hook for listening
in on the data received on the console programatically, and the interactivity
may also be disabled (and re-enabled) programatically if so desired.
## console.on()
Used to register or deregister a callback function to handle console events.
#### Syntax
`console.on(method, [number/end_char], [function])`
#### Parameters
- `method`. One of
- "data" for bytes received on the console
- "error" if an error condition is encountered on the console
- `number/end_char`. Only for event `data`.
- if pass in a number n, the callback will called when n chars are received.
- if n=0, will receive every char in buffer.
- if pass in a one char string "c", the callback will called when "c" is encounterd, or max n=255 received.
- `function` callback function.
- event "data" has a callback like this: `function(data) end`
- event "error" has a callback like this: `function(err) end`
To unregister the callback, specify `nil` as the function.
#### Returns
`nil`
#### Example
```lua
-- when 4 chars is received.
console.on("data", 4,
function(data)
print("received from console:", data)
if data=="quit" then
console.on("data", 0, nil)
end
end)
-- when '\r' is received.
console.on("data", "\r",
function(data)
print("received from console:", data)
if data=="quit\r" then
console.on("data", 0, nil)
end
end)
-- error handler
console.on("error",
function(err)
print("error on console:", err)
end)
```
## console.mode()
Controls the interactivity of the console.
#### Syntax
`console.mode(mode)`
#### Parameters
- `mode` One of
- `console.INTERACTIVE` automatically pass console data to the Lua VM
for execution. This is the default mode.
- `console.NONINTERACTIVE` disables the automatic passing of console data
to the Lua VM. The data only goes to the registered "data" callback,
if any.
#### Returns
`nil`
#### Example
```lua
-- Implement a REL instead of the usual REPL
console.on("data", "\r", function(line) node.input(line.."\r\n") end)
console.mode(console.NONINTERACTIVE)
```

View File

@ -5,31 +5,31 @@
The [UART](https://en.wikipedia.org/wiki/Universal_asynchronous_receiver/transmitter) (Universal asynchronous receiver/transmitter) module allows configuration of and communication over the UART serial port.
The default setup for the console uart is controlled by build-time settings. The default uart for console is `UART0`. The default rate is 115,200 bps. In addition, auto-baudrate detection is enabled for the first two minutes
after platform boot. This will cause a switch to the correct baud rate once a few characters are received. Auto-baudrate detection is disabled when `uart.setup` is called.
If the UART is in use as the system console, it is unavailable for use by this
module. Instead, refer to the `console` module.
For other uarts, you should call `uart.setup` and `uart.start` to get them working.
Before using a UART, you must call `uart.setup` and `uart.start` to set them up.
## uart.on()
Sets the callback function to handle UART events.
Sets the callback function to handle UART events. For a UART used by the
console, refer to the `console` module instead.
#### Syntax
`uart.on([id], method, [number/end_char], [function], [run_input])`
`uart.on([id], method, [number/end_char], [function])`
#### Parameters
- `id` uart id, default value is uart num of the console.
- `id` uart id, except console uart. Default value is uart 0.
- `method` "data", data has been received on the UART. "error", error occurred on the UART.
- `number/end_char`. Only for event `data`.
- if pass in a number n<255, the callback will called when n chars are received.
- if pass in a number n, the callback will called when n chars are received.
- if n=0, will receive every char in buffer.
- if pass in a one char string "c", the callback will called when "c" is encounterd, or max n=255 received.
- `function` callback function.
- event "data" has a callback like this: `function(data) end`
- event "error" has a callback like this: `function(err) end`. `err` could be one of "out_of_memory", "break", "rx_error".
- `run_input` 0 or 1. Only for "data" event on console uart. If 0, input from UART will not go into Lua interpreter, can accept binary data. If 1, input from UART will go into Lua interpreter, and run.
To unregister the callback, provide only the "data" parameter.
To unregister the callback, provide only the "method" parameter.
#### Returns
`nil`
@ -71,17 +71,16 @@ uart.on(2, "error",
(Re-)configures the communication parameters of the UART.
#### Syntax
`uart.setup(id, baud, databits, parity, stopbits, echo_or_pins)`
`uart.setup(id, baud, databits, parity, stopbits, pins)`
#### Parameters
- `id` uart id
- `id` uart id, except console uart
- `baud` one of 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 74880, 115200, 230400, 256000, 460800, 921600, 1843200, 3686400
- `databits` one of 5, 6, 7, 8
- `parity` `uart.PARITY_NONE`, `uart.PARITY_ODD`, or `uart.PARITY_EVEN`
- `stopbits` `uart.STOPBITS_1`, `uart.STOPBITS_1_5`, or `uart.STOPBITS_2`
- `echo_or_pins`
- for console uart, this should be a int. if 0, disable echo, otherwise enable echo
- for others, this is a table:
- `pins`
- table with the following entries:
- `tx` int. TX pin. Required
- `rx` int. RX pin. Required
- `cts` in. CTS pin. Optional
@ -113,7 +112,7 @@ Returns the current configuration parameters of the UART.
`uart.getconfig(id)`
#### Parameters
- `id` UART id (0 or 1).
- `id` uart id, except console uart
#### Returns
Four values as follows:
@ -130,7 +129,7 @@ print (uart.getconfig(0))
```
## uart.start()
Start the UART. You do not need to call `start()` on the console uart.
Start the UART.
#### Syntax
`uart.start(id)`
@ -143,7 +142,7 @@ Boolean. `true` if uart is started.
## uart.stop()
Stop the UART. You should not call `stop()` on the console uart.
Stop the UART.
#### Syntax
`uart.stop(id)`
@ -164,7 +163,7 @@ Set UART controllers communication mode
`uart.setmode(id, mode)`
#### Parameters
- `id` uart id
- `id` uart id, except console uart
- `mode` value should be one of
- `uart.MODE_UART` default UART mode, is set after uart.setup() call
- `uart.MODE_RS485_COLLISION_DETECT` receiver must be always enabled, transmitter is automatically switched using RTS pin, collision is detected by UART hardware (note: no event is generated on collision, limitation of esp-idf)
@ -184,15 +183,15 @@ Wait for any data currently in the UART transmit buffers to be written out. It c
`uart.txflush(id)`
#### Parameters
- `id` uart id
- `id` uart id, except console uart
#### Returns
`nil`
#### Example
```lua
print("I want this to show up now not in 5 seconds")
uart.txflush(0) -- assuming 0 is the console uart
uart.write(0, "I want this to show up now not in 5 seconds")
uart.txflush(0)
node.sleep({secs=5})
```
@ -208,7 +207,7 @@ Configure the light sleep wakeup threshold. This is the number of positive edges
`uart.wakeup(id, val)`
#### Parameters
- `id` uart id
- `id` uart id, except console uart
- `val` the new value
#### Returns
@ -231,7 +230,7 @@ Write string or byte to the UART.
`uart.write(id, data1 [, data2, ...])`
#### Parameters
- `id` uart id
- `id` uart id, except console uart
- `data1`... string or byte to send via UART
#### Returns

View File

@ -43,6 +43,7 @@ nav:
- 'bit': 'modules/bit.md'
- 'bthci': 'modules/bthci.md'
- 'can': 'modules/can.md'
- 'console': 'modules/console.md'
- 'crypto': 'modules/crypto.md'
- 'dac': 'modules/dac.md'
- 'dht': 'modules/dht.md'