diff --git a/application/basic_demo/fatfs_image/scripts/clock_dial_demo.lua b/application/basic_demo/fatfs_image/scripts/clock_dial_demo.lua new file mode 100644 index 0000000..1fb0cd6 --- /dev/null +++ b/application/basic_demo/fatfs_image/scripts/clock_dial_demo.lua @@ -0,0 +1,180 @@ +local bm = require("board_manager") +local display = require("display") +local delay = require("delay") +local system = require("system") + +local FRAME_MS = 100 +local RUN_TIME_MS = 180000 + +local BG_R, BG_G, BG_B = 6, 12, 22 +local CARD_R, CARD_G, CARD_B = 10, 18, 30 +local RING_R, RING_G, RING_B = 22, 38, 64 +local TICK_R, TICK_G, TICK_B = 70, 118, 170 +local MAJOR_TICK_R, MAJOR_TICK_G, MAJOR_TICK_B = 124, 214, 255 +local HOUR_R, HOUR_G, HOUR_B = 208, 232, 255 +local MIN_R, MIN_G, MIN_B = 72, 170, 255 +local SEC_R, SEC_G, SEC_B = 0, 248, 208 +local SHADOW_R, SHADOW_G, SHADOW_B = 2, 8, 16 +local TEXT_R, TEXT_G, TEXT_B = 218, 238, 255 +local SUBTEXT_R, SUBTEXT_G, SUBTEXT_B = 112, 160, 204 +local GLOW_R, GLOW_G, GLOW_B = 56, 214, 255 + +local panel_handle, io_handle, width, height, panel_if = bm.get_display_lcd_params("display_lcd") +if not panel_handle then + print("[clock_dial_demo] ERROR: get_display_lcd_params(display_lcd) failed: " .. tostring(io_handle)) + return +end + +local ok, err = pcall(display.init, panel_handle, io_handle, width, height, panel_if) +if not ok then + print("[clock_dial_demo] ERROR: init failed: " .. tostring(err)) + return +end + +local screen_created = true + +local function cleanup() + if screen_created then + pcall(display.end_frame) + pcall(display.deinit) + screen_created = false + end +end + +width = display.width() +height = display.height() + +if width <= 0 or height <= 0 then + print("[clock_dial_demo] ERROR: invalid display size after init") + cleanup() + return +end + +local cx = width // 2 +local dial_cy = math.max(52, math.floor(height * 0.36)) +local radius = math.max(44, math.min(width, math.floor(height * 0.66)) // 2 - 10) +local card_margin = 12 +local card_x = card_margin +local card_y = 10 +local card_w = width - card_margin * 2 +local card_h = height - 20 +local digital_y = math.min(height - 62, dial_cy + radius + 16) + +local function polar_to_xy(center_x, center_y, r, angle_deg) + local rad = math.rad(angle_deg - 90) + local x = center_x + math.floor(math.cos(rad) * r + 0.5) + local y = center_y + math.floor(math.sin(rad) * r + 0.5) + return x, y +end + +local function draw_hand(angle_deg, inner_r, outer_r, color_r, color_g, color_b) + local x0, y0 = polar_to_xy(cx, dial_cy, inner_r, angle_deg) + local x1, y1 = polar_to_xy(cx, dial_cy, outer_r, angle_deg) + display.draw_line(x0, y0, x1, y1, color_r, color_g, color_b) +end + +local function draw_ticks() + for i = 0, 59 do + local angle = i * 6 + local is_major = (i % 5) == 0 + local outer_r = radius - 8 + local inner_r = outer_r - (is_major and 14 or 7) + local x0, y0 = polar_to_xy(cx, dial_cy, inner_r, angle) + local x1, y1 = polar_to_xy(cx, dial_cy, outer_r, angle) + local tick_r = is_major and MAJOR_TICK_R or TICK_R + local tick_g = is_major and MAJOR_TICK_G or TICK_G + local tick_b = is_major and MAJOR_TICK_B or TICK_B + + display.draw_line(x0, y0, x1, y1, tick_r, tick_g, tick_b) + end +end + +local function draw_dial_shell() + display.clear(BG_R, BG_G, BG_B) + display.fill_round_rect(card_x + 3, card_y + 6, card_w, card_h, 26, SHADOW_R, SHADOW_G, SHADOW_B) + display.fill_round_rect(card_x, card_y, card_w, card_h, 26, CARD_R, CARD_G, CARD_B) + display.draw_round_rect(card_x, card_y, card_w, card_h, 26, 34, 58, 88) + display.draw_round_rect(card_x + 4, card_y + 4, card_w - 8, card_h - 8, 22, 18, 40, 64) + + display.fill_circle(cx, dial_cy, radius + 12, 12, 24, 40) + display.fill_circle(cx, dial_cy, radius + 4, RING_R, RING_G, RING_B) + display.fill_circle(cx, dial_cy, radius - 8, 8, 18, 32) + display.fill_circle(cx, dial_cy, radius - 24, 4, 10, 20) + display.fill_arc(cx, dial_cy, radius - 14, radius - 4, -50, 42, GLOW_R, GLOW_G, GLOW_B) + display.fill_arc(cx, dial_cy, radius - 14, radius - 4, 132, 220, 26, 64, 108) + display.fill_arc(cx, dial_cy, radius - 28, radius - 22, -8, 108, 32, 112, 188) + display.fill_arc(cx, dial_cy, radius - 28, radius - 22, 172, 278, 18, 72, 130) + draw_ticks() +end + +local function draw_labels() + display.fill_round_rect(card_x + 16, card_y + 14, card_w - 32, 12, 6, 12, 28, 48) + display.fill_round_rect(card_x + 24, card_y + 18, 42, 4, 2, GLOW_R, GLOW_G, GLOW_B) + display.fill_round_rect(card_x + card_w - 66, card_y + 18, 42, 4, 2, 28, 110, 188) + display.fill_round_rect(cx - 26, digital_y + 55, 52, 4, 2, 18, 52, 88) + display.fill_round_rect(cx - 12, digital_y + 55, 24, 4, 2, GLOW_R, GLOW_G, GLOW_B) +end + +local function parse_now() + local h = tonumber(system.date("%H")) or 0 + local m = tonumber(system.date("%M")) or 0 + local s = tonumber(system.date("%S")) or 0 + return h, m, s +end + +local function draw_time(h, m, s) + local hour_angle = ((h % 12) + m / 60 + s / 3600) * 30 + local minute_angle = (m + s / 60) * 6 + local second_angle = s * 6 + local digital = string.format("%02d:%02d:%02d", h, m, s) + local digital_w = display.measure_text(digital, { font_size = 28 }) + + draw_hand(hour_angle, -10, radius - 56, HOUR_R, HOUR_G, HOUR_B) + draw_hand(hour_angle + 1, -10, radius - 56, HOUR_R, HOUR_G, HOUR_B) + draw_hand(minute_angle, -12, radius - 36, MIN_R, MIN_G, MIN_B) + draw_hand(second_angle, -18, radius - 28, SEC_R, SEC_G, SEC_B) + + display.fill_circle(cx, dial_cy, 12, 12, 26, 44) + display.fill_circle(cx, dial_cy, 7, SEC_R, SEC_G, SEC_B) + display.draw_circle(cx, dial_cy, 12, 100, 190, 255) + local sec_tip_x, sec_tip_y = polar_to_xy(cx, dial_cy, radius - 28, second_angle) + display.fill_circle(sec_tip_x, sec_tip_y, 4, SEC_R, SEC_G, SEC_B) + + display.fill_round_rect(cx - 88, digital_y - 8, 176, 52, 16, 8, 20, 34) + display.draw_round_rect(cx - 88, digital_y - 8, 176, 52, 16, 60, 132, 206) + display.draw_round_rect(cx - 84, digital_y - 4, 168, 44, 13, 18, 48, 78) + display.draw_text(cx - digital_w // 2, digital_y + 4, digital, { + r = TEXT_R, + g = TEXT_G, + b = TEXT_B, + font_size = 28, + bg_r = 8, + bg_g = 20, + bg_b = 34, + }) +end + +local run_ok, run_err = xpcall(function() + local elapsed_ms = 0 + + while elapsed_ms < RUN_TIME_MS do + local hour, minute, second = parse_now() + + display.begin_frame({ clear = true, r = BG_R, g = BG_G, b = BG_B }) + draw_dial_shell() + draw_labels() + draw_time(hour, minute, second) + display.present() + display.end_frame() + + delay.delay_ms(FRAME_MS) + elapsed_ms = elapsed_ms + FRAME_MS + end +end, debug.traceback) + +cleanup() +if not run_ok then + error(run_err) +end + +print("[clock_dial_demo] done") diff --git a/application/basic_demo/main/basic_demo_lua_modules.c b/application/basic_demo/main/basic_demo_lua_modules.c index 448e949..dd870e1 100644 --- a/application/basic_demo/main/basic_demo_lua_modules.c +++ b/application/basic_demo/main/basic_demo_lua_modules.c @@ -12,6 +12,7 @@ #include "lua_module_storage.h" #include "lua_module_button.h" #include "lua_module_esp_heap.h" +#include "lua_module_system.h" #include "lua_module_board_manager.h" #if defined(CONFIG_ESP_BOARD_DEV_AUDIO_CODEC_SUPPORT) @@ -92,5 +93,10 @@ esp_err_t basic_demo_lua_modules_register(void) return err; } + err = lua_module_system_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 5ca32ba..3baed4b 100644 --- a/application/basic_demo/main/idf_component.yml +++ b/application/basic_demo/main/idf_component.yml @@ -125,3 +125,6 @@ dependencies: lua_module_storage: path: ../../../components/lua_modules/lua_module_storage + + lua_module_system: + path: ../../../components/lua_modules/lua_module_system diff --git a/components/lua_modules/lua_module_system/CMakeLists.txt b/components/lua_modules/lua_module_system/CMakeLists.txt new file mode 100644 index 0000000..5cf2765 --- /dev/null +++ b/components/lua_modules/lua_module_system/CMakeLists.txt @@ -0,0 +1,11 @@ +idf_component_register( + SRCS + "src/lua_module_system.c" + INCLUDE_DIRS + "include" + REQUIRES + cap_lua + esp_timer + esp_wifi + heap +) diff --git a/components/lua_modules/lua_module_system/include/lua_module_system.h b/components/lua_modules/lua_module_system/include/lua_module_system.h new file mode 100644 index 0000000..58d5381 --- /dev/null +++ b/components/lua_modules/lua_module_system/include/lua_module_system.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_system(lua_State *L); +esp_err_t lua_module_system_register(void); + +#ifdef __cplusplus +} +#endif diff --git a/components/lua_modules/lua_module_system/skills/lua_module_system.md b/components/lua_modules/lua_module_system/skills/lua_module_system.md new file mode 100644 index 0000000..a5ca03a --- /dev/null +++ b/components/lua_modules/lua_module_system/skills/lua_module_system.md @@ -0,0 +1,60 @@ +# Lua System + +This skill describes how to correctly use system when writing Lua scripts. + +## How to call +- Import it with `local system = require("system")` +- This module does not require manual initialization after `require` + +## API + +### `system.time()` +- Inputs: none +- Output: `number` +- Returns the current Unix timestamp in seconds +- Raises an error if the system clock is not set + +### `system.date(format)` +- Inputs: + - `format`: optional `string`, defaults to `"%Y-%m-%d %H:%M:%S"` +- Output: `string` +- Returns the formatted local time string using `strftime`-style placeholders +- Raises an error if the format is too long or produces an empty string + +### `system.millis()` +- Inputs: none +- Output: `number` +- Returns the device uptime in milliseconds + +### `system.uptime()` +- Inputs: none +- Output: `integer` +- Returns the device uptime in seconds + +### `system.info()` +- Inputs: none +- Output: `table` +- Returns a table with these possible fields: + - `uptime_s`: `integer`, uptime in seconds + - `time`: `number`, current Unix timestamp in seconds + - `date`: `string`, formatted local time as `%Y-%m-%d %H:%M:%S` + - `sram_free`: `integer`, free internal SRAM bytes + - `sram_total`: `integer`, total internal SRAM bytes + - `sram_largest`: `integer`, largest free internal SRAM block in bytes + - `psram_free`: `integer`, free PSRAM bytes, present only when PSRAM exists + - `psram_total`: `integer`, total PSRAM bytes, present only when PSRAM exists + - `wifi_rssi`: `integer`, connected AP RSSI, present only when Wi-Fi STA is connected + - `wifi_ssid`: `string`, connected AP SSID, present only when available + +## Example +```lua +local system = require("system") + +print(system.time()) +print(system.date("%Y-%m-%d %H:%M:%S")) +print(system.millis()) +print(system.uptime()) + +local info = system.info() +print(info.sram_free) +``` diff --git a/components/lua_modules/lua_module_system/skills/skills_list.json b/components/lua_modules/lua_module_system/skills/skills_list.json new file mode 100644 index 0000000..17591e6 --- /dev/null +++ b/components/lua_modules/lua_module_system/skills/skills_list.json @@ -0,0 +1,13 @@ +{ + "skills": [ + { + "id": "lua_module_system", + "file": "lua_module_system.md", + "title": "lua_module_system", + "summary": "How to access time, uptime, memory, and Wi-Fi info from Lua through the system module.", + "cap_groups": [ + "cap_lua" + ] + } + ] +} diff --git a/components/lua_modules/lua_module_system/src/lua_module_system.c b/components/lua_modules/lua_module_system/src/lua_module_system.c new file mode 100644 index 0000000..688c407 --- /dev/null +++ b/components/lua_modules/lua_module_system/src/lua_module_system.c @@ -0,0 +1,158 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "lua_module_system.h" + +#include +#include +#include + +#include "cap_lua.h" +#include "esp_heap_caps.h" +#include "esp_timer.h" +#include "esp_wifi.h" +#include "lauxlib.h" +#include "sdkconfig.h" + +static void lua_module_system_ensure_tz(void) +{ + setenv("TZ", CONFIG_BASIC_DEMO_TIME_TIMEZONE, 1); + tzset(); +} + +static int lua_module_system_time(lua_State *L) +{ + time_t now; + + lua_module_system_ensure_tz(); + now = time(NULL); + if (now < 0) { + return luaL_error(L, "system clock not set"); + } + + lua_pushnumber(L, (lua_Number)now); + return 1; +} + +static int lua_module_system_date(lua_State *L) +{ + const char *fmt = luaL_optstring(L, 1, "%Y-%m-%d %H:%M:%S"); + char buf[128]; + struct tm local_time; + time_t now; + size_t len; + + lua_module_system_ensure_tz(); + now = time(NULL); + localtime_r(&now, &local_time); + + len = strftime(buf, sizeof(buf), fmt, &local_time); + if (len == 0) { + return luaL_error(L, "system.date: format too long or produced empty string"); + } + + lua_pushstring(L, buf); + return 1; +} + +static int lua_module_system_millis(lua_State *L) +{ + int64_t us = esp_timer_get_time(); + + lua_pushnumber(L, (lua_Number)(us / 1000)); + return 1; +} + +static int lua_module_system_uptime(lua_State *L) +{ + int64_t us = esp_timer_get_time(); + + lua_pushinteger(L, (lua_Integer)(us / 1000000)); + return 1; +} + +static int lua_module_system_info(lua_State *L) +{ + char date_buf[32]; + struct tm local_time; + time_t now; + size_t psram_total; + wifi_ap_record_t ap_info = {0}; + int64_t us; + + lua_newtable(L); + + us = esp_timer_get_time(); + lua_pushinteger(L, (lua_Integer)(us / 1000000)); + lua_setfield(L, -2, "uptime_s"); + + lua_module_system_ensure_tz(); + now = time(NULL); + lua_pushnumber(L, (lua_Number)now); + lua_setfield(L, -2, "time"); + + localtime_r(&now, &local_time); + if (strftime(date_buf, sizeof(date_buf), "%Y-%m-%d %H:%M:%S", &local_time) > 0) { + lua_pushstring(L, date_buf); + lua_setfield(L, -2, "date"); + } + + lua_pushinteger(L, (lua_Integer)heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); + lua_setfield(L, -2, "sram_free"); + + lua_pushinteger(L, (lua_Integer)heap_caps_get_total_size(MALLOC_CAP_INTERNAL)); + lua_setfield(L, -2, "sram_total"); + + lua_pushinteger(L, (lua_Integer)heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); + lua_setfield(L, -2, "sram_largest"); + + psram_total = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + if (psram_total > 0) { + lua_pushinteger(L, (lua_Integer)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + lua_setfield(L, -2, "psram_free"); + + lua_pushinteger(L, (lua_Integer)psram_total); + lua_setfield(L, -2, "psram_total"); + } + + if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) { + lua_pushinteger(L, (lua_Integer)ap_info.rssi); + lua_setfield(L, -2, "wifi_rssi"); + + if (ap_info.ssid[0] != '\0') { + lua_pushstring(L, (const char *)ap_info.ssid); + lua_setfield(L, -2, "wifi_ssid"); + } + } + + return 1; +} + +int luaopen_system(lua_State *L) +{ + lua_newtable(L); + + lua_pushcfunction(L, lua_module_system_time); + lua_setfield(L, -2, "time"); + + lua_pushcfunction(L, lua_module_system_date); + lua_setfield(L, -2, "date"); + + lua_pushcfunction(L, lua_module_system_millis); + lua_setfield(L, -2, "millis"); + + lua_pushcfunction(L, lua_module_system_uptime); + lua_setfield(L, -2, "uptime"); + + lua_pushcfunction(L, lua_module_system_info); + lua_setfield(L, -2, "info"); + + return 1; +} + +esp_err_t lua_module_system_register(void) +{ + return cap_lua_register_module("system", luaopen_system); +}