Ghost-Desk Monitor

How It Works

ตั้งแต่มีคนนั่งลง จนป้ายบนโต๊ะเปลี่ยนเป็นสีชมพูและจอขึ้นว่าโต๊ะไม่ว่าง ใช้เวลาราว 1 นาที หน้านี้ตามสัญญาณไปทีละ hop: เซนเซอร์แปลงโลกจริงเป็นไฟฟ้า, ESP32-S3 อ่านผ่านสายแบบต่าง ๆ, ส่งต่อทางวิทยุ, gateway ตัดสิน แล้วสั่ง servo และขึ้น cloud

ทุกรูปและตัวเลขจำลองตามแบบที่วางไว้ ยังไม่ใช่ค่าที่วัดจากเครื่องจริง · AI บนบอร์ดหรือบน cloud ยังรอทีมตัดสินใน P1

นั่งลงแล้วเกิดอะไรขึ้น

Fig 1 · 8 ขั้น
ชิ้นที่ทำงานในขั้นนั้นสัญญาณวิทยุ: ESP-NOW, Wi-Fi, Internetป้ายและจอเปลี่ยนสีตามสถานะโต๊ะห้องอ่านหนังสือหนึ่งมุมบนแท่น: โต๊ะ D07 กับ sensor node (เรดาร์ใต้ขอบโต๊ะ, หัวกล้องบนเสา, ป้ายปริซึมกับ servo), gateway กับ Wi-Fi router บนตู้, จอ dashboard บนผนัง และโทรศัพท์ของเจ้าของโต๊ะ · เส้นโค้งที่ขึ้นไปสูงแทนการวิ่งออกอินเทอร์เน็ตไป Supabase หรือ LINE แล้วกลับมา
ขั้น 1 · 0 s

มีคนนั่งลง

เรดาร์ LD2410 ใต้ขอบโต๊ะส่งคลื่น 24 GHz ออกไปตลอดเวลา คลื่นสะท้อนจากตัวคนกลับมา และแม้คนจะนั่งนิ่ง การหายใจทำให้หน้าอกขยับไม่กี่มิลลิเมตร เรดาร์จึงรายงานว่า “มีเป้านิ่ง” ได้ ส่วน AMG8833 บนหัวกล้องเห็นพิกเซลอุ่นขึ้นจาก 25 เป็น 31 °C

บนสาย · on the wireLD2410 → UART 256000 baud · เฟรม F4 F3 F2 F1 … ~10 ครั้ง/วินาที · AMG8833 → I²C 0x69, 64 พิกเซล

ทฤษฎี: เซนเซอร์ ↓
เส้นเวลาทั้งหมด · แตะแถวเพื่อดูในรูป

จากโลกจริงเป็นตัวเลข

sense → signal → bus → data

ระบบ embedded ทุกระบบทำ 4 อย่างวนไป: รับรู้ (sensor แปลงปริมาณจริงเป็นสัญญาณไฟฟ้า), อ่าน (MCU รับสัญญาณผ่าน interface ที่ตกลงกันไว้), คิด (โปรแกรมตัดสินจากข้อมูล) และ สั่ง (actuator เปลี่ยนโลกจริงกลับ) ตารางนี้คือทุกเส้นทางเข้าออกของโปรเจกต์

ปริมาณจริงตัวแปลง (transducer)เป็นสัญญาณแบบไหนต่อเข้า MCU ด้วยได้เป็น
การเคลื่อนไหวและลมหายใจLD2410 เรดาร์ 24 GHzคลื่นสะท้อน → โมดูลประมวลผลเองUART 256000 baud + ขา OUTมี/ไม่มีเป้า, ระยะ, ความแรง
ความร้อนจากตัวคนAMG8833 thermopile 8 × 8แรงดันเล็ก ๆ ต่อพิกเซล → ADC ในชิปI²C ที่อยู่ 0x69อุณหภูมิ 64 จุด
แสงที่สะท้อนจากโต๊ะOV2640 กล้องพิกเซลทีละไบต์แบบขนานDVP: 8 สายข้อมูล + PCLK, VSYNC, HREFภาพ JPEG → ตัวจำแนก
CO₂ ในห้องSCD41 (gateway)วัดการดูดกลืนแสงอินฟราเรด (NDIR)I²C 0x62ppm
ความสว่างBH1750 (gateway)photodiode → ADC ในชิปI²C 0x23lux
เสียงในห้องMAX4466 ไมค์ + op-amp (gateway)แรงดันแอนะล็อกรอบ 1.65 VADC1 ของ ESP32-S3, 12 บิตระดับเสียง
มุมของป้าย (ขาออก)TD-7120MG servo 270°PWM 50 Hz พัลส์ 0.5–2.5 msLEDC ของ ESP32-S3ป้ายหันหน้าที่ต้องการ

เซนเซอร์รู้ได้อย่างไรว่ามีคน

ขั้น 1 และ 7

เรดาร์ mmWave: เห็นแม้คนนั่งนิ่ง

LD2410 ส่งคลื่น 24 GHz ที่ความถี่ไต่ขึ้นเรื่อย ๆ (chirp แบบ FMCW) คลื่นที่สะท้อนตัวคนกลับมาช้ากว่าเดิม t = 2R / c พอเอาคลื่นขาออกกับขาเข้ามาผสมกัน จะได้ความถี่ผลต่าง (beat) ที่แปรตามระยะ R โมดูลทำ FFT เองแล้วแบ่งระยะเป็นช่อง (gate) ละ 0.75 m

คนนั่งนิ่งก็ยังหายใจ หน้าอกขยับไม่กี่มิลลิเมตร ทำให้เฟสของคลื่นสะท้อนเปลี่ยนเป็นจังหวะ เรดาร์จึงรายงาน เป้านิ่ง ได้ ซึ่ง PIR ธรรมดาทำไม่ได้ · ข้อควรระวัง: เรดาร์ทะลุโต๊ะบาง ๆ และเห็นคนโต๊ะข้าง ๆ ต้องตั้ง gate สูงสุดให้ไม่เกินระยะเก้าอี้

Thermal: แยกคนออกจากของ

ทุกวัตถุแผ่รังสีอินฟราเรดตามอุณหภูมิ (กำลัง ∝ T⁴) แต่ละพิกเซลของ AMG8833 เป็น thermopile ที่ร้อนขึ้นตามรังสีที่ได้รับ แล้วชิปแปลงเป็นอุณหภูมิให้ · คนผ่านเสื้อผ้าราว 30 °C ส่วนกระเป๋าเท่าห้อง จึงตอบได้ว่าสิ่งที่อยู่บนโต๊ะ เป็นคนหรือเป็นของ

กล้อง: ใช้เมื่อจำเป็นเท่านั้น

กล้องถ่ายภาพเดียวตอน gateway ขอ คือเมื่อไม่เห็นคนครบ 2 นาที เพื่อตอบว่าโต๊ะโล่งหรือมีของ ภาพไม่ถูกเก็บ ใช้แล้วทิ้ง ประหยัดไฟและรักษาความเป็นส่วนตัว

AMG8833 · 8 × 8 pixels
th_max_c10 · ร้อนสุด × 10
318
th_hot_px · พิกเซลที่เกิน 30 °C
5
แปลว่า
มีวัตถุอุ่นขนาดคน
ค่าในตารางจำลองตามสเปก (ช่วง 0–80 °C ละเอียด 0.25 °C, 10 เฟรม/วินาที) ยังไม่ใช่ค่าที่วัดจริง · กระเป๋ามีอุณหภูมิเท่าห้อง AMG8833 จึงแยก “คน” กับ “ของ” ได้ แม้เรดาร์จะเห็นทั้งคู่ไม่ชัด
Sensor fusion · ไม่มีเซนเซอร์ตัวไหนตอบได้ครบคนเดียว จึงต้องรวมกัน
เรดาร์Thermalกล้องGateway สรุปว่า
เห็นอุ่น—มีคน
ไม่เห็น 2 นาทีไม่อุ่นitems_onlyไม่อยู่ (ทิ้งของ)
ไม่เห็น 2 นาทีไม่อุ่นemptyว่าง
เห็นไม่อุ่น—สงสัย: พัดลม/ม่านไหว รอกล้องยืนยัน

สัญญาณบนสาย

UART · I²C · PWM · ADC

สายระหว่างบอร์ดส่งได้แค่ไฟสูง (3.3 V = 1) กับไฟต่ำ (0 V = 0) ตามจังหวะ สิ่งที่ต่างกันในแต่ละแบบคือ ใครกำหนดจังหวะ และ ใช้กี่สาย

  • UART ไม่มีสายนาฬิกา (asynchronous) ทั้งสองฝั่งตกลง baud rate กันไว้ แล้วใช้ start bit เป็นจุดเริ่มจับเวลา · TX ของฝั่งหนึ่งต่อเข้า RX ของอีกฝั่ง
  • I²C มีสายนาฬิกา (synchronous) ที่ MCU เป็นคนตี อุปกรณ์หลายตัวใช้ 2 สายร่วมกัน แยกด้วยที่อยู่ สายเป็น open-drain จึงต้องมีตัวต้านทาน pull-up (โมดูลส่วนใหญ่ใส่มาแล้ว)
  • PWM ส่งค่าด้วยความกว้างพัลส์ ฮาร์ดแวร์ LEDC ของ ESP32 สร้างให้เองโดยไม่กิน CPU
  • ADC อ่านแรงดันแอนะล็อกตรง ๆ เป็นตัวเลข ต้องสุ่มอ่านถี่พอกับสิ่งที่อยากรู้ (เสียงพูดต้องหลายพันครั้งต่อวินาที)

ทุกบอร์ดต้องต่อ GND ร่วมกัน เพราะ “ไฟสูง” วัดเทียบกับ GND ถ้า GND ไม่ร่วม 1 กับ 0 จะเพี้ยน

ไบต์ที่ส่ง · ต้นเฟรมรายงานของ LD2410
idleS0b00b10b21b30b41b51b61b71Pidle

256000 baud → 1 บิต = 3.9 µs · 1 ไบต์ = start + 8 บิต + stop = 10 บิต = 39 µs · ส่งบิตต่ำสุด (b0) ก่อน · 0xF4 = 11110100₂

ข้างใน ESP32-S3

ขั้น 2 · interrupt, task, queue

Interrupt แทนการวนถาม

ถ้าโปรแกรมวนถามขา OUT ของเรดาร์ซ้ำ ๆ (polling) CPU จะเสียเวลาเปล่า และอาจพลาดถ้ากำลังทำอย่างอื่นอยู่ · interrupt ให้ฮาร์ดแวร์เรียกฟังก์ชัน ISR ทันทีที่ขาเปลี่ยน ISR ต้องสั้นที่สุด แค่ปลุก task แล้วออก งานหนักไปทำใน task

แบ่งงานเป็น task

FreeRTOS สลับหลาย task บน CPU ให้ดูเหมือนทำพร้อมกัน task ส่งข้อมูลให้กันผ่าน queue ไม่ใช้ตัวแปร global ร่วมกัน จึงไม่ชนกัน · ห้ามใช้ delay() ยาว ๆ เพราะจะบล็อกทุกอย่าง ใช้ vTaskDelay ที่ยกเวลาให้ task อื่นแทน

sensor_task.cpp ร่างแนวคิด ยังไม่ใช่โค้ดจริง
// เรดาร์ดึงขา OUT ขึ้นเมื่อเห็นคน: ปลุก task แทนการวนถาม
void IRAM_ATTR onRadarOut() {
  BaseType_t woke = pdFALSE;
  xSemaphoreGiveFromISR(radarSem, &woke);   // สั้นที่สุด: แค่ส่งสัญญาณ
  portYIELD_FROM_ISR(woke);
}

void sensorTask(void *) {
  for (;;) {
    xSemaphoreTake(radarSem, pdMS_TO_TICKS(100));  // ตื่นเมื่อ ISR ปลุก หรือทุก 100 ms
    Reading r = readRadarFrame();                  // UART
    readThermal(r.pixels);                         // I²C 64 พิกเซล
    xQueueSend(readings, &r, 0);                   // ให้ radio task รวมเป็นรายงาน
  }
}
Task บน node
Taskทำงานเมื่อทำอะไร
sensor10 ครั้ง/วินาที และเมื่อ ISR ปลุกอ่านเฟรม UART ของเรดาร์ และ 64 พิกเซลจาก I²C แล้วใส่ queue
visionเมื่อ gateway ขอ (ไม่เห็นคน 2 นาที)ถ่าย 1 ภาพ จำแนก empty / person / items_only · ภาพไม่ถูกเก็บ
radioทุก 2 วินาที และทันทีเมื่อค่าเปลี่ยนรวมค่าจาก queue เป็น node_report_t ส่ง ESP-NOW รอ ACK และ retry
signเมื่อได้คำสั่ง SET_SIGNตั้ง LEDC duty ให้ servo หมุน แล้วหยุดจ่ายพัลส์หลัง 1.5 วินาที (timeout)

ส่งทางวิทยุด้วย ESP-NOW

ขั้น 3 · 18 ไบต์

ESP-NOW ใช้วิทยุ Wi-Fi ตัวเดิมของ ESP32 ส่งข้อความสั้นถึงบอร์ดอื่นตรง ๆ ด้วย MAC address ไม่ต้องต่อ router และไม่ต้องมี IP ข้อความหนึ่งยาวได้ถึง 250 ไบต์ ของเรามี 18

  • ACK: ผู้รับตอบกลับในระดับฮาร์ดแวร์ทันที send callback บอกฝั่งส่งว่าสำเร็จไหม ไม่สำเร็จก็ส่งซ้ำ (สูงสุด 3 ครั้ง) · seq ช่วยให้ gateway ทิ้งข้อความซ้ำ
  • ช่องสัญญาณต้องตรงกัน: gateway ต่อ Wi-Fi ห้องสมุดด้วย วิทยุตัวเดียวจึงอยู่ได้ช่องเดียว node ต้องส่งบนช่องเดียวกับ access point
  • ทางกลับ: gateway สั่ง node ด้วยข้อความสั้นกว่า {ver, node_id, seq, cmd = SET_SIGN, face}

ตัวอย่างไบต์ด้านขวาคือรายงานหนึ่งวินาทีหลังมีคนนั่งที่ D07 · โครงสร้างเต็มอยู่ใน Docs และ Build Plan ขั้น B2

node_report_t · 18 ไบต์ · แตะช่องเพื่อดูความหมาย
th_max_c10 = 318 (2 ไบต์, ไบต์ต่ำก่อน) พิกเซลที่ร้อนที่สุด × 10 = 31.8 °C ใช้ × 10 เพื่อเก็บทศนิยมในจำนวนเต็ม
ESP32 เป็น little-endian: seq = 1042 = 0x0412 จึงส่ง 12 แล้วตามด้วย 04 · __attribute__((packed)) กันไม่ให้ compiler แทรกช่องว่างระหว่างฟิลด์ ทั้งสองฝั่งจึงตีความไบต์ตรงกัน

Gateway ตัดสินด้วย state machine

ขั้น 4 และ 7–8

โต๊ะหนึ่งตัวอยู่ได้ทีละ สถานะ และเปลี่ยนสถานะเมื่อเงื่อนไขครบเท่านั้น gateway เก็บ state machine แยกต่อโต๊ะ พร้อมเวลาที่เห็นคนครั้งล่าสุด (จาก millis()) ทุกรายงานที่เข้ามาจะถูกเอามาเทียบกับเงื่อนไขในรูป

ทำไมตัดสินที่ gateway ไม่ใช่ที่ node: กติกาอยู่ที่เดียว แก้ทีเดียวได้ทุกโต๊ะ, cloud เห็นความจริงชุดเดียว และ node เหลือหน้าที่แค่วัดกับหมุนป้าย

เห็นคนครบ 1 นาทีโต๊ะโล่งกล้อง: emptyมีของค้างกล้อง: items_onlyกลับมานั่งครบ 30 นาที · ปล่อยโต๊ะธงให้เจ้าหน้าที่มาเก็บของว่างfreeใช้งานinUseไม่อยู่awayไม่เห็นคน 2 นาที → ถามกล้องนาทีที่ 20: LINE เตือนเจ้าของ
ตัวเลขทุกตัวในรูปมาจากค่าคงที่ชุดเดียวกับเดโมในหน้า Overview (lib/session.ts) · ถ้า AI ล่ม gateway ใช้กติกาเดียวคือไม่เห็นคนครบ 30 นาทีแล้วปล่อยโต๊ะ

หมุนป้ายด้วย servo

ขั้น 5 · PWM และไฟเลี้ยง

Servo คือระบบควบคุมแบบป้อนกลับในกล่องเล็ก ๆ

ข้างใน servo มีมอเตอร์ เฟือง และตัวต้านทานปรับค่าที่หมุนตามแกน วงจรข้างในเทียบมุมที่วัดได้กับมุมที่พัลส์สั่ง แล้วหมุนจนตรงกัน (closed loop) · เราแค่ส่งพัลส์ 500–2500 µs ทุก 20 ms ดูได้ในแท็บ PWM ด้านบน

ป้ายมี 3 หน้าห่างกัน 120° จึงต้องใช้ servo 270° · มีหมุดกันหมุนเกินที่ฐาน และหยุดส่งพัลส์หลังหมุนเสร็จ (timeout) servo จะได้ไม่เกร็งค้างจนร้อน

ทำไมต้องมีตัวเก็บประจุ 1000 µF

ตอน servo เริ่มหมุน มันดึงกระแสกระชากสูงชั่วขณะ (ระดับ 1–2 A) อแดปเตอร์ตอบสนองไม่ทัน แรงดัน 5 V จึงตกวูบ ถ้าตกต่ำพอ ESP32 จะ brownout แล้วรีเซ็ต

ΔV = I · Δt / C = 2 A × 0.1 ms / 1000 µF = 0.2 V

ตัวเก็บประจุที่วางชิดขั้ว servo จ่ายกระแสช่วงสั้น ๆ นั้นแทน แรงดันจึงตกแค่ราว 0.2 V แทนที่จะตกจนบอร์ดดับ (ตัวเลขเป็นค่าประมาณเพื่อดูขนาด) · servo รับไฟจาก terminal block ตรงจากอแดปเตอร์ ไม่ผ่านบอร์ด และใช้ GND ร่วมกัน

ขึ้น cloud แล้วกลับลงมาที่จอ

ขั้น 6 และ 8

Gateway เป็นบอร์ดเดียวที่ต่ออินเทอร์เน็ต เมื่อสถานะโต๊ะเปลี่ยน มันส่งข้อมูล 1 แถวเป็น JSON ไปที่ REST API ของ Supabase ผ่าน HTTPS ซึ่งเข้ารหัสด้วย TLS (บอร์ดต้องรู้จัก root certificate ของ server) Supabase เก็บแถวลง Postgres แล้วใช้ Realtime ดันแถวใหม่ผ่าน WebSocket ไปที่หน้า dashboard ที่เปิดอยู่

LINE ก็ใช้ HTTPS เหมือนกัน: gateway POST ข้อความไปที่ LINE Messaging API พร้อม token แล้ว LINE ส่งต่อถึงโทรศัพท์ · key และ token อยู่ใน secrets.h ที่ไม่ขึ้น GitHub

ถ้าเน็ตหลุด gateway ยังตัดสินสถานะและสั่งป้ายได้ตามปกติ (ทุกอย่างในห้องใช้ ESP-NOW) แล้วเก็บแถวไว้ใน RAM ส่งตามทีหลังเมื่อเน็ตกลับมา (store-and-forward)

ขั้น 6 แถวที่ gateway ส่ง (ตัวอย่าง)
POST https://<project>.supabase.co/rest/v1/desk_events
apikey: <anon key จาก secrets.h>
Content-Type: application/json

{
  "desk": "D07",
  "state": "inUse",
  "at": "2026-11-20T13:01:00Z",
  "mmw_dist_cm": 62,
  "th_max_c": 31.8,
  "ai_class": null,
  "seq": 1042
}
Latency budget · จากนั่งลงถึงจอเปลี่ยนสี (ค่าประมาณ ต้องวัดจริงในขั้น F)
ช่วงใช้เวลาเพราะ
เรดาร์เห็นคน → node อ่านค่า~0.1 sเรดาร์ส่งเฟรม ~10 ครั้ง/วินาที
ESP-NOW node → gateway< 1 ms18 ไบต์บนอากาศ (+ retry ถ้าไม่ได้ ACK)
กติกา “เห็นครบ 1 นาที”60 sตั้งใจรอ กันคนเดินผ่าน
SET_SIGN → servo หมุนเสร็จ≤ 0.8 sservo หมุนราว 0.2 s ต่อ 60° · ไกลสุด 240°
HTTPS ขึ้น Supabase~0.3–1 sTLS handshake กินเวลามากสุด
Realtime → dashboard~0.1–0.5 sWebSocket ที่เปิดค้างไว้
รวม~61–63 sเกือบทั้งหมดคือการรอ 1 นาทีที่ตั้งใจไว้

ศัพท์ที่ใช้ในหน้านี้

Glossary
MCU
microcontroller ชิปเดียวที่มี CPU, RAM, flash และขา I/O ในตัว · ที่นี่คือ ESP32-S3
GPIO
ขาดิจิทัลอเนกประสงค์ อ่านได้ว่าเป็น 0 หรือ 1 หรือสั่งให้เป็น 0/1
ISR
Interrupt Service Routine ฟังก์ชันสั้น ๆ ที่ฮาร์ดแวร์เรียกทันทีเมื่อขาเปลี่ยน แทนการวนถามซ้ำ (polling)
FreeRTOS task
งานย่อยที่ทำงานสลับกันบน CPU มีลำดับความสำคัญของตัวเอง ส่งข้อมูลกันผ่าน queue
UART
สายสื่อสารอนุกรมไม่มีนาฬิกา สองฝั่งต้องตั้ง baud rate ตรงกัน TX ต่อ RX ไขว้กัน
I²C
บัส 2 สาย (SDA, SCL) ต่อพ่วงหลายอุปกรณ์ แยกกันด้วยที่อยู่ 7 บิต ต้องมีตัวต้านทาน pull-up
PWM
สัญญาณเปิด-ปิดเป็นจังหวะ ส่งข้อมูลด้วยความกว้างของพัลส์ · servo อ่านความกว้างเป็นมุม
ADC
ตัวแปลงแรงดันแอนะล็อกเป็นตัวเลข 12 บิต = 0–4095
ESP-NOW
โปรโตคอลของ Espressif ส่งข้อความสั้นถึงบอร์ดอื่นตรง ๆ ด้วยวิทยุ Wi-Fi โดยไม่ต้องต่อ router
Gateway
บอร์ดกลางของห้อง รับข้อมูลจาก node ทุกตัว ตัดสินสถานะ และเป็นตัวเดียวที่ต่ออินเทอร์เน็ต
TLS / HTTPS
การเข้ารหัสระหว่างบอร์ดกับ server ทำให้คนในเครือข่ายเดียวกันอ่านหรือปลอมข้อมูลไม่ได้
Realtime
บริการของ Supabase ที่ดันแถวข้อมูลใหม่ไปยังหน้าเว็บผ่าน WebSocket ทันที หน้าเว็บไม่ต้องรีเฟรช

ดูอุปกรณ์ทุกชิ้น · Equipmentลำดับการสร้าง · Build Plan