ปรับแก้โค้ดให้เหมาะสม

จากสเก็ตช์ที่ทำงานได้ ไปสู่โคมไฟที่ใช้งานได้จริง

Author

ผศ.ดร.ศุภชัย วรพจน์พิศุทธิ์

Published

4 ตุลาคม 2569

โค้ดในขั้นที่ 2–3 ทำงานได้ แต่ถ้าลองใช้จริงจะพบปัญหา หน้านี้พาคุณ สังเกตปัญหา → หาสาเหตุ → ปรับแก้ → วัดผล ทีละข้อ โดยแก้เฉพาะโค้ด Arduino โค้ดที่ปรับแล้วทั้งหมดอยู่ใน devices/04_smart_lamp_tuned/04_smart_lamp_tuned.ino เปิดเทียบกับ 03_smart_lamp.ino แล้วดูว่าแตกต่างตรงไหนบ้าง

ปัญหาที่พบ สาเหตุ วิธีปรับแก้ ที่ไหน
ไฟกะพริบเมื่อแสงใกล้เกณฑ์ ค่าที่อ่านมีสัญญาณรบกวน และเกณฑ์มีค่าเดียว Smoothing + Hysteresis บอร์ด
ค่าเซนเซอร์ไม่เต็ม 0–1023 วงจร/เซนเซอร์แต่ละตัวต่างกัน Calibration บอร์ด
ละเอียดไม่พอ ADC 10 บิตค่าเริ่มต้น analogReadResolution(14) บอร์ด
หลอดไฟกระโดด ติด-ดับทันที ใช้ digitalWrite PWM fade บอร์ด
โปรแกรมค้างรอ ไม่ตอบสนอง ใช้ delay() millis() ไม่หยุดรอ บอร์ด

1. สังเกตปัญหา: ไฟกะพริบ

อัปโหลดสเก็ตช์ 02_local_nightlight แล้วค่อย ๆ หมุน potentiometer ให้ค่าแกว่งอยู่ใกล้ 358 (ประมาณ 35%) ดู Serial Monitor จะเห็น lamp:1 กับ lamp:0 สลับกันถี่ ๆ

Noteทำไมจึงกะพริบ

ถ้าสัญญาณจริงคือ 35% แต่สัญญาณรบกวนทำให้ค่าที่อ่านแกว่ง 34% ↔︎ 36% ทุกครั้งที่ข้ามเส้น 35% กฎจะกลับคำตัดสิน เรามีสองวิธีแก้ที่ใช้ร่วมกันได้: ให้เส้นตัดสินมีสองเส้น (hysteresis) และทำให้ค่านิ่งขึ้น (smoothing)

2. Hysteresis: เกณฑ์สองเส้น

แทนที่จะมีเกณฑ์เดียว ให้มี เกณฑ์เปิด ต่ำกว่า เกณฑ์ปิด ในช่วงกลางให้คงสถานะเดิม

ค่าที่อ่านได้   0 ───────── 307 ═══════════ 409 ───────── 1023
                 [ เปิดไฟ ]   [ คงสถานะเดิม ]   [ ปิดไฟ ]
                 (ต่ำกว่า 30%)                    (ตั้งแต่ 40% ขึ้นไป)

ลองทำ: แก้ 02_local_nightlight.ino ให้มีเกณฑ์สองค่า

const int ON_BELOW = 307;    // ประมาณ 30% ของ 1023
const int OFF_ABOVE = 409;   // ประมาณ 40% ของ 1023
bool lampOn = false;         // จำสถานะไว้นอก loop()

void loop() {
  int raw = analogRead(SENSOR_PIN);
  if (raw < ON_BELOW) lampOn = true;
  else if (raw >= OFF_ABOVE) lampOn = false;
  // ค่าอยู่ระหว่างสองเกณฑ์: ไม่แตะ lampOn (คงสถานะเดิม)
  digitalWrite(LED_PIN, lampOn ? HIGH : LOW);
  ...
}

หมุน potentiometer ให้ค่าอยู่ช่วง 307–409 ไฟควรนิ่ง สังเกตว่าตัวแปร lampOn ต้องประกาศนอก loop() เพราะต้องจำค่าข้ามรอบ

สเก็ตช์ขั้นที่ 4 ใช้หลักการเดียวกันใน offlineDecision() สำหรับโหมด offline

Noteฝั่งผู้สอน

เมื่อบอร์ดเชื่อมต่อ gateway การตัดสินใจโหมดอัตโนมัติอยู่ที่ gateway ซึ่งผู้สอนตั้ง hysteresis ได้ด้วย --on-below 30 --off-above 40 (ดูหน้า Gateway) นักเรียนไม่ต้องแก้ส่วนนี้

3. Smoothing: เฉลี่ยค่าหลายครั้ง

แทนที่จะส่งค่าที่อ่านได้ครั้งเดียวทุก 3 วินาที ให้อ่านบ่อย ๆ (ทุก 100 ms) แล้วเก็บค่าล่าสุด 16 ค่า ส่งค่าเฉลี่ย นี่คือ moving average

04_smart_lamp_tuned.ino
void addSample(long value) {
  if (sampleCount == WINDOW) sampleSum -= samples[sampleIndex];
  else sampleCount++;
  samples[sampleIndex] = value;
  sampleSum += value;
  sampleIndex = (sampleIndex + 1) % WINDOW;
}

สังเกตว่าเก็บ ผลรวม ไว้และเพิ่ม/ลบทีละค่า ไม่ต้องบวกครบ 16 ค่าใหม่ทุกรอบ เป็นเทคนิคที่ประหยัดเวลาประมวลผลบนไมโครคอนโทรลเลอร์

Tipลองเปลี่ยน WINDOW

เพิ่มจาก 16 เป็น 64 ค่านิ่งขึ้นแต่ตอบสนองช้าลง (เฉลี่ยค่าย้อนหลัง 6.4 วินาที) ลดเหลือ 4 ตอบสนองเร็วแต่ยังแกว่ง เลือกค่าที่สมดุลสำหรับห้องของคุณ นี่คือ trade-off พื้นฐานของการกรองสัญญาณ

4. Calibration: ปรับเทียบเซนเซอร์

เซนเซอร์แต่ละตัวมีช่วงค่าที่ใช้งานจริงไม่เท่ากัน เช่น ห้องมืดสุดอ่านได้ 800 และสว่างสุดอ่านได้ 14,000 (จากเต็ม 16,383) โค้ดจะมองว่ามืดสุดคือ 5% และสว่างสุดคือ 85% ซึ่งไม่ตรงความจริง ให้วัดสองค่านี้จาก Serial Monitor แล้วใส่ใน

04_smart_lamp_tuned.ino
const long SENSOR_DARK = 800;      // ค่า 14 บิตเมื่อมืดสุดในห้องของคุณ
const long SENSOR_BRIGHT = 14000;  // ค่า 14 บิตเมื่อสว่างสุด

ฟังก์ชัน lightRaw10() จะแปลงช่วงนี้ให้เป็น 0–1023 ด้วยสูตรเส้นตรง (เหมือน map() ของ Arduino) แล้ว constrain ไม่ให้เกินช่วง ถ้าเซนเซอร์ของคุณให้ค่ากลับด้าน (มืด = ค่ามาก) ให้สลับค่าสองตัวนี้

5. อ่านละเอียดขึ้นด้วย 14 บิต

analogReadResolution(14) ทำให้ ADC ของ UNO R4 ให้ค่า 0–16383 (ละเอียดกว่า 10 บิต 16 เท่า) ค่าที่ส่ง gateway ยังเป็น 10 บิตตามสัญญา API เดิม แต่คำนวณหลังเฉลี่ยและปรับเทียบแล้ว ทำให้ได้ค่าที่นิ่งกว่า

6. PWM fade: ไฟค่อย ๆ สว่าง

ต่อ LED ภายนอกที่ D5 ตามแผนผังวงจร แล้วใช้ analogWrite ปรับความสว่าง 0–255 ทีละน้อย

04_smart_lamp_tuned.ino
void setLamp(bool on) { fadeTarget = on ? 255 : 0; }

void updateFade() {
  if (millis() - lastFade < FADE_INTERVAL_MS) return;
  lastFade = millis();
  if (fadeLevel < fadeTarget) fadeLevel = min(fadeLevel + FADE_STEP, fadeTarget);
  else if (fadeLevel > fadeTarget) fadeLevel = max(fadeLevel - FADE_STEP, fadeTarget);
  analogWrite(LED_PIN, fadeLevel);
}

setLamp() เพียงตั้งเป้าหมาย ส่วน updateFade() ขยับความสว่างเข้าหาเป้าหมายทีละ 5 ระดับทุก 5 ms (ใช้เวลาราว 0.25 วินาทีจากดับถึงสว่างสุด) ลองเปลี่ยน FADE_STEP ให้เป็น 1 แล้วดูว่าหลอดไฟเปลี่ยนไปอย่างไร

7. millis() แทน delay()

delay(3000) ทำให้บอร์ดหยุดทำทุกอย่าง 3 วินาที (อ่าน Serial ไม่ได้ ปรับไฟไม่ได้) ทางที่ดีกว่าคือจดเวลาไว้แล้วเช็คว่าถึงเวลาหรือยัง

if (millis() - lastSample < SAMPLE_INTERVAL_MS) return;  // ยังไม่ถึงเวลา ไปทำอย่างอื่นก่อน
lastSample = millis();

ด้วยรูปแบบนี้ loop() วนรอบเร็วมาก และงานแต่ละอย่าง (อ่านเซนเซอร์ ปรับไฟ ส่ง Wi-Fi) ทำตามจังหวะของตัวเอง

Warningข้อจำกัดที่ยังเหลือ

การเชื่อมต่อ Wi-Fi และส่ง HTTP ในสเก็ตช์นี้ยังหยุดรอ (สูงสุดราว 5 วินาทีเมื่อเครือข่ายมีปัญหา) ระหว่างนั้น fade จะหยุดชั่วคราว ถ้าอยากให้ไม่หยุดเลย ต้องเขียนส่วนเครือข่ายเป็น state machine ซึ่งเป็นโจทย์ท้าทายด้านล่าง

8. วัดผลก่อนและหลัง

เติมตารางนี้ขณะทดลองหมุน potentiometer ช้า ๆ ผ่านจุดเกณฑ์ (ดูจำนวนครั้งที่ lamp สลับระหว่าง 0 กับ 1 ใน 1 นาทีจาก Serial Monitor)

เงื่อนไข จำนวนครั้งที่ไฟสลับใน 1 นาที
ขั้นที่ 2 เดิม (เกณฑ์เดียว)
ขั้นที่ 2 + hysteresis 30%/40% (โค้ดที่คุณแก้เอง)
ขั้นที่ 4 (smoothing + calibration + hysteresis)

โจทย์ท้าทาย

  1. ปรับ WINDOW และช่วง hysteresis ให้ไฟไม่กะพริบแต่ยังตอบสนองไม่เกิน 5 วินาที เมื่อห้องเปลี่ยนจากสว่างเป็นมืดจริง
  2. ปุ่มกดสลับโหมด: ต่อปุ่มกดที่ขา D2 (INPUT_PULLUP) ให้กดแล้วสลับโหมด offline ระหว่าง “ตามแสง” กับ “เปิดตลอด” (เซนเซอร์แบบ digital) อย่าลืมจัดการปัญหาปุ่มเด้ง (debounce) ด้วย millis()
  3. ไม่หยุดรอ: เขียน requestDecision() ใหม่ให้ทำงานเป็นขั้น ๆ ใน loop() ด้วย millis() โดยไม่ใช้ delay() หรือ client.readStringUntil() ที่รอเป็นวินาที
  4. เพิ่มข้อมูลที่ส่ง: ให้บอร์ดส่งค่า RSSI ของ Wi-Fi (WiFi.RSSI()) ไปด้วย ฝั่ง gateway ต้องแก้อะไรบ้าง และทำไมต้องเปลี่ยน schema_version (ถามผู้สอน)
  5. หรี่ตามความสว่าง: ให้ความสว่างของหลอดสวนทางกับความสว่างห้อง (ห้องมืดมาก ไฟสว่างมาก) ตัดสินใจที่บอร์ดหรือ gateway ดีกว่ากัน เพราะอะไร