diff --git a/README.md b/README.md index 8cf31af..cf9c784 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ Cardstock is a lightweight Lua-based OS for the M5Stack Cardputer. -The project currently builds a Lua VM and loads a lua entrypoint (which will eventually be a launcher) +This repository contains all the C/C++ infrastructure and bindings required to support the Lua portion of the OS. -I am currently writing Lua bindings for all the M5Cardputer libraries, starting with the GFX library right now. +Currently supported modules are: +- M5GFX +- M5Canvas +- Keyboard -DISCLAIMER: A decent portion of the C++ is written by AI right now. The plan is to go back and check over/rewrite this later as I add more complex functionality to the "bootloader". \ No newline at end of file +The eventual goal is for every API to be built and passed through to Lua, with certain things like wireless being controlled globally across apps. \ No newline at end of file diff --git a/src/lua/bindings/lua_gfx.cpp b/src/lua/bindings/lua_gfx.cpp index 542033a..157f9c6 100644 --- a/src/lua/bindings/lua_gfx.cpp +++ b/src/lua/bindings/lua_gfx.cpp @@ -1,6 +1,33 @@ #include "lua_gfx.h" #include "services/GfxService.h" +#include "M5Cardputer.h" + +// ------------------------------- +// gfx.sprite userdata (M5Canvas) +// ------------------------------- + +static const char* kSpriteMT = "gfx.sprite"; + +struct LuaSprite { + M5Canvas* canvas = nullptr; +}; + +static LuaSprite* lua_check_sprite(lua_State* L, int idx) { + return static_cast(luaL_checkudata(L, idx, kSpriteMT)); +} + +static M5Canvas* lua_sprite_require_alive(lua_State* L, LuaSprite* s) { + if (!s || !s->canvas) luaL_error(L, "sprite is freed"); + return s->canvas; +} + +static void lua_sprite_free(LuaSprite* s) { + if (!s || !s->canvas) return; + s->canvas->deleteSprite(); + delete s->canvas; + s->canvas = nullptr; +} static uint16_t lua_check_u16(lua_State* L, int idx) { lua_Integer v = luaL_checkinteger(L, idx); @@ -9,6 +36,114 @@ static uint16_t lua_check_u16(lua_State* L, int idx) { return static_cast(v); } +static int l_sprite_gc(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + lua_sprite_free(s); + return 0; +} + +static int l_sprite_free(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + lua_sprite_free(s); + return 0; +} + +static int l_sprite_clear(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + M5Canvas* c = lua_sprite_require_alive(L, s); + uint16_t color = 0x0000; + if (lua_gettop(L) >= 2 && !lua_isnil(L, 2)) color = lua_check_u16(L, 2); + c->fillScreen(color); + return 0; +} + +static int l_sprite_set_text_color(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + M5Canvas* c = lua_sprite_require_alive(L, s); + uint16_t fg = lua_check_u16(L, 2); + if (lua_gettop(L) >= 3 && !lua_isnil(L, 3)) { + uint16_t bg = lua_check_u16(L, 3); + c->setTextColor(fg, bg); + } else { + c->setTextColor(fg); + } + return 0; +} + +static int l_sprite_set_text_size(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + M5Canvas* c = lua_sprite_require_alive(L, s); + lua_Integer size = luaL_checkinteger(L, 2); + if (size < 1) size = 1; + if (size > 255) size = 255; + c->setTextSize(static_cast(size)); + return 0; +} + +static int l_sprite_draw_string(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + M5Canvas* c = lua_sprite_require_alive(L, s); + const char* text = luaL_checkstring(L, 2); + int32_t x = static_cast(luaL_checkinteger(L, 3)); + int32_t y = static_cast(luaL_checkinteger(L, 4)); + int32_t w = c->drawString(text, x, y); + lua_pushinteger(L, static_cast(w)); + return 1; +} + +static int l_sprite_draw_center_string(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + M5Canvas* c = lua_sprite_require_alive(L, s); + const char* text = luaL_checkstring(L, 2); + int32_t x = static_cast(luaL_checkinteger(L, 3)); + int32_t y = static_cast(luaL_checkinteger(L, 4)); + int32_t w = c->drawCenterString(text, x, y); + lua_pushinteger(L, static_cast(w)); + return 1; +} + +static int l_sprite_push(lua_State* L) { + LuaSprite* s = lua_check_sprite(L, 1); + M5Canvas* c = lua_sprite_require_alive(L, s); + int32_t x = static_cast(luaL_checkinteger(L, 2)); + int32_t y = static_cast(luaL_checkinteger(L, 3)); + c->pushSprite(x, y); + return 0; +} + +static const luaL_Reg kSpriteMethods[] = { + {"clear", l_sprite_clear}, + {"setTextColor", l_sprite_set_text_color}, + {"setTextSize", l_sprite_set_text_size}, + {"drawString", l_sprite_draw_string}, + {"drawCenterString", l_sprite_draw_center_string}, + {"push", l_sprite_push}, + {"free", l_sprite_free}, + {nullptr, nullptr}, +}; + +static int l_gfx_new_sprite(lua_State* L) { + int32_t w = static_cast(luaL_checkinteger(L, 1)); + int32_t h = static_cast(luaL_checkinteger(L, 2)); + if (w <= 0 || h <= 0) luaL_error(L, "newSprite: width and height must be > 0"); + + LuaSprite* ud = static_cast(lua_newuserdatauv(L, sizeof(LuaSprite), 0)); + new (ud) LuaSprite(); + + // Create canvas (parented to the real display). + ud->canvas = new (std::nothrow) M5Canvas(&M5Cardputer.Display); + if (!ud->canvas) luaL_error(L, "newSprite: OOM allocating canvas"); + + bool ok = ud->canvas->createSprite(w, h); + if (!ok) { + lua_sprite_free(ud); + luaL_error(L, "newSprite: createSprite failed"); + } + + luaL_setmetatable(L, kSpriteMT); + return 1; +} + static int l_gfx_clear(lua_State* L) { uint16_t color = 0x0000; if (lua_gettop(L) >= 1 && !lua_isnil(L, 1)) color = lua_check_u16(L, 1); @@ -63,7 +198,41 @@ static int l_gfx_draw_string(lua_State* L) { return 1; } +static int l_gfx_fill_rect(lua_State* L) { + int32_t x = static_cast(luaL_checkinteger(L, 1)); + int32_t y = static_cast(luaL_checkinteger(L, 2)); + int32_t w = static_cast(luaL_checkinteger(L, 3)); + int32_t h = static_cast(luaL_checkinteger(L, 4)); + uint16_t color = lua_check_u16(L, 5); + GfxService::fillRect(x, y, w, h, color); + return 0; +} + +static int l_gfx_draw_center_string(lua_State* L) { + const char* s = luaL_checkstring(L, 1); + int32_t x = static_cast(luaL_checkinteger(L, 2)); + int32_t y = static_cast(luaL_checkinteger(L, 3)); + int32_t font = -1; + if (lua_gettop(L) >= 4 && !lua_isnil(L, 4)) font = static_cast(luaL_checkinteger(L, 4)); + int32_t w = GfxService::drawCenterString(s, x, y, font); + lua_pushinteger(L, static_cast(w)); + return 1; +} + +static int l_gfx_width(lua_State* L) { + int32_t w = GfxService::width(); + lua_pushinteger(L, static_cast(w)); + return 1; +} + +static int l_gfx_height(lua_State* L) { + int32_t h = GfxService::height(); + lua_pushinteger(L, static_cast(h)); + return 1; +} + static const luaL_Reg kGfxLib[] = { + {"newSprite", l_gfx_new_sprite}, {"clear", l_gfx_clear}, {"setCursor", l_gfx_set_cursor}, {"setTextSize", l_gfx_set_text_size}, @@ -71,10 +240,26 @@ static const luaL_Reg kGfxLib[] = { {"print", l_gfx_print}, {"println", l_gfx_println}, {"drawString", l_gfx_draw_string}, + {"fillRect", l_gfx_fill_rect}, + {"drawCenterString", l_gfx_draw_center_string}, + {"width", l_gfx_width}, + {"height", l_gfx_height}, {nullptr, nullptr}, }; int luaopen_gfx(lua_State* L) { + // Create gfx.sprite metatable. + if (luaL_newmetatable(L, kSpriteMT)) { + // metatable.__index = methods table + luaL_newlib(L, kSpriteMethods); + lua_setfield(L, -2, "__index"); + + // metatable.__gc = l_sprite_gc + lua_pushcfunction(L, l_sprite_gc); + lua_setfield(L, -2, "__gc"); + } + lua_pop(L, 1); // pop metatable + luaL_newlib(L, kGfxLib); return 1; } diff --git a/src/lua/bindings/lua_keyboard.cpp b/src/lua/bindings/lua_keyboard.cpp new file mode 100644 index 0000000..748bd23 --- /dev/null +++ b/src/lua/bindings/lua_keyboard.cpp @@ -0,0 +1,39 @@ +#include "lua_keyboard.h" + +#include "services/KeyboardService.h" + +static int l_keyboard_is_changed(lua_State* L) { + lua_pushboolean(L, KeyboardService::isChanged()); + return 1; +} + +static int l_keyboard_is_pressed(lua_State* L) { + lua_pushinteger(L, KeyboardService::isPressed()); + return 1; +} + +static int l_keyboard_is_key_pressed(lua_State* L) { + const char* c = luaL_checkstring(L, 1); + lua_pushboolean(L, KeyboardService::isKeyPressed(c[0])); + return 1; +} + +static int l_keyboard_get_key(lua_State* L) { + int32_t x = static_cast(luaL_checkinteger(L, 1)); + int32_t y = static_cast(luaL_checkinteger(L, 2)); + lua_pushinteger(L, KeyboardService::getKey(x, y)); + return 1; +} + +static const luaL_Reg kKeyboardLib[] = { + {"isChanged", l_keyboard_is_changed}, + {"isPressed", l_keyboard_is_pressed}, + {"isKeyPressed", l_keyboard_is_key_pressed}, + {"getKey", l_keyboard_get_key}, + {nullptr, nullptr}, +}; + +int luaopen_keyboard(lua_State* L) { + luaL_newlib(L, kKeyboardLib); + return 1; +} \ No newline at end of file diff --git a/src/lua/bindings/lua_keyboard.h b/src/lua/bindings/lua_keyboard.h new file mode 100644 index 0000000..5f8c076 --- /dev/null +++ b/src/lua/bindings/lua_keyboard.h @@ -0,0 +1,6 @@ +#pragma once + +#include "lua.hpp" + +// Lua module entrypoint: local keyboard = require("keyboard") +int luaopen_keyboard(lua_State* L); \ No newline at end of file diff --git a/src/lua/require_sd.cpp b/src/lua/require_sd.cpp new file mode 100644 index 0000000..3d6acbe --- /dev/null +++ b/src/lua/require_sd.cpp @@ -0,0 +1,217 @@ +#include "require_sd.h" + +#include +#include + +#include +#include + +namespace { + +// Registry key for current app root. +static const char* kAppRootKey = "cardstock.app_root"; +static const char* kSysLibRoot = "/syslib"; + +static String normalize_abs_path(const String& p) { + if (!p.length()) return ""; + String out = p; + if (out[0] != '/') out = String("/") + out; + // Strip trailing slashes (except for "/") + while (out.length() > 1 && out[out.length() - 1] == '/') out.remove(out.length() - 1); + return out; +} + +static bool contains_path_traversal(const char* s) { + if (!s) return false; + // Disallow any ".." path segment. + // (Accepts module names like "foo..bar"? That would be weird; still blocked.) + return strstr(s, "..") != nullptr; +} + +static String module_name_to_rel_path(const char* modname) { + // Convert dotted module name to path: "a.b.c" -> "a/b/c" + String out; + if (!modname) return out; + for (const char* p = modname; *p; ++p) { + char c = *p; + if (c == '.') out += '/'; + else if (c == '\\') out += '/'; + else out += c; + } + // Strip any leading slashes to keep it relative. + while (out.length() && out[0] == '/') out.remove(0, 1); + return out; +} + +static bool read_entire_file(const String& path, std::unique_ptr& out_buf, size_t& out_len, String& out_err) { + const String normalized = normalize_abs_path(path); + File f = SD.open(normalized.c_str(), FILE_READ); + if (!f) { + out_err = "open failed: " + normalized; + return false; + } + + size_t sz = static_cast(f.size()); + if (sz == 0) { + out_err = "empty file: " + normalized; + f.close(); + return false; + } + + std::unique_ptr buf(new (std::nothrow) uint8_t[sz]); + if (!buf) { + out_err = "OOM reading: " + normalized; + f.close(); + return false; + } + + size_t read_total = 0; + while (read_total < sz) { + int n = f.read(buf.get() + read_total, sz - read_total); + if (n <= 0) break; + read_total += static_cast(n); + } + f.close(); + + if (read_total != sz) { + out_err = "short read: " + normalized; + return false; + } + + out_buf = std::move(buf); + out_len = sz; + return true; +} + +static String get_app_root(lua_State* L) { + lua_pushstring(L, kAppRootKey); + lua_gettable(L, LUA_REGISTRYINDEX); // registry[kAppRootKey] + const char* s = lua_tostring(L, -1); + String out = s ? String(s) : String(""); + lua_pop(L, 1); + return out; +} + +static void push_searcher_error(lua_State* L, const String& msg) { + // Match Lua's style: searchers return a string starting with "\n\t..." + String m = "\n\t"; + m += msg; + lua_pushstring(L, m.c_str()); +} + +static int l_cardstock_searcher(lua_State* L) { + const char* modname = luaL_checkstring(L, 1); + if (contains_path_traversal(modname)) { + push_searcher_error(L, String("invalid module name: ") + modname); + return 1; + } + + const String rel = module_name_to_rel_path(modname); + if (!rel.length()) { + push_searcher_error(L, "invalid module name"); + return 1; + } + + const String app_root = get_app_root(L); + bool loaded = false; + + // Build candidates. + String candidates[4]; + int n = 0; + + if (app_root.length()) { + candidates[n++] = app_root + "/" + rel + ".lua"; + candidates[n++] = app_root + "/" + rel + "/init.lua"; + } + + candidates[n++] = String(kSysLibRoot) + "/" + rel + ".lua"; + candidates[n++] = String(kSysLibRoot) + "/" + rel + "/init.lua"; + + // Try candidates in order. + for (int i = 0; i < n; i++) { + const String& p = candidates[i]; + String err; + std::unique_ptr buf; + size_t len = 0; + + // Load full file into chunk. + if (!read_entire_file(p, buf, len, err)) continue; + + int rc = luaL_loadbuffer(L, reinterpret_cast(buf.get()), len, p.c_str()); + if (rc != LUA_OK) { + const char* emsg = lua_tostring(L, -1); + String m = "load error in "; + m += p; + m += ": "; + m += emsg ? emsg : "(unknown)"; + lua_pop(L, 1); + push_searcher_error(L, m); + return 1; + } + + lua_pushstring(L, normalize_abs_path(p).c_str()); + loaded = true; + break; + } + + if (loaded) return 2; + + // Not found. Provide a single aggregated message. + String msg = "module '"; + msg += modname; + msg += "' not found (searched "; + if (app_root.length()) { + msg += app_root; + msg += " and "; + } + msg += kSysLibRoot; + msg += ")"; + push_searcher_error(L, msg); + return 1; +} + +static void insert_searcher(lua_State* L) { + // package.searchers is a table (Lua 5.2+). + lua_getglobal(L, "package"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + lua_getfield(L, -1, "searchers"); + if (!lua_istable(L, -1)) { + lua_pop(L, 2); + return; + } + + // Insert at index 2 (after preload searcher). + const int insert_at = 2; + int len = static_cast(luaL_len(L, -1)); + for (int i = len + 1; i > insert_at; --i) { + lua_rawgeti(L, -1, i - 1); + lua_rawseti(L, -2, i); + } + + lua_pushcfunction(L, l_cardstock_searcher); + lua_rawseti(L, -2, insert_at); + + lua_pop(L, 2); // pop searchers, package +} + +} // namespace + +void lua_cardstock_install_require(lua_State* L) { + if (!L) return; + insert_searcher(L); +} + +void lua_cardstock_set_app_root(lua_State* L, const char* app_root) { + if (!L) return; + String root = app_root ? String(app_root) : String(""); + root = normalize_abs_path(root); + lua_pushstring(L, kAppRootKey); + lua_pushstring(L, root.c_str()); + lua_settable(L, LUA_REGISTRYINDEX); +} + + diff --git a/src/lua/require_sd.h b/src/lua/require_sd.h new file mode 100644 index 0000000..ad7f234 --- /dev/null +++ b/src/lua/require_sd.h @@ -0,0 +1,20 @@ +#pragma once + +// Custom SD-backed module loader ("require") for Cardstock. +// +// Installs a Lua `package.searcher` that loads modules from: +// 1) /.lua and //init.lua +// 2) /syslib/.lua and /syslib//init.lua +// +// The app root is set by the host (C++) per loaded entrypoint. + +#include "lua.hpp" + +// Install the Cardstock SD-backed searcher into `package.searchers`. +void lua_cardstock_install_require(lua_State* L); + +// Set the current app root used by the searcher (absolute, e.g. "/apps/foo"). +// Pass "" or nullptr to disable app-scoped lookup. +void lua_cardstock_set_app_root(lua_State* L, const char* app_root); + + diff --git a/src/main.cpp b/src/main.cpp index bf77dbb..88545f6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,13 +26,15 @@ extern "C" { #include "M5Cardputer.h" #include "lua/bindings/lua_gfx.h" +#include "lua/require_sd.h" +#include "lua/bindings/lua_keyboard.h" // ------------------------------- // Build-time configuration knobs // ------------------------------- // Default Lua entrypoint on SD. Override with: -DCARDSTOCK_LUA_ENTRY=\"/my.lua\" #ifndef CARDSTOCK_LUA_ENTRY -#define CARDSTOCK_LUA_ENTRY "/app.lua" +#define CARDSTOCK_LUA_ENTRY "/launcher/main.lua" #endif // SD init pins. If you don't define these, the code falls back to SD.begin(CS). @@ -57,6 +59,7 @@ struct LuaHost { String pending_path; bool reload_requested = false; uint32_t last_ms = 0; + String app_root; }; static LuaHost g_host; @@ -100,22 +103,25 @@ static bool init_sd_card() { } static bool read_entire_file_from_sd(const String& path, std::unique_ptr& out_buf, size_t& out_len, String& out_err) { - File f = SD.open(path.c_str(), FILE_READ); + // ESP32 SD/FS paths are effectively absolute (no working-directory semantics). + // Root files often work without a leading '/', but subdirectories commonly won't. + const String normalized = (path.length() && path[0] == '/') ? path : (String("/") + path); + File f = SD.open(normalized.c_str(), FILE_READ); if (!f) { - out_err = "SD.open failed: " + path; + out_err = "SD.open failed: " + normalized; return false; } size_t sz = static_cast(f.size()); if (sz == 0) { - out_err = "Empty file: " + path; + out_err = "Empty file: " + normalized; f.close(); return false; } std::unique_ptr buf(new (std::nothrow) uint8_t[sz]); if (!buf) { - out_err = "OOM reading: " + path; + out_err = "OOM reading: " + normalized; f.close(); return false; } @@ -129,7 +135,7 @@ static bool read_entire_file_from_sd(const String& path, std::unique_ptr 1 && out[out.length() - 1] == '/') out.remove(out.length() - 1); + return out; +} + +static String compute_app_root_for_entrypoint(const String& entry_script_path) { + const String p = normalize_abs_path(entry_script_path); + + // Preferred: apps live at /apps//... + if (p.startsWith("/apps/")) { + const int app_id_start = 6; // strlen("/apps/") + const int slash_after_app_id = p.indexOf('/', app_id_start); + if (slash_after_app_id > app_id_start) { + return p.substring(0, slash_after_app_id); // "/apps/" + } + } + + // Fallback: use directory of the entrypoint (useful for launcher scripts too). + const int last_slash = p.lastIndexOf('/'); + if (last_slash > 0) return p.substring(0, last_slash); + return ""; +} + static int l_print_serial(lua_State* L) { int n = lua_gettop(L); String line; @@ -217,9 +249,16 @@ static bool lua_boot_and_load(LuaHost& host, const String& script_path) { host_set_for_lua(host.L, &host); luaL_openlibs(host.L); + // Install SD-backed require searcher and set app root scope for this entrypoint. + lua_cardstock_install_require(host.L); + host.app_root = compute_app_root_for_entrypoint(script_path); + lua_cardstock_set_app_root(host.L, host.app_root.c_str()); + // Register built-in modules (Lua: local gfx = require("gfx")). luaL_requiref(host.L, "gfx", luaopen_gfx, 1); lua_pop(host.L, 1); // pop returned module table + luaL_requiref(host.L, "keyboard", luaopen_keyboard, 1); + lua_pop(host.L, 1); // pop returned module table // Override print() to go to Serial (handy on embedded). lua_pushcfunction(host.L, l_print_serial); diff --git a/src/services/GfxService.cpp b/src/services/GfxService.cpp index 6b6b902..af8c70a 100644 --- a/src/services/GfxService.cpp +++ b/src/services/GfxService.cpp @@ -4,47 +4,66 @@ namespace GfxService { -void clear(uint16_t color) { - M5Cardputer.Display.fillScreen(color); -} - -void setCursor(int32_t x, int32_t y) { - M5Cardputer.Display.setCursor(x, y); -} - -void setTextSize(uint8_t size) { - M5Cardputer.Display.setTextSize(size); -} - -void setTextColor(uint16_t fg, int32_t bg) { - if (bg < 0) { - M5Cardputer.Display.setTextColor(fg); - } else { - M5Cardputer.Display.setTextColor(fg, static_cast(bg)); + void clear(uint16_t color) { + M5Cardputer.Display.fillScreen(color); } -} -void print(const char* s) { - if (!s) return; - M5Cardputer.Display.print(s); -} - -void println(const char* s) { - if (!s) { - M5Cardputer.Display.println(); - return; + void setCursor(int32_t x, int32_t y) { + M5Cardputer.Display.setCursor(x, y); } - M5Cardputer.Display.println(s); -} -int32_t drawString(const char* s, int32_t x, int32_t y, int32_t font) { - if (!s) return 0; - if (font < 0) { - return M5Cardputer.Display.drawString(s, x, y); + void setTextSize(uint8_t size) { + M5Cardputer.Display.setTextSize(size); } - return M5Cardputer.Display.drawString(s, x, y, font); -} + void setTextColor(uint16_t fg, int32_t bg) { + if (bg < 0) { + M5Cardputer.Display.setTextColor(fg); + } else { + M5Cardputer.Display.setTextColor(fg, static_cast(bg)); + } + } + + void print(const char* s) { + if (!s) return; + M5Cardputer.Display.print(s); + } + + void println(const char* s) { + if (!s) { + M5Cardputer.Display.println(); + return; + } + M5Cardputer.Display.println(s); + } + + int32_t drawString(const char* s, int32_t x, int32_t y, int32_t font) { + if (!s) return 0; + if (font < 0) { + return M5Cardputer.Display.drawString(s, x, y); + } + return M5Cardputer.Display.drawString(s, x, y, font); + } + + void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t color) { + M5Cardputer.Display.fillRect(x, y, w, h, color); + } + + int32_t width() { + return M5Cardputer.Display.width(); + } + + int32_t height() { + return M5Cardputer.Display.height(); + } + + int32_t drawCenterString(const char* s, int32_t x, int32_t y, int32_t font) { + if (!s) return 0; + if (font < 0) { + return M5Cardputer.Display.drawCenterString(s, x, y); + } + return M5Cardputer.Display.drawCenterString(s, x, y, font); + } } // namespace GfxService diff --git a/src/services/GfxService.h b/src/services/GfxService.h index 891cf8d..a4e7eb5 100644 --- a/src/services/GfxService.h +++ b/src/services/GfxService.h @@ -11,10 +11,14 @@ void setTextSize(uint8_t size); void setTextColor(uint16_t fg, int32_t bg = -1); // bg < 0 => don't set background void print(const char* s); void println(const char* s); +void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t color = 0x0000 /* BLACK */); // Returns the pixel width of the rendered string (per M5GFX/LovyanGFX convention). int32_t drawString(const char* s, int32_t x, int32_t y, int32_t font = -1); +int32_t drawCenterString(const char* s, int32_t x, int32_t y, int32_t font = -1); +int32_t width(); +int32_t height(); } // namespace GfxService diff --git a/src/services/KeyboardService.cpp b/src/services/KeyboardService.cpp new file mode 100644 index 0000000..8b57352 --- /dev/null +++ b/src/services/KeyboardService.cpp @@ -0,0 +1,21 @@ +#include "KeyboardService.h" + +#include "M5Cardputer.h" + +namespace KeyboardService { + bool isChanged() { + return M5Cardputer.Keyboard.isChange(); + } + uint8_t isPressed() { + return M5Cardputer.Keyboard.isPressed(); + } + bool isKeyPressed(char c) { + return M5Cardputer.Keyboard.isKeyPressed(c); + } + uint8_t getKey(int32_t x, int32_t y) { + Point2D_t keyCoord; + keyCoord.x = static_cast(x); + keyCoord.y = static_cast(y); + return M5Cardputer.Keyboard.getKey(keyCoord); + } +} \ No newline at end of file diff --git a/src/services/KeyboardService.h b/src/services/KeyboardService.h new file mode 100644 index 0000000..10484fc --- /dev/null +++ b/src/services/KeyboardService.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace KeyboardService { + bool isChanged(); + uint8_t isPressed(); // returns number of pressed keys + bool isKeyPressed(char c); // returns true if the key is pressed + uint8_t getKey(int32_t x, int32_t y); // returns the key code of the pressed key at (x,y) +} \ No newline at end of file