
1. 项目背景与痛点分析如果你是一位嵌入式或物联网爱好者很可能对“Holocubic”这个名字不陌生。它是一款基于ESP32的开源桌面小摆件以其独特的透明亚克力外壳和悬浮显示效果一度在极客圈内风靡。然而在深入把玩后许多开发者包括我自己都遇到了几个共同的瓶颈屏幕尺寸太小通常为1.3或1.5寸显示内容有限交互方式单一大多为预编译的固件修改功能需要重新编译、烧录开发调试周期长可玩性受限难以快速实现个性化的动态效果或联网功能。正是这些痛点驱动了本次的改造项目。我们的目标很明确保留Holocubic标志性的“悬浮”美学但对其进行全面升级。核心升级点包括显示升级将屏幕从1.x寸升级到3.2寸大幅提升可视面积和内容承载能力。主控升级从ESP32升级到性能更强的ESP32-S3拥有更快的双核处理器、更多的GPIO和USB OTG支持为复杂图形和脚本运行奠定硬件基础。交互升级引入Lua脚本引擎实现固件功能的“热更新”。用户无需接触C/C和复杂的编译环境通过编写简单的Lua脚本就能动态控制屏幕显示内容、联网获取数据、实现动画效果极大降低了二次开发门槛。框架升级采用LVGL (Light and Versatile Graphics Library)作为图形库。LVGL是一个高度可裁剪、硬件要求低的开源嵌入式GUI库其丰富的控件和流畅的动画效果非常适合在ESP32-S3上构建精美的用户界面。最终我们得到了一个“超大杯”的Holocubic它拥有更大的透明显示屏运行着LVGL驱动的精美UI并且能通过Lua脚本实时改变行为。下文将完整拆解从硬件选型、环境搭建、固件开发到Lua脚本编写的全流程。2. 核心硬件与软件环境准备工欲善其事必先利其器。一次成功的硬件改造离不开合适的组件和稳定的开发环境。2.1 硬件清单 (BOM)以下是本项目的核心硬件组件所有部件均可在主流电商平台找到组件型号/规格说明主控芯片ESP32-S3 (模组如ESP32-S3-WROOM-1-N16R8)双核240MHz集成USB JTAG支持PSRAM是运行LVGL和Lua的理想选择。注意务必确认是ESP32-S3而非ESP32。显示屏3.2寸 IPS TFT LCD (分辨率建议320x240或480x320)接口优先选择SPI以节省GPIO。本项目以320x240的ST7789驱动芯片屏幕为例。屏幕接口SPI相比并行RGB接口SPI占用引脚少接线简单虽刷新率较低但对信息显示类应用足够。结构外壳定制亚克力透明外壳为了容纳3.2寸屏幕需要重新设计或购买尺寸匹配的透明外壳。其他杜邦线、USB Type-C数据线、电源5V/2A用于连接、供电与调试。关键提醒ESP32-S3与ESP32的区别这是第一个大坑。两者引脚和部分功能相似但内核与二进制格式不兼容。在后续使用esptool.py或PlatformIO烧录时必须指定正确的芯片型号否则会出现类似“a fatal error occurred: this chip is esp32-s3, not esp32. wrong --chip argument”的错误。2.2 软件开发环境搭建我们选择VS Code PlatformIO作为一站式开发环境它集成了代码编辑、库管理和固件烧录极大简化了ESP32的开发流程。安装VS Code: 从官网下载并安装。安装PlatformIO插件: 在VS Code扩展商店搜索“PlatformIO IDE”并安装。创建新项目:打开VS Code点击左侧的PlatformIO图标小蚂蚁。选择“New Project”。在项目向导中填写项目名称如holocubic_s3_lvgl。Board选择Espressif ESP32-S3-DevKitC-1(或根据你的具体模组选择核心是ESP32-S3)。Framework选择Arduino或ESP-IDF。为了快速上手和丰富的库支持本项目选用Arduino框架。选择项目路径后点击“Finish”。PlatformIO会自动创建项目结构并下载必要的工具链。你的项目目录结构将类似于holocubic_s3_lvgl/ ├── include/ # 存放头文件 ├── lib/ # 存放第三方库 ├── src/ # 存放主源代码 │ └── main.cpp # 程序入口 ├── test/ # 测试代码 └── platformio.ini # 项目配置文件核心2.3 关键库依赖配置项目的核心功能依赖于几个重要的开源库。我们需要在platformio.ini文件中进行声明。这是该文件的核心内容[env:esp32-s3-devkitc-1] platform espressif32 board esp32-s3-devkitc-1 framework arduino monitor_speed 115200 ; 启用PSRAM支持如果你的模组有 board_build.arduino.memory_type qio_opi board_build.partitions huge_app.csv ; 核心库依赖 lib_deps ; LVGL图形库 lvgl/lvgl^8.3.10 ; TFT_eSPI强大的SPI屏幕驱动库已集成多种芯片驱动 bodmer/TFT_eSPI^2.5.0 ; Lua解释器使ESP32能够运行Lua脚本 bleaklight/eLua^0.2.0 ; 网络时间、WiFi管理等其他所需库 arduino-libraries/NTPClient^3.2.1 ; 构建配置优化代码大小和性能 build_flags -DLV_CONF_INCLUDE_SIMPLE -DLV_USE_LOG1保存platformio.ini后PlatformIO会自动下载并安装上述所有库。3. 核心原理与框架拆解在动手写代码前理解整个系统的运行框架至关重要。这能帮助你在遇到问题时快速定位是硬件驱动、图形渲染还是脚本逻辑层的问题。3.1 系统架构图整个项目的软件架构可以简化为以下三层[Lua 脚本层] (动态逻辑) | | 调用LVGL绑定API / 读取传感器数据 V [Arduino 核心层] (桥梁与驱动) |-- LVGL 图形引擎 (负责界面绘制) |-- TFT_eSPI 显示驱动 (负责向屏幕发送像素数据) |-- eLua 解释器 (负责解析和执行Lua脚本) |-- WiFi/网络服务 (提供网络能力) | V [硬件层] (执行) ESP32-S3 - SPI - 3.2寸 TFT屏幕工作流程设备上电Arduinosetup()函数初始化LVGL、屏幕驱动、Lua虚拟机、文件系统、WiFi等。在loop()中持续调用lv_timer_handler()来处理LVGL的界面刷新和事件。用户将编写好的Lua脚本如clock.lua放入设备的文件系统如SPIFFS。主程序加载并执行该Lua脚本。Lua脚本通过我们预先注册的“绑定函数”调用LVGL的API来创建标签、按钮、图表等控件并设置其属性如文本、位置、颜色。Lua脚本也可以设置定时器定期更新控件状态如刷新时间或读取网络数据更新界面。LVGL根据控件的状态变化通过TFT_eSPI驱动最终将图形渲染到3.2寸屏幕上。3.2 关键技术点解析1. LVGL与TFT_eSPI的适配LVGL是一个纯图形库它需要一个“显示驱动”和“输入设备驱动”来与硬件交互。TFT_eSPI库则是一个优秀的硬件抽象层它封装了ST7789等众多屏幕芯片的底层SPI通信细节。我们的任务是将两者连接起来即告诉LVGL它的“画布”应该通过TFT_eSPI的API来填充像素。这通常通过实现一个lv_disp_drv_t结构体并注册一个flush_cb回调函数来完成。2. Lua与C/C的交互绑定Lua脚本本身不能直接调用LVGL的C函数。我们需要在C侧Arduino代码创建Lua虚拟机并将特定的C函数注册为Lua的全局函数或模块。例如我们创建一个C函数lv_label_set_text_wrapper在其中调用真正的lv_label_set_text。然后将这个包装函数注册到Lua中命名为label_set_text。这样在Lua脚本里写label_set_text(myLabel, “Hello”)就能实际改变LVGL标签的文本。这是实现“动态运行Lua脚本”的核心。3. 文件系统与脚本热加载为了能让用户方便地更换脚本我们需要将Lua脚本文件存储在ESP32-S3的SPIFFSSPI Flash File System中。主程序在启动时从指定路径如/spiffs/script.lua读取脚本文件内容并交给Lua解释器执行。用户只需通过USB或OTA更新这个脚本文件即可改变设备行为无需重新烧录整个固件。4. 完整实战从驱动屏幕到运行Lua脚本接下来我们一步步实现一个基础版本显示一个“Hello World”标签并通过Lua脚本改变其文本。4.1 步骤一配置并测试屏幕驱动首先我们需要让屏幕亮起来。这需要配置TFT_eSPI库。定位库配置文件在PlatformIO的项目目录下找到.pio/libdeps/你的环境名/TFT_eSPI文件夹。复制其中的User_Setup.h文件到项目根目录或lib目录下自定义位置并重命名为User_Setup_Select.h。这样做可以避免直接修改库文件便于管理。编辑配置文件打开User_Setup_Select.h启用你的屏幕驱动型号并定义引脚。以下是一个针对ST7789 320x240屏幕的示例配置// User_Setup_Select.h #define ST7789_DRIVER // 指定驱动芯片 #define TFT_WIDTH 240 // 实际显示宽度 #define TFT_HEIGHT 320 // 实际显示高度 // 定义SPI引脚 (根据你的实际接线修改) #define TFT_CS 5 // 芯片选择 #define TFT_DC 2 // 数据/命令选择 #define TFT_RST 4 // 复位引脚 (可接至ESP32的EN引脚或单独控制) #define TFT_MOSI 7 // SPI数据输出 (ESP32-S3的GPIO7) #define TFT_SCLK 6 // SPI时钟 (ESP32-S3的GPIO6) #define TFT_MISO -1 // 未使用设为-1 #define LOAD_GLCD // 启用默认字体 #define SPI_FREQUENCY 40000000 // SPI时钟频率可尝试提高以获得更快刷新注意ESP32-S3的默认SPI引脚可能与ESP32不同请查阅开发板引脚图确认。编写基础测试代码在src/main.cpp中先编写一个简单的屏幕测试程序。// src/main.cpp #include Arduino.h #include TFT_eSPI.h TFT_eSPI tft TFT_eSPI(); void setup() { Serial.begin(115200); tft.init(); tft.setRotation(1); // 根据屏幕方向调整0-3 tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); tft.setCursor(20, 50); tft.println(Hello, Holocubic S3!); } void loop() { // 简单颜色交替闪烁证明程序在运行 static bool toggle false; tft.fillRect(100, 150, 40, 40, toggle ? TFT_RED : TFT_BLUE); toggle !toggle; delay(500); }连接硬件并烧录按照引脚定义连接好屏幕与ESP32-S3。在VS Code中点击PlatformIO工具栏的“Upload”按钮。如果一切顺利你将看到屏幕显示文字和一个闪烁的方块。4.2 步骤二集成LVGL并显示基础界面屏幕驱动正常后接下来集成LVGL。修改主程序更新src/main.cpp初始化LVGL并将其与TFT_eSPI连接。// src/main.cpp #include Arduino.h #include TFT_eSPI.h #include lvgl.h TFT_eSPI tft TFT_eSPI(); static lv_disp_draw_buf_t draw_buf; // 定义显示缓冲区 static lv_color_t buf[TFT_WIDTH * 10]; // 声明一个缓冲区 // LVGL的“刷屏”回调函数由LVGL在需要更新屏幕时调用 void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w (area-x2 - area-x1 1); uint32_t h (area-y2 - area-y1 1); tft.startWrite(); tft.setAddrWindow(area-x1, area-y1, w, h); tft.pushColors((uint16_t *)color_p-full, w * h, true); tft.endWrite(); lv_disp_flush_ready(disp); // 通知LVGL刷新完成 } void setup() { Serial.begin(115200); // 1. 初始化屏幕 tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); // 2. 初始化LVGL lv_init(); // 3. 初始化显示缓冲区 lv_disp_draw_buf_init(draw_buf, buf, NULL, TFT_WIDTH * 10); // 4. 注册显示驱动 static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res TFT_WIDTH; disp_drv.ver_res TFT_HEIGHT; disp_drv.flush_cb my_disp_flush; disp_drv.draw_buf draw_buf; lv_disp_drv_register(disp_drv); // 5. 创建一个简单的LVGL界面 lv_obj_t *label lv_label_create(lv_scr_act()); // 在默认屏幕上创建标签 lv_label_set_text(label, LVGL is ready!); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); // 居中显示 Serial.println(LVGL and TFT initialized.); } void loop() { lv_timer_handler(); // 必须定期调用LVGL任务处理器 delay(5); }烧录并运行再次上传程序。此时屏幕应由LVGL控制显示居中的“LVGL is ready!”文本。loop()中不断调用的lv_timer_handler()是LVGL运行的心脏。4.3 步骤三集成Lua解释器并建立绑定这是最核心的一步我们将eLua集成进来并创建几个关键的LVGL API绑定。包含头文件与全局变量修改src/main.cpp。// src/main.cpp (续接之前LVGL部分) #include “lua.hpp” // eLua库的主头文件 lua_State *L; // Lua虚拟机状态机 // 将要暴露给Lua的C函数创建一个标签 static int lv_create_label(lua_State *L) { const char *text luaL_checkstring(L, 1); // 获取Lua传来的第一个参数文本 int x luaL_checkinteger(L, 2); // 第二个参数X坐标 int y luaL_checkinteger(L, 3); // 第三个参数Y坐标 lv_obj_t *label lv_label_create(lv_scr_act()); lv_label_set_text(label, text); lv_obj_set_pos(label, x, y); // 将LVGL对象的指针转换为整数返回给Lua以便后续操作 lua_pushinteger(L, (intptr_t)label); return 1; // 返回值数量为1 } // 将要暴露给Lua的C函数设置标签文本 static int lv_set_label_text(lua_State *L) { lv_obj_t *label (lv_obj_t *)(intptr_t)luaL_checkinteger(L, 1); // 第一个参数标签对象指针 const char *text luaL_checkstring(L, 2); // 第二个参数新文本 lv_label_set_text(label, text); return 0; // 没有返回值 } // 注册所有C函数到Lua全局表 void register_lvgl_bindings(lua_State *L) { lua_register(L, “create_label”, lv_create_label); lua_register(L, “set_label_text”, lv_set_label_text); // 后续可以继续注册lv_obj_set_style, lv_btn_create等更多函数 }在setup()中初始化Lua并加载脚本void setup() { // ... (之前的LVGL初始化代码不变) ... Serial.println(“LVGL and TFT initialized.”); // 6. 初始化Lua虚拟机 L luaL_newstate(); luaL_openlibs(L); // 打开Lua标准库 // 7. 注册我们的绑定函数 register_lvgl_bindings(L); // 8. 加载并执行Lua脚本文件 // 首先需要初始化SPIFFS文件系统假设脚本文件已上传 #include SPIFFS.h SPIFFS.begin(true); File luaFile SPIFFS.open(“/script.lua”, “r”); if (!luaFile) { Serial.println(“Failed to open Lua script!”); // 创建一个默认的标签作为fallback lv_obj_t *label lv_label_create(lv_scr_act()); lv_label_set_text(label, “No script.lua found”); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } else { String scriptContent luaFile.readString(); luaFile.close(); // 执行Lua脚本字符串 int err luaL_dostring(L, scriptContent.c_str()); if (err) { Serial.printf(“Lua Error: %s\n”, lua_tostring(L, -1)); lv_obj_t *label lv_label_create(lv_scr_act()); lv_label_set_text(label, “Lua Script Error”); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); } else { Serial.println(“Lua script executed successfully.”); } } } void loop() { lv_timer_handler(); delay(5); }4.4 步骤四编写并上传Lua脚本现在我们需要编写一个Lua脚本并通过工具上传到ESP32-S3的SPIFFS文件系统中。编写Lua脚本 (script.lua)-- script.lua print(“[LUA] Hello from Lua script!”) -- 使用C绑定的函数创建一个标签 local myLabel create_label(“Dynamic Text”, 50, 80) -- 定义一个计数器 local counter 0 -- 定义一个在LVGL主循环中会被调用的函数需要C端配合轮询此处为简单演示 -- 更佳实践是在C端注册一个LVGL定时器回调函数内调用Lua函数。 function update_label() counter counter 1 set_label_text(myLabel, “Count: “ .. counter) end -- 注意在纯Lua端无法直接“挂接”到LVGL循环。 -- 实际项目中需要在C端创建一个lv_timer定期调用这个Lua函数。 -- 例如在C中lua_getglobal(L, “update_label”); lua_pcall(L, 0, 0, 0);这个脚本创建了一个标签并定义了一个更新函数。但如何周期调用update_label呢这引出了下一个关键点。实现C端定时调用Lua函数 我们需要修改C代码创建一个LVGL定时器在定时器回调中调用Lua的update_label函数。// 在全局定义定时器回调函数 static void lua_timer_cb(lv_timer_t *timer) { if (L) { lua_getglobal(L, “update_label”); // 获取Lua中的update_label函数 if (lua_isfunction(L, -1)) { int err lua_pcall(L, 0, 0, 0); // 调用函数0个参数0个返回值 if (err) { Serial.printf(“Lua timer error: %s\n”, lua_tostring(L, -1)); lua_pop(L, 1); // 弹出错误信息 } } else { lua_pop(L, 1); // 弹出的不是函数清理栈 } } } // 在setup()函数末尾Lua脚本执行成功后创建定时器 // 放在执行Lua脚本成功的分支里 if (!err) { Serial.println(“Lua script executed successfully.”); // 创建LVGL定时器每1000ms触发一次调用lua_timer_cb lv_timer_create(lua_timer_cb, 1000, NULL); }上传文件到SPIFFS PlatformIO提供了便捷的工具。首先在platformio.ini中启用上传工具; 在platformio.ini的[env]部分添加 board_build.filesystem littlefs ; 或 spiffs 根据框架选择然后在项目根目录创建data文件夹将script.lua放入其中。在VS Code的PlatformIO侧边栏找到“Project Tasks” - “esp32-s3-devkitc-1” - “Platform” - “Upload Filesystem Image”点击即可将data文件夹内容上传至开发板。4.5 步骤五编译、烧录与运行完整编译点击PlatformIO的“Build”按钮确保没有错误。烧录固件点击“Upload”。上传文件系统点击“Upload Filesystem Image”。观察结果设备重启后屏幕首先会显示“LVGL is ready!”随后Lua脚本被执行创建出显示“Count: 0”的标签并且该数字会每秒递增。至此一个具备3.2寸透明显示、运行LVGL、并能动态执行Lua脚本的“超级Holocubic”核心框架就搭建完成了。你可以通过修改script.lua文件轻松改变屏幕上显示的内容和行为实现真正的“热更新”。5. 常见问题与深度排查指南在实践过程中你几乎一定会遇到一些问题。以下是高频问题及其解决方案。问题现象可能原因排查步骤与解决方案编译错误lvgl.h未找到LVGL库未正确安装或platformio.ini配置错误。1. 检查platformio.ini中lib_deps是否正确包含lvgl/lvgl。2. 尝试运行pio lib update更新库索引。3. 删除.pio构建缓存文件夹重新编译。屏幕白屏或花屏1. 引脚定义错误。2. SPI频率过高。3. 屏幕初始化序列或复位时序不对。4. 电源功率不足。1.首要检查用万用表或逻辑分析仪确认CS,DC,MOSI,SCLK,RST引脚连接与代码定义一致。2. 降低SPI_FREQUENCY如改为20000000。3. 在tft.init()前手动控制RST引脚复位digitalWrite(TFT_RST, LOW); delay(50); digitalWrite(TFT_RST, HIGH); delay(150);4. 确保使用5V/2A以上电源单独为屏幕供电或检查开发板3.3V输出电流是否足够。LVGL刷新卡顿、撕裂1. 缓冲区太小。2.lv_timer_handler()调用间隔不稳定或太长。3. SPI时钟频率太低。4. 图形操作过于复杂。1. 增大buf数组大小例如lv_color_t buf[TFT_WIDTH * 40];。2. 确保loop()中delay(5)存在且稳定或使用lv_tick_inc(5)配合非阻塞延时。3. 在屏幕驱动允许范围内适当提高SPI_FREQUENCY。4. 优化Lua脚本避免每帧创建/删除大量对象使用样式复用。Lua脚本执行失败报语法错误1. Lua脚本文件格式错误如UTF-8 with BOM。2. 脚本中存在不支持的语法或API。1. 使用VS Code、Notepad等编辑器将脚本保存为纯UTF-8无BOM格式。2. 在PC上用标准Lua环境如Lua for Windows先测试脚本语法。3. 检查C绑定函数名是否与Lua中调用的一致大小写敏感。a fatal error occurred: this chip is esp32-s3, not esp32烧录工具未指定正确芯片型号。1.PlatformIO用户确保platformio.ini中board选择的是ESP32-S3系列如esp32-s3-devkitc-1。2.使用esptool.py手动烧录时将--chip参数改为esp32s3例如esptool.py --chip esp32s3 --port COMx write_flash ...。SPIFFS文件上传失败1. 文件系统类型不匹配。2. 开发板未进入上传模式。3. 端口被占用。1. 确认platformio.ini中board_build.filesystem设置与框架匹配Arduino常用littlefs或spiffs。2. 上传时可能需要手动让开发板进入“下载模式”按住Boot键再按一下Reset键。3. 关闭串口监视器和其他可能占用端口的软件。运行一段时间后死机或重启1. 内存泄漏Lua对象未回收LVGL对象未删除。2. 堆栈溢出。3. WiFi中断处理不当。1. 在Lua中对于不再需要的LVGL对象应在C端提供lv_obj_del的绑定并调用。2. 增加ESP32-S3的堆栈大小在platformio.ini中调整board_build.f_flash和board_build.partitions。3. 检查网络请求是否有超时机制避免阻塞主循环。6. 最佳实践与项目进阶方向当基础功能跑通后为了项目的稳定性和可玩性可以考虑以下优化和扩展。6.1 工程结构优化模块化C绑定不要将所有LVGL绑定函数都写在main.cpp里。创建独立的头文件如lvgl_bindings.h/.cpp和Lua绑定模块文件如lua_bindings.h/.cpp使代码更清晰。错误处理强化在C调用Lua函数时使用lua_pcall并提供错误处理函数防止Lua运行时错误导致整个系统崩溃。文件系统监控可以实现一个简单的文件系统监听当script.lua文件被修改通过OTA或USB后自动重新加载Lua脚本实现真正的“热重载”。6.2 Lua脚本生态构建提供标准API模块为Lua脚本编写者封装一套易用的API模块例如-- 在Lua脚本中理想的使用方式 local ui require(“holocubic_ui”) local label1 ui.create_label(“Time”, 10, 10) local btn1 ui.create_button(“Click Me”, 100, 100, function() label1:set_text(“Clicked!”) end) ui.start_timer(1000, function() -- 每秒更新 label1:set_text(os.date(“%H:%M:%S”)) end)这需要在C端将相关函数组织成Lua table并作为模块加载。支持多脚本与简单应用管理可以设计一个简单的脚本管理器让设备能够存储多个Lua脚本如clock.lua、weather.lua并通过物理按钮或Web界面进行切换。6.3 功能扩展网络功能集成在C端实现HTTP/HTTPS客户端并暴露给Lua。这样Lua脚本就能直接获取网络时间、天气API、股票信息等并动态显示在屏幕上。传感器支持接入温湿度传感器如DHT22、光线传感器等将读取数据的函数也绑定给Lua让脚本能根据环境变化做出反应。更丰富的UI控件逐步将LVGL的更多控件按钮、滑块、图表、列表、滚轮绑定到Lua释放强大的界面设计能力。离线中文字库LVGL支持自定义字库。可以将小尺寸的中文字库如16px编译成C数组或存储在SPIFFS并绑定相应的API让Lua脚本可以显示中文。6.4 性能与内存管理对象池对于频繁创建和销毁的UI元素考虑在C端实现对象池减少内存碎片。Lua内存限制可以使用lua_gc接口在Lua虚拟机中设置内存上限和触发垃圾回收的时机防止恶意或 bug 脚本耗尽内存。双缓冲与局部刷新确保LVGL使用双缓冲机制并利用lv_area_t进行局部刷新而非全屏刷新以提升效率。通过以上步骤你不仅成功将一个经典的Holocubic项目升级到了硬件和软件的新高度更重要的是构建了一个可动态编程的嵌入式图形交互平台。它的潜力不再局限于预定义的几种动画而是交给了无限的脚本创造力。你可以用它做一个网络时钟、一个智能家居状态面板、一个加密货币行情显示器或者只是一个纯粹的电子艺术品。