ตัวจัดการสถานะแบบ OOP (Object Oriented State Machine) ฉบับรุ่นพี่จัดให้
State Machine ในโปรเจกต์แบบเบสิก
น้องๆ หลายคนที่อยากเขียนโปรแกรมหุ่นยนต์หรืออะไรเทือกนั้นบน [Arduino](https://s.shopee.co.th/7fUgFAWSki) ส่วนใหญ่ก็ต้องหนีไม่พ้นเรื่องการทำ State Machine กันทั้งนั้นแหละ อย่างเช่น หุ่นยนต์หลบสิ่งกีดขวางของพี่เนี่ย มันมีขั้นตอนการทำงาน (State) ประมาณนี้:
1) Scan สภาพแวดล้อมรอบตัว เพื่อหาทางที่โล่งที่สุดที่จะไปได้
2) เลี้ยว (Turn) ไปในทิศทางนั้น
3) วิ่ง (Drive) ไปทางนั้น พร้อมกับวัดระยะห่างจากวัตถุข้างหน้าไปด้วยตลอดเวลา
4) หยุด (Stop) ถ้าถอยไปเจอะระยะที่มันแคบเกินไป (Minimum value)
5) กลับไปเริ่มข้อ 1 ใหม่ จัดไปวัยรุ่น!
แล้วเราจะเขียนโค้ดในฟังก์ชัน loop()
ยังไงให้มันทำงานตามนี้ได้ล่ะ? พี่สรุปมาให้ 3 ทางเลือก:
1) เขียนโค้ดเป็นก้อนยาวๆ ใน loop()
โดยใช้ switch
หรือ if
/ else
แบบที่น้องๆ ชอบทำกันนั่นแหละ ถึงจะใช้ enum
มาช่วยตั้งชื่อสถานะให้ดูดีขึ้น แต่เชื่อพี่เหอะ ถ้ามีสถานะเยอะๆ แล้ว Logic ในแต่ละสถานะเริ่มซับซ้อน โค้ดน้องจะเละเทะ อ่านยาก แก้ไขทีนี่มีร้องแน่นอน
2) ไปหาโหลด FSM (Finite State Machine) Library สำเร็จรูปมาใช้ แล้วก็เขียน Adapter เชื่อมกับมันเอา
3) เขียน State Machine แบบ Object Oriented (OO) ของเราเองไปเลย ให้มันเป๊ะกับงานที่เราจะใช้
เท่าที่พี่ลองไปส่องดูในเน็ต ส่วนใหญ่เขาก็ใช้วิธีที่ 1 ไม่ก็ 2 กันทั้งนั้นแหละ แต่เดี๋ยววันนี้พี่จะสอนวิธีที่ 3 ให้ดู รับรองหล่อเท่กว่าเยอะ
ถ้าใครอยากหาความรู้เรื่อง State Machine เพิ่มเติม ลองไปดูพวกนี้เอาเองนะ:
สำหรับโปรเจกต์ Arduino ทั่วไป บางที Library สำเร็จรูปมันก็แอบเยอะเกินความจำเป็นไปนิด (Overkill) สู้งานเองดีกว่าน้อง
อธิบายโค้ดสักหน่อย
เพื่อให้เห็นความเฟี้ยวและพลังของการเขียนโปรแกรมแบบ OO พี่เลยทำโปรเจกต์ตัวอย่างขำๆ ที่มีแค่ 2 สถานะมาให้ดู นั่นคือสถานะ white
(สีขาว) กับ green (สีเขียว)
หลักการคือพี่สร้าง Base Class ชื่อว่า State ขึ้นมา โดยมีฟังก์ชันเทพๆ 2 ตัวคือ enter()
และ run()
.
โค้ดใน enter()
จะถูกเรียกแค่ครั้งเดียวตอนที่มีการเปลี่ยนสถานะ (State transition) ส่วนฟังก์ชัน run()
จะถูกเรียกซ้ำๆ ไปเรื่อยๆ และจะคืนค่ากลับมาเป็น State*
(Pointer ของ State) เพื่อเอาไว้เช็คว่าถึงเวลาต้องเปลี่ยนสถานะหรือยัง ถ้ามันคืนค่าเป็น State ใหม่ ฟังก์ชัน enter()
ของสถานะใหม่ก็จะโดนเรียกทันที
พี่เขียน enter()
ทิ้งไว้ให้ว่างๆ ใน Base Class แต่น้องจะทำให้มันเป็น Pure Virtual Function ก็ได้นะ ส่วนฟังก์ชัน exit()
พี่ไม่ได้ใส่มาให้ เพราะเท่าที่พี่ลองทำโปรเจกต์มา แค่ enter()
กับ run()
ก็เอาอยู่ทุกงานแล้วล่ะ
ทีนี้ในฟังก์ชัน loop()
หน้าที่มันก็แค่เช็คว่า State เปลี่ยนไหม ถ้าเปลี่ยนก็เรียก enter() แล้วก็รันฟังก์ชัน run()
ไปเรื่อยๆ ตราบใดที่มันยังส่งคืนค่าเป็น State เดิมอยู่ ดังนั้น Logic ทั้งหมดที่จะตัดสินใจว่าจะอยู่ที่เดิมหรือย้ายไป State อื่น จะรวมอยู่ใน run()
ของแต่ละ Class เลย น้องจะเปลี่ยนสถานะตามค่า [Sensor](https://s.shopee.co.th/7VBG2rX65j) หรือตามปุ่มกด (Button) ก็ใส่ไปในนี้ได้เลย
จำไว้นะน้อง เราอยากใช้พลังของ Arduino ให้คุ้มที่สุด เพราะฉะนั้นห้ามใช้ฟังก์ชันที่มันหยุดรอ (Blocking function) เด็ดขาด ไม่ว่าจะเป็นใน enter()
หรือ run() (แต่แอบกระซิบว่า ในตัวอย่างพี่แอบแหกกฎใช้ delay()
ในฟังก์ชัน flash()
นิดนึงนะ อย่าหาทำตามล่ะ 5-5-5)
ในงานจริง น้องต้องหลีกเลี่ยงอะไรพวกนี้ แล้วใช้คำสั่งที่รันปุ๊บจบปั๊บแทน อย่างในฟังก์ชัน run()
ของพี่ พี่ก็ใช้ Timer เช็คเอาว่าจะต้องทำอะไรตอนไหน
สุดท้ายเราก็แค่สร้าง Instance ของแต่ละ State ขึ้นมาอย่างละตัว ก็พร้อมลุยแล้ว
โปรเจกต์ State Machine ตัวอย่าง
โปรเจกต์นี้โคตรง่าย มี LED สองสี ขาวกับเขียว ต่อที่ Pins 13 กับ 12 พี่สร้าง Class White
กับ Green
ที่สืบทอดมาจาก State โดยพอ enter()
ปุ๊บ มันจะสั่งไฟกระพริบโชว์ก่อน และตอนอยู่ในฟังก์ชัน run()
มันจะเปิด LED ค้างไว้ 1 วินาที หรือ 2 วินาทีตามลำดับ พอครบเวลาปุ๊บ ฟังก์ชันจะส่งค่าคืนกลับเป็นอีก State หนึ่งเพื่อสลับกันไปมา
ข้อดีของการเขียน State Machine เอง
เห็นชัดๆ เลยว่าวิธีที่ 3 ที่พี่สอนเนี่ย ชนะขาดวิธีแรก สมมติถ้าน้องมี 4 สถานะ แล้วแต่ละสถานะต้องทำอะไรต่างกันเยอะแยะ ถ้าเขียนกองรวมกันใน loop()
น้องต้องมานั่งเช็คตัวแปรโน่นนี่นั่นว่ารันครั้งแรกหรือยัง โค้ดจะยาวเหยียดและดูไม่จืดเลยล่ะ
แถมวิธีนี้ น้องสามารถเพิ่มสถานะใหม่ๆ เข้าไปได้เรื่อยๆ โดยที่ไม่ต้องไปแตะต้องฟังก์ชัน loop()
แม้แต่นิดเดียว! พวกตัวแปรอย่าง Timer ก็เก็บไว้ใน Class ของใครของมัน ไม่ไปรกโลกภายนอก (Global namespace) ให้ปวดหัว
เทียบกับวิธีที่ 2 การใช้ Library มันก็ดีนะ แต่น้องต้องเดินตามเกมที่เขาวางไว้ ต่างจากวิธีนี้ที่เราคุมเองหมด เช่น ถ้าอยากรู้ว่าเราเพิ่งมาจาก State ไหน ก็แค่แก้ interface ของ enter()
ให้เป็น enter(State*)
แค่นี้ก็เขียน Logic แยกตามสถานะก่อนหน้าได้แล้ว หล่อเท่สไตล์วิศวะเลยน้อง!
รายละเอียดทางเทคนิคเพิ่มเติม
โครงสร้างการเขียนโปรแกรม C++ ขั้นสูง
คู่มือเทคนิคสำหรับการทำ "State Machine" โดยใช้หลักการ Object-Oriented Programming (OOP) ซึ่งจะช่วยให้โปรเจกต์ Arduino ของน้องขยายสเกลได้ง่ายและไล่บั๊ก (Debug) ได้แบบมืออาชีพ
- Class-Based State Encapsulation: แทนที่จะใช้
switch-caseบวมๆ น้องก็จับแต่ละสถานะ (เช่น IDLE, RUNNING, ALARM) แยกเป็นคนละ Object ไปเลย โดยแต่ละตัวจะมีฟังก์ชันenter(),update(), และexit()เป็นของตัวเอง - Polymorphic Event Handlers: ใน Loop หลักของ Arduino น้องแค่สั่ง
activeState->update()สั้นๆ พอ จะเพิ่มฟีเจอร์ใหม่หรือ State ใหม่ก็แค่เพิ่ม Class เข้าไป ไม่ต้องกลัวว่าจะไปทำ Logic เก่าพัง
การจัดการความซับซ้อน
- Hierarchical State Transitions: มีระบบ "Transition Manager" คอยคุมการสลับ State (เช่น จาก IDLE ไปเป็น WAITING) เพื่อให้มั่นใจว่า Hardware ต่างๆ เช่น Motor หรือ LED จะถูกรีเซ็ตค่าอย่างปลอดภัยทุกครั้งที่เปลี่ยนท่าการทำงาน ไม่ต้องกลัวช็อต!