From 8dbc56fb7ddcf91f3d3414144c0e7f79a4da0ed9 Mon Sep 17 00:00:00 2001 From: yanke Date: Wed, 22 Apr 2026 10:48:27 +0800 Subject: [PATCH] feat: add lua module uart --- .../basic_demo/main/basic_demo_lua_modules.c | 6 + application/basic_demo/main/idf_component.yml | 3 + .../lua_module_uart/CMakeLists.txt | 9 + .../lua_module_uart/include/lua_module_uart.h | 20 + .../lua_module_uart/skills/lua_module_uart.md | 87 +++++ .../lua_module_uart/skills/skills_list.json | 12 + .../lua_module_uart/src/lua_module_uart.c | 365 ++++++++++++++++++ .../docs/en/reference-cap/lua-modules.mdx | 1 + .../docs/en/reference-project/index.mdx | 1 + .../docs/zh-cn/reference-cap/lua-modules.mdx | 1 + .../docs/zh-cn/reference-project/index.mdx | 1 + 11 files changed, 506 insertions(+) create mode 100644 components/lua_modules/lua_module_uart/CMakeLists.txt create mode 100644 components/lua_modules/lua_module_uart/include/lua_module_uart.h create mode 100644 components/lua_modules/lua_module_uart/skills/lua_module_uart.md create mode 100644 components/lua_modules/lua_module_uart/skills/skills_list.json create mode 100644 components/lua_modules/lua_module_uart/src/lua_module_uart.c diff --git a/application/basic_demo/main/basic_demo_lua_modules.c b/application/basic_demo/main/basic_demo_lua_modules.c index 4cdc95f..0178d56 100644 --- a/application/basic_demo/main/basic_demo_lua_modules.c +++ b/application/basic_demo/main/basic_demo_lua_modules.c @@ -16,6 +16,7 @@ #include "lua_module_system.h" #include "lua_module_board_manager.h" #include "lua_module_mcpwm.h" +#include "lua_module_uart.h" #if defined(CONFIG_ESP_BOARD_DEV_AUDIO_CODEC_SUPPORT) #include "lua_module_audio.h" @@ -110,5 +111,10 @@ esp_err_t basic_demo_lua_modules_register(void) return err; } + err = lua_module_uart_register(); + if (err != ESP_OK) { + return err; + } + return lua_module_event_publisher_register(); } diff --git a/application/basic_demo/main/idf_component.yml b/application/basic_demo/main/idf_component.yml index 7634c82..22b9e0d 100644 --- a/application/basic_demo/main/idf_component.yml +++ b/application/basic_demo/main/idf_component.yml @@ -143,3 +143,6 @@ dependencies: lua_module_mcpwm: path: ../../../components/lua_modules/lua_module_mcpwm + + lua_module_uart: + path: ../../../components/lua_modules/lua_module_uart diff --git a/components/lua_modules/lua_module_uart/CMakeLists.txt b/components/lua_modules/lua_module_uart/CMakeLists.txt new file mode 100644 index 0000000..d23d9e5 --- /dev/null +++ b/components/lua_modules/lua_module_uart/CMakeLists.txt @@ -0,0 +1,9 @@ +idf_component_register( + SRCS + "src/lua_module_uart.c" + INCLUDE_DIRS + "include" + REQUIRES + cap_lua + esp_driver_uart +) diff --git a/components/lua_modules/lua_module_uart/include/lua_module_uart.h b/components/lua_modules/lua_module_uart/include/lua_module_uart.h new file mode 100644 index 0000000..a42d471 --- /dev/null +++ b/components/lua_modules/lua_module_uart/include/lua_module_uart.h @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include "esp_err.h" +#include "lua.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int luaopen_uart(lua_State *L); +esp_err_t lua_module_uart_register(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/lua_modules/lua_module_uart/skills/lua_module_uart.md b/components/lua_modules/lua_module_uart/skills/lua_module_uart.md new file mode 100644 index 0000000..a9477b1 --- /dev/null +++ b/components/lua_modules/lua_module_uart/skills/lua_module_uart.md @@ -0,0 +1,87 @@ +# Lua UART + +This skill describes how to open a UART port and read/write bytes or text +lines from Lua. The module wraps ESP-IDF's UART driver and follows a pure +polling model — scripts call `read` / `read_line` with a timeout and get +back whatever is available. + +## How to call +- Import it with `local uart = require("uart")` +- Open a port with `local u = uart.new(port, tx, rx, baud [, opts])` + - `port`: UART port number. **Start from `1`** (`UART_NUM_1`) by + default — port `0` is normally claimed by the system log console, so + opening it from a script will fight the bootloader / `printf` output. + Each port has a single owner; a second `uart.new()` for the same + port raises a Lua error. + - `tx`, `rx`: GPIO numbers for TX and RX pins + - `baud`: baud rate, e.g. `9600`, `115200` + - `opts` (optional table, omit entirely for the common **8N1** case): + - `data_bits`: `5`–`8`, default `8` + - `parity`: `"none"` / `"even"` / `"odd"`, default `"none"` + - `stop_bits`: `1` or `2`, default `1` + - The RX ring buffer is fixed at 1 KiB and writes are blocking (no TX + ring buffer); flow control is disabled. +- `u:read(len [, timeout_ms])` → string of up to `len` bytes. Timeout in + milliseconds, default `0` (non-blocking — returns immediately with + whatever is buffered, possibly empty). The returned string may be + shorter than `len` if the timeout fires first. +- `u:read_line([max_len, timeout_ms])` → string ending with `\n` (or + truncated at `max_len` / timeout). Default `max_len` is `1024`. The + trailing `\n` is kept; strip with `line:gsub("[\r\n]+$", "")` if needed. +- `u:write(data)` → number of bytes sent. `data` is a string or a table + of byte integers `0..255`. +- `u:available()` → number of bytes currently sitting in the RX buffer. +- `u:flush_input()` → discard all buffered RX data. +- `u:close()` when you're done. Handles are also cleaned up on garbage + collection; explicit close is preferred for determinism. + +## Example: AT-command style request/response +```lua +local uart = require("uart") +local delay = require("delay") + +local u = uart.new(1, 17, 18, 115200) +u:flush_input() +u:write("AT\r\n") + +delay.delay_ms(50) +local reply = u:read_line(128, 500) -- up to 500 ms for the response +print("reply:", reply) + +u:close() +``` + +## Example: binary polling loop +```lua +local uart = require("uart") +local delay = require("delay") + +local u = uart.new(1, 17, 18, 9600) +for _ = 1, 10 do + if u:available() > 0 then + local chunk = u:read(64) -- non-blocking + -- process `chunk` (string; may be 1..64 bytes) + end + delay.delay_ms(20) +end +u:close() +``` + +## Example: non-8N1 frame format +Most serial devices are 8N1, but some legacy / industrial protocols +(e.g. Modbus ASCII, some meters) use 7 data bits with parity. Pass an +`opts` table to override; any field left out keeps its 8N1 default. +```lua +local u = uart.new(1, 17, 18, 9600, { + data_bits = 7, + parity = "even", + stop_bits = 1, +}) +``` + +## Notes +- All reads are **polling with a timeout**. There is no callback / + interrupt interface in this module. For high-rate data, poll often + enough to drain the 1 KiB RX buffer before it overruns. +- Closing a port releases the hardware; the same `port` number can then + be reopened with different settings. diff --git a/components/lua_modules/lua_module_uart/skills/skills_list.json b/components/lua_modules/lua_module_uart/skills/skills_list.json new file mode 100644 index 0000000..49445e3 --- /dev/null +++ b/components/lua_modules/lua_module_uart/skills/skills_list.json @@ -0,0 +1,12 @@ +{ + "skills": [ + { + "id": "lua_module_uart", + "file": "lua_module_uart.md", + "summary": "How to open a UART port and read/write bytes or text lines from Lua using polling.", + "cap_groups": [ + "cap_lua" + ] + } + ] +} diff --git a/components/lua_modules/lua_module_uart/src/lua_module_uart.c b/components/lua_modules/lua_module_uart/src/lua_module_uart.c new file mode 100644 index 0000000..c479eeb --- /dev/null +++ b/components/lua_modules/lua_module_uart/src/lua_module_uart.c @@ -0,0 +1,365 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "lua_module_uart.h" + +#include +#include +#include + +#include "cap_lua.h" +#include "driver/uart.h" +#include "esp_err.h" +#include "freertos/FreeRTOS.h" +#include "hal/uart_types.h" +#include "lauxlib.h" + +#define LUA_MODULE_UART_METATABLE "uart.port" +#define LUA_MODULE_UART_RX_BUF_SIZE 1024 +#define LUA_MODULE_UART_TX_BUF_SIZE 0 +#define LUA_MODULE_UART_MAX_READ_LEN 4096 +#define LUA_MODULE_UART_MAX_LINE_LEN 1024 + +typedef struct { + uart_port_t port; + bool installed; +} lua_module_uart_ud_t; + +static lua_module_uart_ud_t *lua_module_uart_get_ud(lua_State *L, int idx) +{ + lua_module_uart_ud_t *ud = (lua_module_uart_ud_t *)luaL_checkudata( + L, idx, LUA_MODULE_UART_METATABLE); + if (!ud || !ud->installed) { + luaL_error(L, "uart: invalid or closed port"); + } + return ud; +} + +static TickType_t lua_module_uart_timeout_ticks(lua_State *L, int idx) +{ + if (lua_isnoneornil(L, idx)) { + return 0; + } + lua_Integer ms = luaL_checkinteger(L, idx); + if (ms < 0) { + luaL_error(L, "uart timeout must be >= 0"); + } + if (ms == 0) { + return 0; + } + return pdMS_TO_TICKS((TickType_t)ms); +} + +static uart_word_length_t lua_module_uart_parse_data_bits(lua_State *L, int bits) +{ + switch (bits) { + case 5: + return UART_DATA_5_BITS; + case 6: + return UART_DATA_6_BITS; + case 7: + return UART_DATA_7_BITS; + case 8: + return UART_DATA_8_BITS; + default: + luaL_error(L, "uart data_bits must be 5, 6, 7, or 8"); + return UART_DATA_8_BITS; + } +} + +static uart_parity_t lua_module_uart_parse_parity(lua_State *L, const char *s) +{ + if (strcmp(s, "none") == 0) { + return UART_PARITY_DISABLE; + } + if (strcmp(s, "even") == 0) { + return UART_PARITY_EVEN; + } + if (strcmp(s, "odd") == 0) { + return UART_PARITY_ODD; + } + luaL_error(L, "uart parity must be 'none', 'even', or 'odd'"); + return UART_PARITY_DISABLE; +} + +static uart_stop_bits_t lua_module_uart_parse_stop_bits(lua_State *L, int bits) +{ + switch (bits) { + case 1: + return UART_STOP_BITS_1; + case 2: + return UART_STOP_BITS_2; + default: + luaL_error(L, "uart stop_bits must be 1 or 2"); + return UART_STOP_BITS_1; + } +} + +static int lua_module_uart_new(lua_State *L) +{ + lua_Integer port_num = luaL_checkinteger(L, 1); + lua_Integer tx = luaL_checkinteger(L, 2); + lua_Integer rx = luaL_checkinteger(L, 3); + lua_Integer baud = luaL_checkinteger(L, 4); + + if (port_num < 0 || port_num >= UART_NUM_MAX) { + return luaL_error(L, "uart port must be in range 0-%d", UART_NUM_MAX - 1); + } + if (baud <= 0) { + return luaL_error(L, "uart baud must be positive"); + } + + uart_word_length_t data_bits = UART_DATA_8_BITS; + uart_parity_t parity = UART_PARITY_DISABLE; + uart_stop_bits_t stop_bits = UART_STOP_BITS_1; + + if (!lua_isnoneornil(L, 5)) { + luaL_checktype(L, 5, LUA_TTABLE); + + lua_getfield(L, 5, "data_bits"); + if (!lua_isnil(L, -1)) { + data_bits = lua_module_uart_parse_data_bits(L, (int)luaL_checkinteger(L, -1)); + } + lua_pop(L, 1); + + lua_getfield(L, 5, "parity"); + if (!lua_isnil(L, -1)) { + parity = lua_module_uart_parse_parity(L, luaL_checkstring(L, -1)); + } + lua_pop(L, 1); + + lua_getfield(L, 5, "stop_bits"); + if (!lua_isnil(L, -1)) { + stop_bits = lua_module_uart_parse_stop_bits(L, (int)luaL_checkinteger(L, -1)); + } + lua_pop(L, 1); + } + + uart_config_t cfg = { + .baud_rate = (int)baud, + .data_bits = data_bits, + .parity = parity, + .stop_bits = stop_bits, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + + uart_port_t port = (uart_port_t)port_num; + esp_err_t err = uart_driver_install(port, + LUA_MODULE_UART_RX_BUF_SIZE, + LUA_MODULE_UART_TX_BUF_SIZE, + 0, NULL, 0); + if (err != ESP_OK) { + return luaL_error(L, "uart_driver_install failed on port %d: %s", + (int)port, esp_err_to_name(err)); + } + err = uart_param_config(port, &cfg); + if (err != ESP_OK) { + uart_driver_delete(port); + return luaL_error(L, "uart_param_config failed: %s", esp_err_to_name(err)); + } + err = uart_set_pin(port, (int)tx, (int)rx, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + if (err != ESP_OK) { + uart_driver_delete(port); + return luaL_error(L, "uart_set_pin failed: %s", esp_err_to_name(err)); + } + + lua_module_uart_ud_t *ud = (lua_module_uart_ud_t *)lua_newuserdata( + L, sizeof(*ud)); + ud->port = port; + ud->installed = true; + luaL_getmetatable(L, LUA_MODULE_UART_METATABLE); + lua_setmetatable(L, -2); + return 1; +} + +static int lua_module_uart_read(lua_State *L) +{ + lua_module_uart_ud_t *ud = lua_module_uart_get_ud(L, 1); + lua_Integer len = luaL_checkinteger(L, 2); + TickType_t ticks = lua_module_uart_timeout_ticks(L, 3); + + if (len <= 0 || len > LUA_MODULE_UART_MAX_READ_LEN) { + return luaL_error(L, "uart read length must be 1-%d", + LUA_MODULE_UART_MAX_READ_LEN); + } + + luaL_Buffer b; + uint8_t *buf = (uint8_t *)luaL_buffinitsize(L, &b, (size_t)len); + int got = uart_read_bytes(ud->port, buf, (uint32_t)len, ticks); + if (got < 0) { + return luaL_error(L, "uart read failed"); + } + luaL_pushresultsize(&b, (size_t)got); + return 1; +} + +static int lua_module_uart_read_line(lua_State *L) +{ + lua_module_uart_ud_t *ud = lua_module_uart_get_ud(L, 1); + lua_Integer max_len = luaL_optinteger(L, 2, LUA_MODULE_UART_MAX_LINE_LEN); + TickType_t ticks = lua_module_uart_timeout_ticks(L, 3); + + if (max_len <= 0 || max_len > LUA_MODULE_UART_MAX_LINE_LEN) { + return luaL_error(L, "uart read_line max_len must be 1-%d", + LUA_MODULE_UART_MAX_LINE_LEN); + } + + TickType_t deadline = (ticks == 0) ? 0 : xTaskGetTickCount() + ticks; + luaL_Buffer b; + luaL_buffinit(L, &b); + + for (lua_Integer i = 0; i < max_len; i++) { + uint8_t byte = 0; + TickType_t remaining; + if (ticks == 0) { + remaining = 0; + } else { + TickType_t now = xTaskGetTickCount(); + remaining = (now >= deadline) ? 0 : (deadline - now); + } + int got = uart_read_bytes(ud->port, &byte, 1, remaining); + if (got < 0) { + return luaL_error(L, "uart read_line failed"); + } + if (got == 0) { + break; + } + luaL_addchar(&b, (char)byte); + if (byte == '\n') { + break; + } + } + + luaL_pushresult(&b); + return 1; +} + +static int lua_module_uart_write(lua_State *L) +{ + lua_module_uart_ud_t *ud = lua_module_uart_get_ud(L, 1); + + const uint8_t *data = NULL; + size_t data_len = 0; + + int type = lua_type(L, 2); + if (type == LUA_TSTRING) { + data = (const uint8_t *)lua_tolstring(L, 2, &data_len); + } else if (type == LUA_TTABLE) { + lua_Integer n = luaL_len(L, 2); + if (n < 0 || n > LUA_MODULE_UART_MAX_READ_LEN) { + return luaL_error(L, "uart write table length must be 0-%d", + LUA_MODULE_UART_MAX_READ_LEN); + } + uint8_t *tmp = (uint8_t *)lua_newuserdata(L, (size_t)(n > 0 ? n : 1)); + for (lua_Integer i = 0; i < n; i++) { + lua_rawgeti(L, 2, i + 1); + lua_Integer byte = luaL_checkinteger(L, -1); + if (byte < 0 || byte > 0xFF) { + return luaL_error(L, "uart write byte #%d out of range 0-255", + (int)(i + 1)); + } + tmp[i] = (uint8_t)byte; + lua_pop(L, 1); + } + data = tmp; + data_len = (size_t)n; + } else { + return luaL_error(L, "uart write expects a string or table"); + } + + if (data_len == 0) { + lua_pushinteger(L, 0); + return 1; + } + + int sent = uart_write_bytes(ud->port, (const char *)data, data_len); + if (sent < 0) { + return luaL_error(L, "uart write failed"); + } + lua_pushinteger(L, sent); + return 1; +} + +static int lua_module_uart_available(lua_State *L) +{ + lua_module_uart_ud_t *ud = lua_module_uart_get_ud(L, 1); + size_t size = 0; + esp_err_t err = uart_get_buffered_data_len(ud->port, &size); + if (err != ESP_OK) { + return luaL_error(L, "uart available failed: %s", esp_err_to_name(err)); + } + lua_pushinteger(L, (lua_Integer)size); + return 1; +} + +static int lua_module_uart_flush_input(lua_State *L) +{ + lua_module_uart_ud_t *ud = lua_module_uart_get_ud(L, 1); + esp_err_t err = uart_flush_input(ud->port); + if (err != ESP_OK) { + return luaL_error(L, "uart flush_input failed: %s", esp_err_to_name(err)); + } + return 0; +} + +static int lua_module_uart_gc(lua_State *L) +{ + lua_module_uart_ud_t *ud = (lua_module_uart_ud_t *)luaL_testudata( + L, 1, LUA_MODULE_UART_METATABLE); + if (ud && ud->installed) { + uart_driver_delete(ud->port); + ud->installed = false; + } + return 0; +} + +static int lua_module_uart_close(lua_State *L) +{ + lua_module_uart_ud_t *ud = (lua_module_uart_ud_t *)luaL_checkudata( + L, 1, LUA_MODULE_UART_METATABLE); + if (ud->installed) { + esp_err_t err = uart_driver_delete(ud->port); + ud->installed = false; + if (err != ESP_OK) { + return luaL_error(L, "uart close failed: %s", esp_err_to_name(err)); + } + } + return 0; +} + +int luaopen_uart(lua_State *L) +{ + if (luaL_newmetatable(L, LUA_MODULE_UART_METATABLE)) { + lua_pushcfunction(L, lua_module_uart_gc); + lua_setfield(L, -2, "__gc"); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); + lua_pushcfunction(L, lua_module_uart_read); + lua_setfield(L, -2, "read"); + lua_pushcfunction(L, lua_module_uart_read_line); + lua_setfield(L, -2, "read_line"); + lua_pushcfunction(L, lua_module_uart_write); + lua_setfield(L, -2, "write"); + lua_pushcfunction(L, lua_module_uart_available); + lua_setfield(L, -2, "available"); + lua_pushcfunction(L, lua_module_uart_flush_input); + lua_setfield(L, -2, "flush_input"); + lua_pushcfunction(L, lua_module_uart_close); + lua_setfield(L, -2, "close"); + } + lua_pop(L, 1); + + lua_newtable(L); + lua_pushcfunction(L, lua_module_uart_new); + lua_setfield(L, -2, "new"); + return 1; +} + +esp_err_t lua_module_uart_register(void) +{ + return cap_lua_register_module("uart", luaopen_uart); +} diff --git a/docs/src/content/docs/en/reference-cap/lua-modules.mdx b/docs/src/content/docs/en/reference-cap/lua-modules.mdx index 2cda394..8a11d1b 100644 --- a/docs/src/content/docs/en/reference-cap/lua-modules.mdx +++ b/docs/src/content/docs/en/reference-cap/lua-modules.mdx @@ -29,6 +29,7 @@ import { Aside, CardGrid, LinkCard } from '@astrojs/starlight/components'; | `mcpwm` | `lua_module_mcpwm` | Generic PWM output (frequency/duty control) | | `event_publisher` | `lua_module_event_publisher` | Publish events from Lua into the Event Router | | `board_manager` | `lua_module_board_manager` | Board init + peripheral handles | +| `uart` | `lua_module_uart` | UART serial I/O (polling read/write) |