ESP32-035 · ST7796 320×480 · LVGL 9

TanPlanet Live
จอตั้งโต๊ะ ESP32 ที่รู้ว่าวันนี้ต้องสนใจอะไร

จอ 3.5" ราคา ฿640 บนโต๊ะทำงาน ที่ดึงปฏิทินไทย จันทรคติ อากาศ ดวงลงทุน พอร์ตหุ้น โควตา AI และสถานะ Claude Code มาเรียงเป็นการ์ดแตะดูได้ — โดยให้ ESP32 เป็นแค่ "จอ" ส่วนสมองอยู่บน Mac

ESP32 LVGL 9 TFT_eSPI XPT2046 Touch Node Backend launchd mDNS ฟอนต์ไทยเจนเอง
Empathize · Define

ปัญหาที่เจอ

ข้อมูลที่ต้องดูทุกเช้ามีไม่กี่อย่าง แต่มันกระจายอยู่คนละที่ และไม่มีอันไหน "อยู่ตรงหน้า" ตลอดเวลา

ทุกเช้าต้องเปิด 6 อย่าง — ปฏิทินดูวันพระ, แอปอากาศ, พอร์ตหุ้นใน Dime, การ์ดโค้ชจาก LINE, เช็คโควตา Claude เหลือเท่าไร, แล้วดูว่า agent ที่สั่งไว้ทำอะไรอยู่ ทั้งหมดนี้คือข้อมูล "ชำเลืองดู" ไม่ใช่ข้อมูลที่ต้องเปิดแอปมานั่งอ่าน — แต่บนคอมมันโดนหน้าต่างอื่นทับตลอด

— โจทย์ตั้งต้น ก่อนสั่งบอร์ด ESP32

ข้อมูลกระจาย 6 แอป แต่ละอันดูแค่ 3 วินาที

อากาศ / วันพระ / ราคาหุ้นที่เฝ้า / โควตา AI — ข้อมูลที่ใช้เวลาดูรวมกันไม่ถึงนาที แต่ต้องปลดล็อกมือถือ เปิดแอป สลับแท็บ ทุกวัน

Dashboard บนจอคอม = ไม่มีอยู่จริง

เปิด dashboard ไว้อีกแท็บ สุดท้ายก็โดนหน้าต่างงานทับ ข้อมูลที่ต้อง "ชำเลืองเห็น" ต้องมีพื้นที่ของตัวเองที่ไม่แย่งกับงาน

โมดูลจอสำเร็จรูปพูดไทยไม่ได้

เฟิร์มแวร์สำเร็จรูปของจอ ESP32 แทบทั้งหมดมีแต่ฟอนต์ละติน — ภาษาไทยออกมาเป็นกล่องสี่เหลี่ยม และวรรณยุกต์ซ้อนผิดตำแหน่ง

ESP32 คิดเองไม่ไหว

คำนวณดวงสุริยยาตร์ เรียก AI สรุป อ่านโควตา OAuth — งานพวกนี้กิน RAM และต้องใช้ secret ซึ่งไม่ควรฝังในบอร์ดที่วางอยู่บนโต๊ะ

เปลี่ยนวง Wi-Fi ทีต้อง flash ใหม่ที

เฟิร์มแวร์ที่ hardcode IP ของ backend ไว้ พอ Mac ย้าย IP หรือเปลี่ยนเราเตอร์ จอก็ตายเงียบ ต้องต่อสาย USB มา flash ใหม่ทุกครั้ง


Ideate

วิธีแก้ปัญหา

แยกให้ชัดว่าอะไรคือ "จอ" อะไรคือ "สมอง" แล้วให้ทั้งสองคุยกันด้วยสัญญาข้อมูลเดียว

ESP32 เป็น thin client ไม่ใช่สมอง

Backend บน Mac รวมข้อมูลทุกแหล่ง คำนวณให้เสร็จ แล้วส่ง JSON การ์ดสั้น ๆ ให้จอวาด — เฟิร์มแวร์ไม่ต้องรู้จัก API ไหนเลย และไม่มี key อยู่ในบอร์ด

ทุกอย่างคือ "การ์ด" หน้าตาเดียวกัน

ไม่ว่าจะอากาศ หุ้น ดวง หรือโควตา AI ทุกใบใช้สัญญาเดียวกัน id / type / title / value / detail / tone / priority — เพิ่มการ์ดใหม่ = เพิ่ม 1 ฟังก์ชันใน backend ไม่ต้องแตะจอ

ตั้งค่าจากมือถือ ไม่ต้อง flash

หน้า /manage จัดลำดับการ์ด ซ่อน/แสดง เลือกหุ้นที่เฝ้า ตั้งเวลาโพโมโดโร ความสว่างกลางวัน-กลางคืน — จอดึง config รอบถัดไปแล้วเปลี่ยนตามทันที

ฟอนต์ไทยเจนเอง พร้อมวรรณยุกต์ยกสูง

สร้าง bitmap font จาก Anuphan 5 ขนาดเข้าเฟิร์มแวร์ แล้วทำ glyph วรรณยุกต์ตำแหน่งสูงเก็บใน Private Use Area เอง เพราะ LVGL ไม่อ่านตาราง GPOS ของ OpenType

หา backend ด้วยชื่อ ไม่ใช่ IP

จอ resolve Workspace-3.local ผ่าน mDNS แล้วแคช IP ไว้ — ย้ายเราเตอร์ เปลี่ยนวง Wi-Fi หรือ Mac ได้ IP ใหม่ ก็ยังหากันเจอโดยไม่ต้องต่อสาย


Prototype

วิธีทำงาน — 4 จังหวะ

จากแหล่งข้อมูลดิบ ถึงการ์ดที่แตะดูได้บนจอ ใช้เวลารอบละ 60 วินาที

1

รวมของ

Backend ดึงอากาศ ปฏิทิน .ics ราคาหุ้น ดวง โควตา Claude และหางไฟล์ session ของ agent

2

ย่อยเป็นการ์ด

คำนวณให้จบบน Mac แล้วเหลือแต่ข้อความสั้น + ตัวเลขที่จอเอาไปวาดได้ทันที

3

จอดึงแล้ววาด

ESP32 ยิง HTTP ทุก 60 วิ (การ์ด Claude แยกท่อ 10 วิ) แล้ว render ด้วย LVGL

4

แตะเพื่อเจาะ

แตะการ์ด → หน้า detail กราฟราคา กราฟฝนรายชั่วโมง ปฏิทินเดือน หรือ price ladder


Prototype

Tech Stack & Architecture

ไม่มีคลาวด์ ไม่มีบัญชีผู้ใช้ ไม่มี key ในบอร์ด — ทุกอย่างจบใน LAN บ้าน

แหล่งข้อมูล
Open-Meteo MegaCoach JSON Thai Astrology API Claude usage API ~/.claude/projects/*.jsonl ปฏิทิน .ics
↕
Backend Node บน Mac (launchd) — พอร์ต 8787
/api/device-summary /api/claude /manage saju.mjs thai-lunar.mjs
↕
Firmware (Arduino + LVGL 9)
mDNS resolve HTTP poll ฟอนต์ไทย 5 ขนาด touch + scroll auto-dim
↕
ESP32-035 — ฿640
ST7796 320×480 XPT2046 resistive LDR RGB LED audio amp Flash 4MB
ทำไมเลือก stack นี้?

ESP32-035 มีจอ ทัช LDR RGB และ audio amp มาในบอร์ดเดียว ฿640 — ไม่ต้องต่อ sensor เพิ่มเลยสักตัว · LVGL 9 ให้ scroll/touch/animation ระดับใช้งานจริงบน MCU · Backend เป็น Node ล้วน ไม่มี dependency รันด้วย launchd สตาร์ตเองตอนเปิดเครื่อง · วางไว้ใน LAN ทำให้เข้าถึงไฟล์ในเครื่อง (session ของ agent, ข้อมูลพอร์ต) ได้โดยไม่ต้องอัปอะไรขึ้นคลาวด์


Prototype

สิ่งที่สร้าง — การ์ด 12 ใบ

ทุกใบเป็นข้อมูลจริง ไม่มี mock เหลืออยู่บนจอ

นาฬิกา + วันที่ไทย

NTP sync แล้ววาดด้วยฟอนต์ตัวเลข 48px แยกต่างหาก + วันที่ พ.ศ. เต็มรูปแบบบนแถบหัวจอ

ปฏิทินรายเดือน

เลื่อนดูเดือนอื่นได้ · จุดน้ำเงินใต้วันพระ · เลขแดงวันหยุดราชการ · ดึงปฏิทินภายนอกผ่าน .ics ได้

จันทรคติ / วันพระ

คำนวณข้างขึ้นข้างแรมด้วยสูตรสุริยยาตร์ในฝั่ง backend เอง ไม่พึ่ง API ภายนอก

Weather แบบ Google Weather

Open-Meteo + กราฟโอกาสฝนรายชั่วโมงที่วาดเอง + ไอคอนสไตล์ Google ฝังเป็น C array RGB565A8

ดวงลงทุนวันนี้

ต่อกับ Thai Astrology engine ของโปรเจกต์ MegaCoach — ให้คะแนน 0-100 พร้อมคำอธิบายสั้น

พลังวันนี้ (สาจู)

เขียน engine เองใน saju.mjs ตรวจตรงกับแอปอ้างอิงทุกช่วงยาม — กราฟพลังรายยามเป็นเส้นต่อจุด

Market Focus

ราคาหุ้นที่เฝ้าพร้อมกราฟ 1 เดือน กด ◄ ► เลื่อนดูได้ทุกตัว เลือกรายชื่อจากหน้า manage

Monthly Plan & Price Ladder

แสดงโซนเข้า / stop / target เทียบกับราคาปัจจุบันเป็นบันได เห็นทันทีว่าอยู่ตรงไหนของแผน

North Star

เป้าหมายระยะยาวและหลักการคุมความเสี่ยง Fear of Ruin > FOMO ที่ต้องเห็นทุกวัน

AI Status

โควตา Claude รอบ 5 ชม. และ 7 วัน เป็นแถบเส้น + ขีด pace — สีเปลี่ยนเมื่อ % ใช้ไปแซงเวลาที่ผ่านไป

Claude Code + มาสคอต

อ่านหางไฟล์ session ว่า agent กำลังใช้เครื่องมืออะไร แล้วให้มาสคอตทำท่าตามนั้น รีเฟรชทุก 10 วินาที

โพโมโดโร + เตือนกิจวัตร

จับเวลาทำงาน/พัก และเตือนกินน้ำ ข้าวเที่ยง เลิกงาน — แตะตอนกระพริบเพื่อปิดจบในจังหวะเดียว


Prototype

หน้าตาบนจอ

320×480 แนวตั้ง — แถบหัวคงที่ ด้านล่างเป็นการ์ดที่เลื่อนและแตะได้

09:42
อังคาร 5 สิงหาคม 2569
31°
เมฆเป็นส่วนมาก
ขึ้น 12 ค่ำ เดือน 9
🤖
CLAUDE CODE
กำลังแก้ไฟล์
live-arduino · app_main.cpp
📊
AI STATUS
รอบ 5 ชม. 62%
📈
MARKET FOCUS
NVDA $206.69 ▲ 1.2%
1 เดือน · ต่ำสุด $188 · สูงสุด $212
☯️
พลังวันนี้
ธาตุไฟ · 71/100
ยามแรง 09:00-11:00
🍅
โพโมโดโร
18:24
ช่วงทำงาน · เตือนกินน้ำอีก 12 นาที

แถบหัวไม่เปลี่ยนตำแหน่งเลยตลอดวัน เพื่อให้ "ชำเลืองเห็น" เวลาและอากาศได้โดยไม่ต้องโฟกัส ส่วนการ์ดด้านล่างเรียงตาม priority ที่ตั้งจากมือถือได้


Test & Iterate

Hack กับปัญหา

7 โจทย์ที่เจอตอนของจริงอยู่บนโต๊ะ — ส่วนใหญ่ไม่มีในเอกสาร ต้องไล่เอง

กดแล้วค้าง ทั้งที่ heap ว่าง 165KB

แตะการ์ดแล้วจอค้างเป็นระยะ ตอนแรกนึกว่า RAM หมด แต่ ESP รายงาน heap ว่างตั้ง 165KB

แก้: LVGL ไม่ได้ใช้ heap ของระบบ มันใช้ pool ของตัวเองที่ LV_MEM_SIZE = 32KB และเต็มไปแล้ว 89% → เปลี่ยนไปใช้ LV_STDLIB_CLIB ให้ LVGL ขอ memory จาก heap จริงแทน — บทเรียน: บน MCU ต้องถามว่า "หน่วยความจำก้อนไหนเต็ม" ไม่ใช่แค่ "ว่างเท่าไร"

บอร์ดรีบูตตอนกดเข้าหน้า detail

เปิดหน้า detail ของการ์ดมาสคอตทีไรบอร์ดรีบูตทันที ทั้งที่ heap เหลือเฟือ 163KB

แก้: struct ของรูปมาสคอตขนาด 1.7KB ถูกประกาศเป็นตัวแปร local ใน draw callback ซึ่งอยู่บน stack ของ task ไม่ใช่ heap → ย้ายไปเป็น static — บทเรียน: stack overflow บน ESP32 หน้าตาเหมือน "รีบูตเฉย ๆ" ไม่มี error ให้อ่าน

วรรณยุกต์ไทยซ้อนผิดตำแหน่ง

คำที่มีสระบนซ้อนวรรณยุกต์ เช่น "ที่" ออกมาชนกันเป็นก้อน เพราะ LVGL ไม่อ่านตาราง GPOS ของ OpenType

แก้: เขียน make_thai_font.py เจน bitmap font 5 ขนาดจาก Anuphan เอง แล้วสร้าง glyph วรรณยุกต์ตำแหน่งยกสูงเก็บใน Private Use Area ตอน render ก็ map อักขระเป็นตัว PUA เมื่อเจอบริบทที่ต้องยก — ได้ตำแหน่งถูกโดยไม่ต้องมี shaping engine

กล่องสี่เหลี่ยมบนจอ ไม่ใช่ loading

ตัวเลขนาฬิกาบางสถานะขึ้นเป็นกล่องสี่เหลี่ยม — ไล่หา race condition ของการโหลดข้อมูลอยู่นาน

แก้: มันคือ glyph ที่ไม่มีในฟอนต์ ฟอนต์นาฬิกา 48px มีแค่ 0-9 : ° พอส่ง --:-- เข้าไป ขีดจึงกลายเป็นกล่อง → เปลี่ยนเป็นส่งสตริงว่างตอนยังไม่มีเวลา — บทเรียน: กล่องสี่เหลี่ยม = ฟอนต์ ไม่ใช่ state

จอไม่มีข้อมูล แต่ backend ไม่ได้ล่ม

จอขึ้น error ทั้งที่ curl จาก Mac ได้ 200 ปกติ — เสียเวลาไล่หาสาเหตุที่ฝั่ง backend อยู่นาน

แก้: เฟิร์มแวร์ไม่เคยเรียก MDNS.begin() เลย lwip จึง resolve ชื่อ .local ไม่ได้ → ใช้ MDNS.queryHost() เอา IP มาเสียบแล้วแคชไว้ — หลุมที่ทำให้หลงทาง: curl ชื่อ .local จาก Mac เองมันวิ่งเข้า 127.0.0.1 ต้อง curl ด้วย LAN IP ถึงจะรู้ความจริง

ชื่อสภาพอากาศยาวเกินไปทับนาฬิกา

ใส่ LV_LABEL_LONG_DOT แล้วยังทับอยู่ เพราะ LVGL จะ wrap ขึ้นบรรทัดสองก่อน แล้วค่อยใส่จุดไข่ปลา

แก้: เขียน setThaiTextFit() วัดความกว้างจริงด้วย lv_text_get_size แล้วถอยข้อความทีละตัวอักษร UTF-8 จนพอดีกรอบ — ห้ามถอยทีละไบต์ เพราะจะตัดกลางอักขระไทยแล้วได้ขยะ

การ์ด Claude ช้าไป 5 นาที

การ์ดที่บอกว่า agent กำลังทำอะไร ต้องสด แต่มันรออัปเดตพร้อมรอบใหญ่ 60 วินาที/5 นาที ทำให้ข้อมูลตกยุคตลอด

แก้: แยกท่อ /api/claude ที่ส่งเฉพาะการ์ดใบเดียว (~300 bytes) ทุก 10 วิ แล้วเขียนทับ label เดิมในที่ โดยจำ pointer ไว้ ไม่ lv_obj_clean ทั้งจอ — ถ้าล้างทั้งจอ scroll จะเด้งกลับหัวทุก 10 วินาที

Define

Design Decision ที่ตั้งใจเลือก

ขอบเขตที่จงใจ "ไม่ทำ" สำคัญพอ ๆ กับสิ่งที่ทำ

จอนี้ทำหน้าที่เดียว: แสดงผล — มันไม่คำนวณดวงเอง ไม่เรียก AI เอง ไม่เก็บ key ใด ๆ และไม่ต่อออกอินเทอร์เน็ตนอกจากไปหา backend ในบ้าน พอขอบเขตคมขนาดนี้ เฟิร์มแวร์ก็เหลือแค่ "ดึง JSON แล้ววาด" ซึ่ง debug ง่ายและพังยาก

🖥️
จอ ไม่ใช่สมอง

logic หนักอยู่บน Mac จอแค่ดึง JSON มาวาด

🔑
ไม่มี key ในเฟิร์มแวร์

บอร์ดวางอยู่บนโต๊ะ ใครหยิบไปก็ไม่ได้อะไรติดมือ

🏠
อยู่ใน LAN เท่านั้น

ข้อมูลพอร์ตและ session ไม่ต้องขึ้นคลาวด์

📱
config ไม่ต้อง flash

ปรับทุกอย่างจากมือถือ สาย USB เก็บเข้าลิ้นชักได้


Level Up · Reflect

Knowledge Domain ที่แข็งขึ้น

5 พื้นที่ความรู้ที่ระดับขยับขึ้นจริงจากการทำโปรเจกต์นี้

1. Embedded UI ด้วย LVGL 9 — จาก "วาดข้อความ" เป็น "ออกแบบระบบการ์ด":

เข้าใจ object tree, style selector, scroll container, draw callback และวิธีอัปเดต widget ในที่โดยไม่สร้างใหม่ทั้งจอ — ซึ่งเป็นความต่างระหว่าง UI ที่ลื่นกับ UI ที่เด้ง บทเรียน: บน MCU การ "ไม่วาดใหม่" สำคัญกว่าการวาดให้สวย

2. ฟอนต์ไทยบนระบบฝังตัว — ลงลึกถึงระดับ glyph:

รู้ว่า rendering ภาษาไทยต้องมี shaping (GPOS) ที่ LVGL ไม่มีให้ จึงต้องแก้ที่ชั้นฟอนต์เอง ด้วยการเจน glyph ยกสูงเข้า PUA และ map ตอน render บวกกับการจัดการ UTF-8 ที่ห้ามตัดกลางไบต์ บทเรียน: การรองรับภาษาไม่ใช่ฟีเจอร์ปลายทาง มันเป็นข้อจำกัดตั้งแต่ชั้นเลือกไลบรารี

3. หน่วยความจำบน MCU — แยก stack / heap / pool ของไลบรารีออกจากกัน:

เจอทั้งอาการ pool ของ LVGL เต็มขณะ heap ว่าง และ stack overflow ที่หน้าตาเหมือนรีบูตเฉย ๆ ทำให้อ่านอาการเป็นและวางขนาด draw buffer ได้ตามขอบเขต DRAM จริง บทเรียน: "RAM เหลือเท่าไร" เป็นคำถามที่กว้างเกินไปสำหรับงานฝังตัว

4. เครือข่ายในบ้าน — mDNS, subnet และการ debug ที่หลอกตา:

เข้าใจว่า .local ต้องมี responder ทั้งสองฝั่ง, อุปกรณ์ 2.4GHz กับ 5GHz อาจอยู่คนละ subnet จนคุยกันไม่ได้ และการทดสอบจากเครื่องตัวเองมักให้ผลลวง บทเรียน: ทดสอบจากมุมของอุปกรณ์ที่มีปัญหาเสมอ ไม่ใช่จากเครื่องที่สะดวก

5. สถาปัตยกรรม thin client — สัญญาข้อมูลเดียวที่ทำให้ต่อยอดถูก:

การบังคับให้ทุกฟีเจอร์ลงในรูป "การ์ด" เดียวกัน ทำให้เพิ่มของใหม่ 12 อย่างโดยแทบไม่แตะเฟิร์มแวร์ และย้าย logic หนักไปอยู่ฝั่งที่แก้ง่ายกว่าเสมอ บทเรียน: ออกแบบสัญญาข้อมูลให้ดีตั้งแต่แรก คือการลดงาน flash ในอนาคต

12
การ์ดข้อมูลจริงบนจอ
25
commits จาก bring-up ถึงใช้จริง
฿640
ต้นทุนฮาร์ดแวร์ทั้งหมด
0
key ที่อยู่ในบอร์ด
Aw mascot

TanPlanet Live

จอตั้งโต๊ะ ESP32 ส่วนตัว ที่เปลี่ยนการเปิด 6 แอปทุกเช้า ให้เหลือแค่ชำเลืองมองจอข้างคีย์บอร์ด — ฮาร์ดแวร์ ฿640 หนึ่งบอร์ด กับ backend Node ที่รันอยู่บน Mac เครื่องเดิม ไม่มีคลาวด์ ไม่มี key ในบอร์ด ข้อมูลไม่ออกนอกบ้าน

ESP32-035 LVGL 9 TFT_eSPI XPT2046 PlatformIO Node.js launchd mDNS Open-Meteo
Projects