Disclaimer:
ก่อนอื่นเลย รุ่นพี่จะอธิบายแค่แนวคิดหลักๆ และอธิบายการทำงานแบบคร่าวๆ เท่านั้น แต่จะพยายามแนบลิงก์แหล่งข้อมูลเจ๋งๆ ที่พี่ใช้ตอนทำโปรเจกต์นี้ให้เต็มที่
ประการที่สอง Arduino - Oplà IoT Starter-Kit นี้ได้รับสนับสนุนจาก Arduino SRL ในกิจกรรม Arduino Cloud Games พี่รู้สึกขอบคุณมากๆ ที่ได้มีโอกาสเข้าร่วม เพราะโจทย์ท้าทายนี้ช่วยให้พี่มีสมาธิผ่านปัญหามากมายที่เจอระหว่างทาง
Part I. The Motivation (แรงบันดาลใจ)
ถ้าน้องกำลังอ่านเรื่องนี้อยู่ ก็คงเดาได้ว่าน้องไม่ใช่ยอดนักกีฬาพันธุ์แท้ที่เล่นกีฬาอะไรก็ง่ายดายหรอก ไม่งั้นน้องคงไปใช้เวลาอยู่ในสนาม ในน้ำ หรือบนลู่ เพื่อฝึกซ้อมสำหรับการแข่งขันใหญ่ๆ ต่อไปแล้ว สำหรับมนุษย์ปุถุชนอย่างเราๆ การจะสร้างกลไกการเคลื่อนไหวที่เทพๆ เช่น การเตะ การขว้าง หรือแม้แต่การวิ่งที่เพอร์เฟกต์เนี่ย มันยากโคตรๆ เรามักต้องการคำติชม (Feedback) ขณะฝึกซ้อมเพื่อพัฒนาฝีมือ (ไม่ว่าจะด้านไหน) ซึ่งส่วนใหญ่ต้องมีคนที่สองที่มีสายตาอันช่ำชองมาคอยบอกว่าทำตรงไหนผิด โปรเจกต์ของพี่เลยตั้งใจจะแทนที่คนที่สองนั่นด้วยเทคโนโลยี
กีฬาที่พี่ชอบสุดคือเบสบอล บางคนอาจไม่เข้าใจกติกาเต็มที่ แต่ทุกคนต้องเคยเห็นภาพชายร่างใหญ่ถือไม้ตีลูกบอลให้กระเด็นออกจากสนาม (ถ้ายังไม่เคย ลองดูคลิปนี้ซักหน่อย ).
การจะได้สวิงที่เทพแบบนั้น ต้องประกอบด้วยหลายอย่าง ทั้งการประสานงานระหว่างมือกับตาที่แม่นยำ พละกำลังที่ดี จังหวะเวลา และกลไกการเคลื่อนไหว สองอย่างหลังนี่ฝึกและสังเกตได้โดยไม่ต้องตีลูกบอลเลย แค่ดูจากพลวัต (Dynamics) ของไม้ก็พอแล้ว ด้วยสวิงในอุดมคติ ไม้จะตีโดนลูกบอลจนทำให้มันมีความเร็วออกตัว (Exit Velocity) สูงถึง 160 กม./ชม. และมีมุมออกตัว (Launch Angle) ประมาณ 10°-25°

เป้าหมายของนักตีลูก (Hitter) ก็คือเร่งความเร็วไม้ให้เร็วที่สุดรอบแกนหมุน (ซึ่งก็คือตัวนักกีฬาเอง) ขณะเดียวกันก็ให้ไม้มีการเคลื่อนที่ขึ้นเล็กน้อยผ่านโซนการตี (Hitting Zone) ง่ายใช่ไหมล่ะ?
Part II. The Goal (เป้าหมาย)
ใช่ มันง่าย... ถ้าน้องรับรู้/รู้สึกถึงพลวัตของไม้ได้เต็มที่ระหว่างสวิง นี่คือจุดที่โค้ชเข้ามามีบทบาท เขาสามารถเห็นการเคลื่อนไหวและบอกได้ว่าอะไรผิดหรือถูก หรือ... น้องจะวัดพลวัตของไม้ด้วยไมโครคอนโทรลเลอร์แล้วอ่านค่าสถิติจากจอแสดงผลก็ได้
แนวคิดการวัดพลวัตของไม้ด้วย IMU (Inertial Measurement Unit) ไม่ใช่เรื่องใหม่ แม้แต่ในตลาดก็มีผลิตภัณฑ์เชิงพาณิชย์ออกมาบ้างแล้ว แต่การเล่นกับกล่องดำเล็กๆ มันคงไม่สนุกเท่าไหร่ และพี่ก็ไม่อยากจ่าย 150 ดอลลาร์เพื่อซื้อของที่พี่สร้างเองได้ (ด้วยเงินที่แพงกว่าอีก :D) ก่อนจะสร้างระบบของเรา มามาตั้งเป้าหมายกันก่อนดีกว่า:
ฝั่งเซนเซอร์ เป้าหมายสำหรับ BatSense:
- มันจะวัดพลวัตของไม้โดยใช้ IMU กล่าวให้เจาะจงคือ มันจะวัดความเร่ง (ax, ay, az) และความเร็วเชิงมุม (gx, gy, gz) ของไม้ที่บริเวณปลายด้ามจับ หลังจากนั้นเราจะสามารถคำนวณค่าสถิติพลวัตของจุดตีโดนใจ (Sweet Spot) ได้
- คำถามใหญ่คืออัตราการสุ่มตัวอย่าง (Sampling Rate) เนื่องจากระยะเวลาสวิงโดยเฉลี่ยอยู่ที่ประมาณ 150ms ความคิดแรกของพี่คือการเก็บข้อมูลตัวอย่างของสวิงประมาณ 100 จุดก็น่าจะโอเคแล้ว ดังนั้นจึงต้องการอัตราการสุ่มตัวอย่างประมาณ 670 Hz
- การเคลื่อนไหวของไม้นี้ต้องถูกแปลงไปยังระบบอ้างอิง (Reference Frame) ของนักตีลูก ซึ่งทำได้โดยใช้ตัวกรอง AHRS (Attitude Heading Reference System) ในโปรเจกต์นี้พี่จะใช้ตัวกรอง Madgwick เพราะมันค่อนข้างเร็วและแม่นยำพอสมควร และยังนำไปใช้งานได้ง่ายอีกด้วย
ส่วนการประมวลผลและเผยแพร่ข้อมูล นี่คือเป้าหมายของ BatStation
- BatStation จะรับข้อมูลที่วัดได้ผ่าน Bluetooth แล้วเก็บชั่วคราวไว้ใน RAM เป้าหมายระยะยาวคือเก็บข้อมูลลง SD-Card เพื่อเอาไปวิเคราะห์ต่อทีหลัง
- ข้อมูลต้องถูกประมวลผลก่อน เช่น ต้องจัดการกับเรื่อง roll-over เพราะ AHRS-Filter จะให้ค่าอยู่ระหว่าง +- 180° เราก็จะเห็นค่ากระโดดจาก 180° -> -180° และในทางกลับกัน นอกจากนี้เราจะใช้ moving average filter เพื่อทำให้ข้อมูลเรียบขึ้นอีกนิด
- ต่อไป เราต้องคำนวณคุณสมบัติเชิงพลวัต เช่น ความเร็วเชิงมุมสูงสุดรอบแกน z ภายในโซนที่ตีลูก และมุมการปล่อยลูก (launch angle)
- สถิติเหล่านี้จะถูกแสดงบนหน้าจอของผู้ใช้และอัปโหลดขึ้นคลาวด์
รุ่นพี่แบ่งส่วนการสร้างออกเป็น 4 ส่วนใหญ่ๆ: ฮาร์ดแวร์ 2 ส่วน และซอฟต์แวร์ 2 ส่วน แบ่งให้ BatSense กับ BatStation อย่างละเท่าๆ กัน
Part III. ประกอบฮาร์ดแวร์ของระบบ
BatSense
อย่างที่บอกไป BatSense จะถูกวางไว้ในด้ามจับไม้เบสบอล ดังนั้นอุปกรณ์ต้องเล็กและมีแหล่งจ่ายไฟแบบเคลื่อนที่ได้ สำหรับ MCU รุ่นพี่เลือกใช้ Nano 33 BLE เพราะมันมี BLE-Module ในตัว (ซึ่งตอนแรกตั้งใจจะใช้ส่งข้อมูล) และมี IMU ด้วย
สำหรับแหล่งจ่ายไฟ รุ่นพี่เลือกใช้แบตเตอรี่ LiPo ขนาดเล็กแบบที่ใช้ใน iPod Shuffle มันมีแรงดันสูงสุด 3.7 V ซึ่งมากเกินพอสำหรับงานเราแล้ว แบตเตอรี่จะต่อเข้ากับวงจรชาร์จ ซึ่งไม่ใช่แค่ชาร์จแบต แต่ยังเชื่อมต่อกับโหลด (อุปกรณ์) อีกด้วย
ด้วยความหวังลมๆ แล้งๆ ว่าจะใช้ได้นานต่อการชาร์จหนึ่งครั้ง รุ่นพี่คิดว่ามันน่าจะดีถ้าเราสามารถเปิด-ปิด BatSense ได้ สำหรับการสวิตช์ รุ่นพี่วาง Pushbutton Power Switch ระหว่าง Arduino กับวงจรชาร์จ สวิตช์กลไกธรรมดาอาจจะเล็กกว่า แต่เฮ้ย รุ่นพี่ชอบลัทชิ่งพาวเวอร์สวิตช์อ่ะ





แต่โชคร้าย ตอนกลางโปรเจกต์ดันพบว่าเราใช้ BLE module ในตัว Nano 33 BLE ส่งข้อมูลไม่ได้ง่ายๆ แบบที่คิด เลยต้องปรับแผนด่วนๆ และตัดสินใจใช้การส่งข้อมูลผ่านโมดูลบลูทูธภายนอกแทน (HC-05) รุ่นพี่เลยตั้งค่าโมดูล HC-05 สองตัวในโหมด Master/Slave แล้วต่อโมดูลละตัวเข้ากับพิน Serial1 ของ MCU แต่ละตัว สตริงที่เขียนไปยังพอร์ต Serial1 ของ BatSense ก็จะถูกอ่านได้ที่พอร์ต Serial1 ของ BatStation และในทางกลับกัน ค่อนข้างเนี๊ยบเลย ด้วยเหตุนี้ BatSense ก็ประกอบเสร็จสมบูรณ์แล้ว
TheBatStation
ต่อไปคือ BatStation ตัวนี้พี่ใช้ MCU เป็น MKR Wifi 1010 ที่มากับชุด IoT-Kit นะ เนื่องจาก BatStation จะตั้งอยู่กับที่ พี่เลยวางแผนใช้ Powerbank ธรรมดาเป็นแหล่งจ่ายไฟ และที่เหลือก็แค่ต่อโมดูล HC-05 เข้ากับขา Serial1 ของ MKR เท่านั้นเอง
แต่เนื่องจากขา Pin 13 กับ 14 ที่ว่านี้ มันเป็นขาที่ถูกออกแบบมาให้ควบคุมรีเลย์บน IoT-Carrier ด้วย พี่เลยตัดหัวขา (header) ออกซะเลย ไม่อยากได้ยินเสียงรีเลย์กระตุกตามทุกคำสั่ง Serial ที่ส่งมาจากบลูทูธหรอก งานนี้ต้องตัดไฟตั้งแต่ต้นลม!



ใครที่สังเกตดีๆ จะเห็นสองสิ่ง: นอกจากสาย RX/TX และไฟแล้ว ยังมีสายเส้นที่ห้าเชื่อมไปที่โมดูล HC-05 อีกด้วย สายนี้ต่อกับขา Reset ของ HC-05 นะ พี่วางแผนจะทำฟีเจอร์ประหยัดพลังงานโดยเปิดบลูทูธเฉพาะตอนจำเป็น แต่สุดท้ายก็ยังไม่ได้เอาไปใส่ในสคริปต์ เลยขอไม่พูดถึงมันละกัน และอย่างที่สอง เมื่อเทียบกับ Tutorial ทั่วไปเกี่ยวกับ HC-05 พี่ไม่ได้ใช้วงจรแบ่งแรงดัน (resistance division circuit) เพื่อทำ Level Shift ที่ขา RX/TX นะ เพราะไม่จำเป็น! ทั้ง MKR Wifi และ Nano BLE ต่างก็ทำงานที่ 3.3V เหมือนกัน ปลอดภัยหายห่วง
Nextstep,doestheBatSensefitintothehandle?
แต่ก่อนอื่น มาตรวจเช็คความพร้อมกันหน่อย:
- BatSense ประกอบเสร็จแล้ว, เช็ค!
- BatStation ประกอบเสร็จแล้ว, เช็ค!
- ทดสอบการทำงานเบื้องต้น (ชาร์จแบต, อ่านค่าจากเซนเซอร์ได้หมด, ส่งข้อมูลผ่าน Serial1 ได้, ฟังก์ชันบน IoT-Carrier ทำงานปกติ), เช็ค!
เหลืออย่างสุดท้ายคือติดตั้ง BatSense เข้าไปในไม้ตีเนี่ยแหละ สำหรับงานนี้ พี่ต้องขยายรูบนด้ามจับคอมโพสิตอย่างระมัดระวัง เพื่อให้มีที่ว่างพอสำหรับ BatSense



อย่างที่เห็นในรูป มันยัดเข้าไปในด้ามไม้ไม่สุดหรอก และพี่ก็ขยายรูเพิ่มอีกไม่ได้แล้ว เพราะเสี่ยงแตก และพี่ก็ไม่มีเวลา redesign BatSense ใหม่แล้วเหมือนกัน เลยต้องปล่อยให้มันโผล่ออกมาบางส่วนไปก่อน สู้งานนะน้อง!
Part IV. Make the Hardware work with Software
BatSense
มาดูซอฟต์แวร์ของ BatSense กัน มันสามารถแบ่งออกได้เป็นสามขั้นตอนหลัก:
- การเก็บข้อมูลจากเซนเซอร์ IMU
- การแปลงข้อมูลด้วย AHRS-Filter
- การส่งข้อมูลผ่านบลูทูธ
ก่อนพี่จะลงรายละเอียดแต่ละบล็อก มาอธิบายลำดับใน `loop()` กันสั้นๆ ก่อน `loop()` นี้แบ่งได้เป็นสองเฟส: ในเฟสแรก เราจะรอให้การวัดค่าเริ่มต้น (initialization) เสร็จ และในเฟสที่สอง เราจะทำการวัดค่าต่อไป ถ้าการเริ่มต้นเสร็จแล้ว
สำหรับการเริ่มต้น (initialization) ต้องถือไม้ตีให้นิ่งๆ ในมุมประมาณ 45° กับพื้น เราสามารถใช้ช่วงที่นิ่งสนิทนี้มาช่วยชดเชยความคลาดเคลื่อนระยะยาว (long-term drifts) ของ AHRS filter ได้ เพราะสำหรับไม้ตีที่อยู่นิ่งๆ เราสามารถคำนวณมุมจากค่าความเร่ง (accelerations) ได้เลย
หลังจาก init เสร็จ จะมีการหน่วงเวลา (delay) สั้นๆ เพื่อให้ผู้ตีมีเวลาเตรียมตัวเข้าที่พร้อมตี อย่าช็อตนะตัวนี้!
// เริ่มต้นถ้าไม้ยังอยู่นิ่งๆ และยังไม่ได้เริ่มวัดค่า
if (fabs(gx) + fabs(gy) + fabs(gz) <= threshold && init_measure == false)
{
// หามุมเริ่มต้นของไม้ (What is the starting Angle of bat)
phi = asin(-ax); // ในหน่วยเรเดียน
// เริ่มการวัดค่าเฉพาะตอนที่จับไม้ทำมุมประมาณ 45° เท่านั้น
if (phi >= 0.61 && phi <= 0.96)
{
i = 0; // รีเซ็ตตัวแปรสำหรับการวัดค่า
init_measure = true; // ตั้งค่าสถานะว่าเริ่มวัดแล้ว
delay(2000); // หยุดสักครู่เพื่อเตรียมตัวก่อนสวิง
}
}
หลังจากตั้งค่าการวัดแล้ว ก็ถึงเวลาวัดการเคลื่อนไหวของไม้เลยจ้า เพื่อให้ความล่าช้าระหว่างจุดวัดแต่ละจุดน้อยที่สุด พี่เลือกที่จะวัดค่าทั้งหมดของการสวิงหนึ่งครั้งในลูปเดียว แล้วเก็บข้อมูลแต่ละจุดลงในอาร์เรย์ การบันทึกข้อมูลใช้ while loop ซึ่งช่วยให้ควบคุมช่วงเวลาระหว่างจุดวัดได้แม่นยำขึ้น
// ถ้าเริ่มการวัดแล้ว (If initialized measure)
if (init_measure == true)
{
// วัดค่าในลูประหว่างรอบสวิง อ่านค่า IMU, กรองสัญญาณ, และเก็บข้อมูล
// ใช้ while loop เพื่อรอเงื่อนไขภายในลูป
while (i < buffersize)
{
microsNow = micros();
if (microsNow - microsPrevious > microsPerReading) //
{
// ตั้งค่าเวลาเริ่มต้นของจุดวัด "ล่าสุด"
microsPrevious = microsNow;
// วัดค่าต่างๆ ตรงนี้และเก็บลงในอาร์เรย์ data[i]
i++;
}
}
พอเก็บข้อมูลทุกจุดครบแล้ว ช่วงเวลาก็ไม่ใช่ประเด็นแล้ว ส่งข้อมูลออกไปได้แบบชิลๆ เลย สุดท้ายก็มีเวลารอสั้นๆ แปะท้ายไว้
// หลังจากเก็บข้อมูลทุกจุดแล้ว ให้หยุดการวัด
init_measure = false; // รีเซ็ต init_measure
// ส่งข้อมูลหลังการวัดเสร็จ วนลูปตาม buffersize
for (int i = 0; i < buffersize; i++)
{
// จัดเรียงและส่งสตริงข้อมูลออกไป (Compose and send Data String)
}