โปรเจกต์ Bluetooth Motion Controller สำหรับ Android Tablet
การติดตั้ง Motion control เพื่อช่วยให้ผู้พิการสามารถใช้งาน Android Tablet ได้
การติดตั้ง Motion control เพื่อช่วยให้ผู้พิการสามารถใช้งาน Android Tablet ได้
BleController
โปรเจกต์นี้ใช้ Adafruit Feather 32u4 Bluefruit LE เพื่อทำหน้าที่เป็น HID mouse สำหรับแท็บเล็ต Android สำหรับการป้อนข้อมูลการเคลื่อนไหว จะใช้ Adafruit LIS3DH Triple-Axis Accelerometer ติดไว้ที่เท้าของผู้ใช้ มีปุ่ม arcade แบบง่ายๆ สองปุ่มสำหรับคลิกซ้ายและสลับโหมด scroll
เมื่อหลายปีก่อน แม่ของผมได้รับการวินิจฉัยว่าเป็นโรค ALS ตั้งแต่นั้นมาเธอก็ค่อยๆ สูญเสียการทำงานของแขนขา เธอชอบเกม Android ชื่อ Fishdom และในตอนแรกเธอก็เล่นได้ปกติ แต่เมื่อเธอเริ่มขยับไม่ได้ เธอก็ไม่สามารถใช้หน้าจอสัมผัสเพื่อควบคุมเกมได้อีกต่อไป ในตอนนั้นผมได้สร้างคอนโทรลเลอร์แบบมีสายโดยใช้ arcade joystick เพื่อควบคุมการเคลื่อนที่ของเมาส์ และใช้ปุ่มสองปุ่มเดิมเพื่อทำหน้าที่คลิกและสลับโหมด scroll เมื่อเวลาผ่านไปเธอสูญเสียการทำงานของร่างกายมากขึ้นและ joystick ก็เริ่มใช้งานยากขึ้น เธอยังขยับเท้าได้ค่อนข้างดีเมื่อนั่งบนเก้าอี้ ผมจึงตัดสินใจลองใช้ accelerometer เพื่อควบคุมการเคลื่อนที่ของเมาส์ เมื่อผมทำส่วนนั้นสำเร็จ ผมก็ต้องการทำให้เป็นแบบไร้สายเชื่อมต่อกับแท็บเล็ต เพื่อที่เธอจะได้ไม่ต้องหยุดเล่นเพื่อชาร์จแท็บเล็ต
ในตอนนั้นโค้ดเริ่มมีขนาดใหญ่ขึ้นและผมไม่อยากทำอะไรพังในขณะที่พยายามเพิ่มฟีเจอร์ใหม่หรือปรับแต่งค่าต่างๆ ผมจึงคิดว่าถึงเวลาที่ต้องเรียนรู้ Git เสียที โค้ดส่วนใหญ่ถูกเขียนใน Arduino IDE จากนั้นผมก็ได้พบกับ VSCode และการทำงานร่วมกับ Git และได้เริ่มใช้มันในการพัฒนา
ด้วยความโชคดี โปรเจกต์นี้อาจช่วยให้ผู้พิการคนอื่นๆ ได้รับความบันเทิงบ้าง
ตัว Feather และปุ่มต่างๆ ถูกเก็บไว้ในกล่องกระดาษ ตัว accelerometer ถูกเก็บไว้ในกล่องเครื่องประดับที่มีหนังยางรัดผมติดอยู่ ซึ่งจะนำไปวางไว้บนเท้าของผู้ใช้ แต่ละกล่องมี Cat5 keystone ติดอยู่และเชื่อมต่อกันผ่านสาย Cat5 ในการสร้างครั้งต่อๆ ไป ผมอยากจะเปลี่ยนสาย Cat5 เป็นสายที่เล็กลง (ผมต้องการแค่ 4 เส้น) ในขณะที่ยังคงความสามารถในการถอด accelerometer ออกจากยูนิตหลักได้ง่าย




การต่อสายค่อนข้างง่าย คุณจะต้องมีสายสี่เส้นเชื่อมต่อ Feather เข้ากับ accelerometer (สองเส้นหากคุณต้องการให้ accelerometer มีแหล่งจ่ายไฟแยกต่างหาก) และสายสี่เส้นเพื่อเชื่อมต่อปุ่มเข้ากับ Feather (สองเส้นสำหรับแต่ละปุ่ม) ผมใช้สาย Cat5 ระหว่างกล่องหลัก (ที่มีปุ่มและ Feather) กับกล่องควบคุมเท้า (ที่มี accelerometer) เพราะผมมี keystone ที่สามารถยึดเข้ากับกล่องได้ และผมต้องการวิธีที่รวดเร็วในการถอด accelerometer ออกจากส่วนที่เหลือของระบบ
Accelerometer ทำงานคล้ายกับ d-pad ผมไม่ได้กังวลเรื่องปริมาณการเคลื่อนที่หรือมุมที่แน่นอน แค่ต้องการรู้ว่ามันเอียงซ้าย/ขวา หรือขึ้น/ลง นอกจากนี้ยังต้องมีตำแหน่งพักเพื่อให้ผู้ใช้สามารถรักษาตำแหน่ง accelerometer ไว้เพื่อหยุดการเคลื่อนที่ของเคอร์เซอร์ได้ ผู้ใช้ยังต้องการวิธีจำลองการลากนิ้ว (finger drag) ซึ่งทำได้ด้วยการใช้เพียงปุ่มหลักและขยับ accelerometer แต่มันช้า เพื่อให้การ scroll ดูฟอรัมยาวๆ และหน้าเว็บ (Facebook) ได้เร็วขึ้น จึงมีการเพิ่มโหมด scroll เข้ามา ขณะที่อยู่ในโหมดนี้ เคอร์เซอร์จะหยุดเคลื่อนที่และทั้งหน้าจะเลื่อนขึ้น/ลง หรือซ้าย/ขวา ตามการเคลื่อนไหวของ accelerometer
นอกเหนือจากไลบรารีของ Adafruit สำหรับ Feather และ accelerometer แล้ว ผมยังใช้ไลบรารี PushButton จากที่นี่ นี่เป็นไลบรารีที่สะดวกสำหรับการ debounce และตรวจจับการคลิก, ดับเบิลคลิก และการกดค้าง ผมเคย (และยังคง) มีปัญหาในการทำให้ Bluetooth เริ่มทำงานและเชื่อมต่อได้ BluefruitRoutines.h มาจากโปรเจกต์นี้ และรวมฟังก์ชัน initializeBluefruit() ที่ทำงานได้ยอดเยี่ยมมากจึงได้นำมาใส่ไว้ที่นี่ ผมอยากจะรวมสิ่งนี้เข้ากับไฟล์ ino หลักของผมในอนาคต
มีคำสั่ง #define หลายตัวที่ผมใช้เพื่อช่วยปรับแต่งโปรเจกต์ เนื่องจากความสามารถในการเคลื่อนไหวมีจำกัดและจะเสื่อมถอยลงเรื่อยๆ ผมจึงต้องการวิธีที่ง่ายในการปรับความเร็วเคอร์เซอร์, ขนาดของ deadzone และอื่นๆ
#define RANGE 7 //ระยะที่เมาส์เคลื่อนที่
#define RESPONSEDELAY 5 //ความถี่ในการรัน Loop
#define AVERAGEFACTOR 20 //จำนวนการจับค่าต่อการตรวจสอบการเคลื่อนไหวหนึ่งครั้ง
#define CALIBRATIONFACTOR 200 //จำนวนการจับค่าสำหรับการ Calibrate
#define DEADZONE 200 //ระยะก่อนที่จะบันทึกว่ามีการเคลื่อนไหว
#define
#define
ค่าคงที่ RANGE กำหนดระยะที่เมาส์เคลื่อนที่ในการสั่งงานแต่ละครั้ง (โดยพื้นฐานคือความเร็วของเคอร์เซอร์) RESPONSEDELAY คือการหน่วงเวลาเล็กน้อยที่ท้าย Loop ก่อนจะเริ่มทำงานใหม่ ซึ่งส่งผลต่อความเร็วโดยรวมของโปรแกรม (อาจไม่จำเป็นต้องใช้ในตอนนี้) ค่าคงที่ AVERAGEFACTOR กำหนดจำนวนจุดข้อมูลที่จะดึงจาก accelerometer ก่อนตรวจสอบการเคลื่อนไหว เนื่องจาก accelerometer มีสัญญาณรบกวน (noise) ค่อนข้างมาก จึงต้องดึงข้อมูลหลายครั้งและนำมาหาค่าเฉลี่ยก่อนระบุตำแหน่ง CALIBRATIONFACTOR คือจำนวนครั้งที่เราตรวจสอบ accelerometer เพื่อระบุตำแหน่งพัก (ดูฟังก์ชัน calibration()) DEADZONE คือเกณฑ์ที่ accelerometer ต้องข้ามผ่านก่อนที่เราจะบันทึกการเคลื่อนไหว สิ่งนี้จำเป็นเพื่อให้เมื่อผู้ใช้อยู่ในท่าพัก เคอร์เซอร์จะหยุดอยู่ที่เดิมแทนที่จะกระโดดไปมา คุณสามารถตั้งค่า MAINBUTTONPIN และ SCROLLBUTTONPIN เป็น Pins ใดก็ได้ที่คุณต้องการใช้สำหรับปุ่มเหล่านั้น
ผมพยายามลดการใช้ Global Object และ Variable ให้น้อยที่สุดเพื่อหลีกเลี่ยงการเปลี่ยนแปลงที่ไม่ตั้งใจในโค้ด วิธีการทำงานของ Arduino loop() ทำให้ต้องพิจารณาเรื่องนี้
PushButton scrollButton(5); //ปุ่ม Scroll
PushButton mainButton(12); //ปุ่มหลัก
Adafruit_LIS3DH lis = Adafruit_LIS3DH(); //สร้าง acc object
int xCalibrated = 0; //ค่าพื้นฐานสำหรับแกน x
int yCalibrated = 0; //ค่าพื้นฐานสำหรับแกน y
int xDistance = 0; //การเคลื่อนที่ของแกน x
int yDistance = 0; //การเคลื่อนที่ของแกน y
bool bolScroll = false; //โหมด scroll เปิดใช้งานอยู่หรือไม่
Adafruit_BluefruitLE_SPI ble(BLUEFRUIT_SPI_CS, BLUEFRUIT_SPI_IRQ, BLUEFRUIT_SPI_RST); //อ็อบเจกต์ Bluetooth
มีสองปุ่มให้ผู้ใช้สั่งงานคือ scrollButton และ mainButton โดย mainButton ทำหน้าที่เหมือนการคลิกซ้ายของเมาส์ ในขณะที่ scrollButton ใช้สำหรับสลับเข้าและออกจากโหมด scroll การใช้สวิตช์ก็น่าจะทำงานได้ดีสำหรับ scrollButton แต่ผู้ใช้ของผมไม่มีความแม่นยำพอที่จะใช้งานสวิตช์ได้ lis คืออ็อบเจกต์ของ accelerometer ซึ่งใช้เพื่อดึงข้อมูลการเคลื่อนไหวทั้งหมดจาก accelerometer ตัวแปร xCalibrated และ yCalibrated ใช้เพื่อเก็บค่าของ accelerometer ในขณะพัก (ไม่มีการเคลื่อนที่ของเคอร์เซอร์) ตัวแปร xDistance และ yDistance คือปริมาณการเคลื่อนที่ในแต่ละแกน bolScroll จะติดตามว่าผู้ใช้อยู่ในโหมด scroll หรือไม่ สุดท้ายอ็อบเจกต์ ble ถูกสร้างขึ้นใน BluefruitRoutines.h และใช้สำหรับส่งคำสั่งไปยังแท็บเล็ตจริงๆ
เพื่อให้การปรับเปลี่ยนในอนาคตทำได้ง่ายขึ้นเมื่อการเคลื่อนไหวถดถอยลงและต้องหาวิธีใหม่ๆ ในการควบคุมเคอร์เซอร์ ผมพยายามเขียนโค้ดให้เป็นแบบ modular และใช้ฟังก์ชันเมื่อทำได้
int averageX();
int averageY();
void calibrate();
int checkXmovement();
int checkYmovement();
String convertMovement(int x, int y);
ฟังก์ชัน averageX() และ averageY() ดึงข้อมูลจากอ็อบเจกต์ lis หลายครั้ง (AVERAGEFACTOR) แล้วหาค่าเฉลี่ยเพื่อระบุตำแหน่งของ accelerometer ซึ่งทำเพื่อลดสัญญาณรบกวนของข้อมูล accelerometer ฟังก์ชัน calibrate() ดึงข้อมูลจากอ็อบเจกต์ lis เป็นจำนวน CALIBRATIONFACTOR ครั้ง และหาค่าเฉลี่ยทั้งหมดเพื่อระบุตำแหน่งพักสำหรับ Sensor ฟังก์ชันนี้จะถูกเรียกใน setup() และสามารถเรียกใช้ซ้ำได้โดยการกดปุ่ม mainButton และ scrollButton พร้อมกัน ผู้ใช้อาจต้องการปรับตั้งค่าใหม่หากเคอร์เซอร์เริ่มเลื่อนเองในขณะที่อยู่ในตำแหน่งพัก ฟังก์ชัน checkXmovement() และ checkYmovement() จะถูกเรียกเพื่อตรวจสอบว่ามีการเคลื่อนไหวใน accelerometer มากพอที่จะสั่งให้เคอร์เซอร์เลื่อนหรือไม่ โดยการเรียกฟังก์ชัน averageX() หรือ averageY() เพื่อหาตำแหน่งปัจจุบันของ accelerometer จากนั้นจึงนำไปเปรียบเทียบกับ xCalibrated หรือ yCalibrated (ตำแหน่งพัก) และหากความแตกต่างระหว่างตำแหน่งใหม่และตำแหน่งที่ Calibrate ไว้มากกว่า DEADZONE มันจะคืนค่า 1 หรือ -1 ตามทิศทางการเคลื่อนที่ หรือคืนค่า 0 หากไม่มีการเคลื่อนไหว ใน loop() หลัก ค่าที่ได้จะถูกนำไปคูณกับ RANGE เพื่อกำหนดระยะการเคลื่อนที่ของเคอร์เซอร์ สุดท้ายฟังก์ชัน convertMovement() จะนำระยะการเคลื่อนที่ที่ต้องการสำหรับเคอร์เซอร์มาแปลงเป็น string เพื่อให้อ็อบเจกต์ Bluetooth (ble) นำไปส่งให้แท็บเล็ตเพื่อให้เคอร์เซอร์เลื่อนจริงๆ
ใน setup มีงานไม่มากนัก ขั้นแรกคือการตั้งค่า Pins สำหรับปุ่มต่างๆ เป็น Pull up input ปุ่มถูกตั้งค่าให้ใช้ logic แบบ active low สำหรับไลบรารี PushButton ตัว accelerometer จะถูกเริ่มต้นและตั้งค่าช่วงความไว จากนั้นจึงทำการ Calibrate และสุดท้ายคือการเริ่มต้นการทำงานของอ็อบเจกต์ Bluetooth
//กำหนด Pins ของปุ่ม
pinMode(5, INPUT_PULLUP);
pinMode(12, INPUT_PULLUP);
//ตั้งค่าปุ่มเป็น active low
scrollButton.setActiveLogic(LOW);
mainButton.setActiveLogic(LOW);
//เริ่มการทำงานของ acc
lis.begin(0x18);
//ตั้งค่า RANGE สำหรับ Acc
lis.setRange(LIS3DH_RANGE_16_G); // 2, 4, 8 หรือ 16 G!
//Calibrate แกน X และ Y
calibrate();
//เริ่มการทำงานของ bluetooth
initializeBluefruit();
Loop หลักประกอบด้วยขั้นตอนต่างๆ ขั้นแรกคือการตรวจสอบสถานะของปุ่ม หากมีการกดปุ่มใดปุ่มหนึ่งหรือมากกว่านั้น มันจะทำงานตามคำสั่งที่เกี่ยวข้อง จากนั้นจึงตรวจสอบการเคลื่อนไหวและหากพบการเคลื่อนไหวก็จะเลื่อนเคอร์เซอร์ ทำซ้ำขั้นตอนเดิมไปเรื่อยๆ
//อัปเดตสถานะปุ่ม
scrollButton.update();
mainButton.update();
//ตรวจสอบเหตุการณ์ของปุ่ม
if (scrollButton.isActive() && mainButton.isActive()) //คลิกทั้งสองปุ่มเพื่อ Calibrate
{
calibrate();
}
else
{
if (scrollButton.isClicked()) //ตรวจสอบการเปลี่ยนโหมด scroll
{
bolScroll = !bolScroll;
}
if (mainButton.isActive()) //ตรวจสอบปุ่มหลัก
{
ble.println("AT+BleHidMouseButton=L"); //กดค้างไว้โดยยังไม่ปล่อยเพื่อให้ลากเมาส์ได้
}
if (mainButton.isReleased())
{
ble.println("AT+BleHidMouseButton=0"); //ปล่อยปุ่ม
}
}
//ฟังก์ชัน checkmovement คืนค่า 0, 1 หรือ -1 จากนั้นคูณด้วย RANGE
int xDistance = checkXmovement() * RANGE;
int yDistance = checkYmovement() * RANGE;
//ถ้าค่าไม่เป็นศูนย์ให้เลื่อนเคอร์เซอร์
if ((xDistance != 0) || (yDistance != 0))
{
//ถ้าไม่อยู่ในโหมด scroll
if (bolScroll == false)
{
String distance = convertMovement(xDistance,yDistance); //แปลงการเคลื่อนที่เป็น string
ble.print("AT+BleHidMouseMove="); //เลื่อนเมาส์
ble.println(distance);
}
//ถ้าอยู่ในโหมด scroll
else
{
String distance = convertMovement(-yDistance/2,-xDistance/2); //แปลงเป็น string แบบสลับแกนสำหรับการ scroll
ble.print("AT+BleHidMouseMove=0,0,"); //เลื่อนลูกกลิ้งเมาส์
ble.println(distance);
delay(150);
}
}
delay(RESPONSEDELAY);
หลังจากอัปเดตสถานะปุ่มแล้ว โค้ดจะตรวจสอบการกดทั้งสองปุ่มพร้อมกัน และถ้าไม่ใช่ก็จะตรวจสอบทีละปุ่ม สิ่งนี้ทำเพื่อป้องกันการคลิกหรือการเปลี่ยนโหมด scroll โดยไม่ตั้งใจ วิธีนี้อาจไม่ได้ผลเสมอไปขึ้นอยู่กับว่าผู้ใช้กดและปล่อยทั้งสองปุ่มได้ทันท่วงทีหรือไม่ แต่ส่วนใหญ่ก็ทำงานได้ดี ในอนาคตผมต้องการปรับปรุงส่วนนี้ วิธีที่ Feather ส่งคำสั่ง Bluetooth คือผ่าน string ที่ขึ้นต้นด้วย "AT+" ตามด้วยสิ่งที่คุณต้องการทำ เอกสารอ้างอิงเกี่ยวกับเรื่องนี้สามารถดูได้ที่นี่ ผมกำลังหาวิธีที่ดีกว่าในการทำเช่นนี้ ผมเคยเห็นบางโปรแกรมใช้ ble.atcommand() แต่ผมไม่พบเอกสารว่ามันต่างจาก ble.print() อย่างไร นอกจากนี้ ข้อมูลชุดสุดท้ายที่คุณส่งผ่าน Bluetooth ควรใช้คำสั่ง ble.println() ผมเคยเจออาการค้างและพฤติกรรมที่ผิดปกติเมื่อผมใช้เพียง ble.print()
มีการปรับปรุงบางอย่างที่ผมอยากทำกับโค้ดและการตั้งค่าทั่วไป
calibrate() วิธีที่ Feather ส่งคำสั่งเลื่อนเมาส์คือการเลื่อนแบบสัมพัทธ์กับตำแหน่งที่เคอร์เซอร์อยู่ในปัจจุบัน ดังนั้นผมจึงไม่สามารถตั้งค่าให้มันไปอยู่ที่จุดศูนย์กลางตรงๆ ได้ เคอร์เซอร์สามารถตั้งศูนย์ได้โดยการรีเซ็ตอ็อบเจกต์ Bluetooth แต่ผมไม่อยากทำแบบนั้น ขณะนี้ผมกำลังพัฒนาเรื่องนี้อยู่ใน branch ชื่อ centerนี่เป็นโปรเจกต์ที่ใหญ่ที่สุดของผมจนถึงตอนนี้ และผมเป็นเพียงผู้ใช้งานอดิเรก ดังนั้นผมแน่ใจว่ามันอาจไม่ได้ทำตามแนวทางปฏิบัติที่ดีที่สุดเสมอไป หากคุณเห็นอะไรบางอย่าง โปรดแจ้งให้ผมทราบเพื่อที่ผมจะได้ปรับปรุงโค้ดของผม
สนับสนุนเพื่อรับ Source Code หรือแอปพลิเคชันสำหรับโปรเจกต์นี้
ประเมิน Project
เอาฟอร์มยาวออกจากท้ายหน้า Project แล้ว เหลือเป็นปุ่มให้กดไปกรอกหน้าเดียว ตัวใหญ่ เว้นบรรทัดเยอะ อ่านง่ายกว่า
รีวิวจากคนใช้งานจริง
ถ้าเคยสั่งงาน เคยอ่านหน้านี้แล้วได้ประโยชน์ หรือมีข้อเสนอแนะ ฝากรีวิวไว้ได้เลย
ยังไม่มีรีวิวบนหน้านี้ ถ้าเคยใช้งานหรือมีข้อเสนอแนะ เขียนเป็นคนแรกได้เลย