Hướng dẫn tạo ứng dụng trên thẻ nhớ SD cho Loa AI (Lua)
Cách tạo app/game Lua trên thẻ SD cho Loa AI EchoEar: cấu trúc thư mục ungdung, game.json, main.lua, API gfx/http/sd, layout màn tròn 360×360 và checklist đưa lên máy.
Loa AI (EchoEar E1 / VC16 / VC32) có thể chạy ứng dụng và game viết bằng Lua 5.4 ngay từ thẻ nhớ TF/SD. Firmware chỉ cần bật Lua SD; bạn copy thư mục app lên thẻ, mở Menu → Thẻ nhớ → tab Ứng dụng là chạy được — không cần biên dịch lại firmware mỗi lần đổi app.
Bài này hướng dẫn từ A–Z: chuẩn bị thẻ, cấu trúc thư mục, file game.json / main.lua, API sẵn có, layout màn tròn 1.85", xử lý ảnh/âm thanh, và checklist trước khi mang lên máy.
Mua Loa AI / xem sản phẩm: https://loaai.me · Flash firmware: https://loaai.me/flash

1. Cần gì trước khi bắt đầu?
- Máy Loa AI có khe SD: EchoEar E1 / VC16 / VC32 (firmware có
CONFIG_ENABLE_LUA_SD). - Thẻ TF/SD định dạng FAT32, bật long filename (tên file dài).
- Máy tính để soạn
main.lua, tạoicon.png, chuẩn hóa ảnh. - (Tuỳ app) WiFi nếu dùng
http,tts,audio.play, hoặcwebserver local.
Interpreter Lua nằm trong firmware; script, ảnh, WAV, JSON nằm trên thẻ. Lua không có io / os / socket tự do — chỉ dùng API firmware gắn sẵn.
2. Cấu trúc thư mục trên thẻ (quan trọng)
Chuẩn hiện tại (khuyến nghị):
/sdcard/ungdung/<thu_muc>/<id>/
main.lua ← bắt buộc (điểm vào)
game.json ← tên, version, icon trên launcher
icon.png ← 64×64 PNG/JPG
... ← txt, json, ảnh, wav, fonts/…
Ví dụ thực tế:
/sdcard/ungdung/tienich/calc/— máy tính/sdcard/ungdung/games/altp/— Ai là triệu phú/sdcard/ungdung/hoctap/flashcard/— flashcard + TTS
Legacy vẫn chạy: /sdcard/games/<id>/ (một cấp, không có thư mục thể loại).
<thu_muc> và <id> chỉ gồm chữ, số, _, -, . (tối đa ~32 ký tự). Không dùng ...
Firmware quét tối đa khoảng 16 app có main.lua (hoặc main.luac). Không bắt buộc có JSON — thiếu thì dùng tên folder + badge chữ cái.

3. File game.json (manifest launcher)
{
"id": "calc",
"name": "Máy tính",
"version": "1.1.0",
"icon": "icon.png"
}
| Trường | Bắt buộc? | Ghi chú |
|---|---|---|
id |
nên có | trùng tên folder; dùng so sánh/update sau này |
name |
nên có | hiện trên lưới Ứng dụng |
version |
nên có | semver ngắn, tối đa ~15 ký tự; launcher hiện Tên · v1.1.0 |
icon |
không | mặc định tìm icon.png / .jpg / .jpeg / .bmp |
Khi chạy app, firmware gắn sẵn các biến toàn cục:
GAME_DIR— đường dẫn tuyệt đối thư mục app trên SDGAME_ID,GAME_NAME,GAME_VERSION— từ manifest (thiếu version →"0.0.0")
4. Mở app trên Loa AI
- Copy cả folder app vào thẻ (đúng cấu trúc ở mục 2).
- Gắn thẻ vào máy, bật nguồn; bật WiFi nếu app cần mạng.
- Vào Menu → Thẻ nhớ.
- Tab Ứng dụng → chạm icon app.
- Thoát: nút X giữa mép trên, hoặc giữ tay ~3 giây (thanh “GIỮ 3S”).
- Tab Tập tin: xem ảnh / AVI / MP3 như trình duyệt file cũ.
5. Skeleton main.lua tối thiểu (màn tròn)
Màn EchoEar là LCD tròn Ø 1.85", nhưng canvas firmware là 360×360 vuông — bốn góc bị cắt. Nội dung chính nên nằm trong BOX 240×240 giữa màn (hoặc bán kính SAFE ≈ R−10).
-- Thoát: giữ 3 giây, hoặc nút X (firmware vẽ).
if not gfx or not gfx.open or not gfx.open() then
print("Không mở được gfx")
return
end
local W, H = gfx.width, gfx.height
local CX, CY = math.floor(W / 2), math.floor(H / 2)
local R = math.floor(math.min(W, H) / 2)
local SAFE = math.max(80, R - 10)
local BOX = 240
local BX = CX - math.floor(BOX / 2) -- 60 trên 360
local BY = CY - math.floor(BOX / 2)
local HUD_Y = math.max(12, CY - R + 16)
local function text_c(s, y, col, sc)
sc = sc or 2
local w = (gfx.measure and gfx.measure(s, sc)) or (#tostring(s) * 6 * sc)
gfx.text(tostring(s), math.floor((W - w) / 2), y, col, sc)
end
local function draw()
gfx.clear(0x071428)
gfx.circle(CX, CY, R - 2, 0xC9A227) -- phủ CẢ đĩa
gfx.circle(CX, CY, R - 9, 0x0B1B3A)
text_c("UNG DUNG MAU", HUD_Y, 0xE8C547, 2)
gfx.rect(BX, BY, BOX, BOX, 0x123056)
text_c("BOX 240", CY - 4, 0xF8FAFC, 2)
end
while gfx.present() do
if gfx.tapped() then
local x, y = gfx.pos()
-- xử lý chạm trong BOX / nút…
end
draw()
end
gfx.close()

Số chuẩn layout màn tròn
| Hạng mục | Giá trị | Ghi chú |
|---|---|---|
| Canvas | 360×360 | gfx.width / gfx.height |
| Tâm / bán kính | 180 / R=180 | chỉ đĩa nhìn thấy |
| SAFE | R−10 ≈ 170 | đừng dùng R×0.78 (~140) — trống lãng phí |
| BOX chơi | 240 | chia hết 2/3/4; tránh 252 (góc bị cắt) |
| Nút X thoát | ≈ (180, 16) | chạm trong BOX 240 không thoát |
| Thanh GIỮ 3S | y ≈ 273 | nút đáy đừng che hết |
| Icon launcher | 64×64 | icon.png |
| Ảnh trong app | ~320×320 | qua tool resize trước khi copy |
Công thức chord (nửa bề rộng còn nhìn thấy tại hàng y) — dùng để giới hạn rộng nút:
local function chord(y, pad)
local dy = y - CY
local lim = SAFE - (pad or 0)
local m = lim * lim - dy * dy
if m <= 0 then return 0 end
return math.sqrt(m)
end
-- max_w ≈ 2 * chord(btn_y + btn_h/2, 8)
6. API Lua có sẵn (tóm tắt dùng thật)
6.1 gfx — vẽ + chạm
gfx.open()/gfx.close()/while gfx.present() do … end(~30 fps)gfx.clear(0xRRGGBB),rect,circle,text,measuregfx.image("icon.png", x, y),gfx.sprite(…),gfx.size(path)gfx.tapped()/gfx.touching()/gfx.pos()/gfx.ms()/gfx.quit()gfx.exit_pos(cx, cy, hit),gfx.show_exit(false)- Font UTF-8 tiếng Việt tự chọn; IPA / ký tự đặc biệt:
gfx.load_font("fonts/….bin")
Cache ảnh ~8 file LRU; ảnh tối đa ~360×360 sau scale; file đọc tối đa ~2 MB.
6.2 sd — đọc/ghi thẻ (chroot thư mục app)
if sd.exists("todo.json") then
local txt = sd.read_file("todo.json") -- ≤ 64 KB
end
sd.mkdir("assets")
sd.write_file("todo.json", json.encode(obj))
local names = sd.list("pics") -- tối đa 64 tên
Path tương đối thư mục app (hoặc dưới /sdcard). Không dùng ... File lớn nên http.download, không sd.write_file.
6.3 http + json
local body, status, err = http.get("https://example.com/api.json")
local obj = json.decode(body)
local s = json.encode({ a = 1, tags = { "game" } })
-- File nhị phân / lớn: stream thẳng ra thẻ (≤ 8 MB)
local bytes, err = http.download("https://cdn.example.com/icon.png", "icon.png")
http.get/post: timeout 20s, body nhận ≤ 48 KB; POST gửi ≤ 32 KBhttp.download: timeout 60s, ≤ 8 MBhttp.snip: cắt đoạn nhỏ từ HTML lớn (không nhét cả trang vào Lua)
6.4 Âm thanh
sfx.play("win.wav")/sfx.loop("bgm.wav")/sfx.stop()— WAV PCM ngắn ≤ ~512 KBaudio.play(url, "Tiêu đề")— MP3/stream URL (cần WiFi)tts.speak("hello", "en")/tts.speak("xin chào", "vi")— cache ~2 câu gần nhất
6.5 web — HTTP server trên IP WiFi nhà
Điện thoại cùng mạng mở http://<ip-máy>/. File tĩnh trong thư mục www/; tối đa 8 route Lua. Cần vòng gfx.present() để poll request. Demo: quán nước, weblocal.

7. Chuẩn hóa ảnh trước khi copy lên thẻ
Phone JPEG/HEIC/PNG 16-bit dễ lỗi trên ESP32. Nên chạy tool trong repo firmware:
python3 scripts/lua_img_resize.py anh.jpg -o pics/ --format auto
# → khoảng 320×320 PNG nhỏ hoặc JPEG baseline
- Không copy HEIC / AVIF / JPEG progressive / PNG khổ lớn gốc.
- Ảnh thật:
gfx.sprite(..., false)(tắt chroma trắng). - Icon launcher: 64×64.
8. Ví dụ nhanh: app HTTP lưu JSON lên thẻ
local body, status, err = http.get("https://jsonplaceholder.typicode.com/todos/1")
if not body then
print("HTTP lỗi", err)
return
end
local todo, jerr = json.decode(body)
if not todo then
print("JSON lỗi", jerr)
return
end
sd.write_file("todo.json", json.encode(todo))
print("title =", todo.title)
App có UI đầy đủ tham khảo máy tính tienich/calc/, HTTP demo congcu/net/, giá vàng tienich/vang/, thời tiết tienich/weather/.
9. Checklist trước khi đưa lên thẻ
Màn tròn
- Phủ cả đĩa bằng
gfx.circle(CX, CY, R, …)— không “lỗ” trống giữa kính - Playfield / form dùng BOX 240 (không 252)
- Nút đáy không che hết vùng y≈273 (GIỮ 3S)
- Hit-test dùng cùng hằng số với lúc vẽ
Asset & manifest
main.lua+game.json(name,version) +icon.png64×64- Ảnh đã qua resize; WAV PCM ngắn nếu có tiếng
- Folder đúng
/sdcard/ungdung/<mục>/<id>/
Hiệu năng
- Lua heap ~128 KB — đừng
http.getfile lớn - Mỗi frame: vẽ lại scene gọn; tránh decode JSON / đọc SD liên tục trong loop
10. Việc thường làm sai
| Sai | Đúng |
|---|---|
| Đặt nút góc canvas 360×360 | Trong BOX 240 / theo chord |
SAFE = R * 0.78 |
SAFE = R - 10 |
| Playfield 252 “to hơn” | 240 |
| Copy JPEG phone / HEIC lên SD | lua_img_resize.py trước |
http.get file lớn |
http.download |
| Hardcode hit-box lệch layout mới | Một bộ hằng số dùng chung draw + touch |
11. Gợi ý thể loại app
- Game cảm ứng: playfield trong BOX/SAFE; HUD overlay lưỡi liềm trên.
- Quiz / nhiều nút chữ: pill 2×2 nửa dưới đĩa (xem altp, toanchu).
- Tiện ích form: keypad trong BOX 240; hàng đáy trước y≈265.
- App mạng: màn “Đang tải…” trước
http.get(blocking); chữ lỗi ngắn, căn giữa. - Vẽ kéo: lưu nét rồi vẽ lại mỗi frame (paint).
Kết
Tạo ứng dụng cho Loa AI trên thẻ nhớ = viết main.lua + game.json + asset, copy vào /sdcard/ungdung/…, mở từ tab Ứng dụng. Nhớ thiết kế cho đĩa tròn R=180, nội dung chính trong BOX 240, và dùng API gfx / sd / http / sfx / tts của firmware.
Có Loa AI EchoEar (có SD) là đủ để tự làm máy tính, quiz, flashcard, game cảm ứng… ngay trên máy.
Mua / xem Loa AI: https://loaai.me
Flash firmware: https://loaai.me/flash