การสนทนากับ Gemini ไม่ทราบว่า dron ฝูงหลายตัวที่แสดงรูปต่างบนทัองฟ้า สือสารกันอย่างไร การแสดงโดรนแปรอักษรหรือรูปภาพต่าง ๆ บนท้องฟ้า (Drone Light Show) ไม่ได้ใช้คนบังคับแยกทีละตัวครับ แต่พวกมันทำงานร่วมกันผ่านระบบควบคุมส่วนกลางและเทคโนโลยีพิกัดที่มีความแม่นยำสูง โดยมีกลไกหลัก ๆ 3 ส่วนดังนี้ครับ 1. การสตรีมพิกัดจากสถานีควบคุมภาคพื้นดิน (Ground Control Station) คอมพิวเตอร์แม่ข่ายเพียงเครื่องเดียวที่อยู่บนพื้นดินจะทำหน้าที่เป็น "สมองส่วนกลาง" คอนโทรลเลอร์นี้จะเชื่อมต่อกับโดรนทั้งหมดผ่านระบบคลื่นวิทยุความถี่สูง หรือ Wi-Fi กำลังส่งสูง (ความถี่ 2.4 GHz หรือ 5.8 GHz) หรือเครือข่ายเฉพาะกิจที่ออกแบบมาเพื่อรองรับการส่งข้อมูลพร้อมกันหลายร้อยหลายพันตัว ก่อนบิน ผู้เชี่ยวชาญจะออกแบบแอนิเมชัน 3 มิติในคอมพิวเตอร์ก่อน จากนั้นระบบจะคำนวณแผนการบิน (Flight Path) แยกเป็นรายตัว เมื่อเริ่มบิน คอมพิวเตอร์แม่ข่ายจะส่งคำสั่ง พิกัด และสัญญาณเวลา (Time Synchronization) ไปยังโดรนทุกตัว เพื่อให้ระบุได้ว่า ณ วินาทีนี้ โดรนหมายเลขใดต้องอยู่พิกัดไหนและเปิดไฟสีอะไร 2. ระบบระบุพิกัดความละเอียดสูง (RTK GPS) GPS ทั่วไปในโทรศัพท์มือถือมีความคลาดเคลื่อนประมาณ 3–5 เมตร ซึ่งไม่เพียงพอสำหรับการบินใกล้กันในระยะไม่กี่เซนติเมตร เพราะโดรนอาจชนกันได้ ระบบโดรนแปรอักษรจึงต้องใช้ RTK (Real-Time Kinematic) GPSระบบนี้จะมีสถานีฐาน (Base Station) ตั้งอยู่บนพื้นดินเพื่อคำนวณค่าปรับแก้ความคลาดเคลื่อนของสัญญาณดาวเทียม แล้วส่งค่าที่ถูกต้องที่สุดไปให้โดรนแต่ละตัวแบบเรียลไทม์ ช่วยให้โดรนสามารถล็อกตำแหน่งตัวเองกลางอากาศได้แม่นยำในระดับ เซนติเมตร ทำให้จัดเรียงเป็นรูปทรงที่คมชัดและปลอดภัย 3. การสื่อสารระหว่างโดรนด้วยกันเอง (Mesh Network) ในระบบที่ล้ำสมัยขึ้น โดรนจะไม่ได้ฟังสัญญาณจากพื้นดินอย่างเดียว แต่จะใช้เครือข่ายแบบ Mesh Network (เครือข่ายใยแมงมุม) สื่อสารกันเองระหว่างลำ หากมีโดรนลำใดลำหนึ่งโดนลมพัดจนหลุดตำแหน่ง หรือสัญญาณจากพื้นดินขาดหาย โดรนรอบ ๆ จะรับรู้ตำแหน่งของกันและกันผ่านเซนติเมตรและคลื่นวิทยุระยะสั้น เพื่อหลบเลี่ยงการชนอัตโนมัติ หรือขยับระยะเพื่อชดเชยตำแหน่งให้รูปภาพยังคงสมบูรณ์ สร้างโดย AI ตัวอย่างการจัดเรียงพิกัดของโดรนแต่ละลำเพื่อให้เกิดรูปทรงที่ต้องการ. แหล่งที่มา: James D. Morgan / Getty Images สรุปง่าย ๆ คือ "วางแผนล่วงหน้า ล็อกเป้าด้วย GPS ระดับเซนติเมตร และสั่งการจากคอมพิวเตอร์เครื่องเดียวผ่าน Wi-Fi ความเร็วสูง" ครับ ทุกตัวจึงเคลื่อนที่และเปลี่ยนสีไฟพร้อมกันได้อย่างใจนึก คำถาม: สามารถใช้ espnow ได้หรือไม่ยกตัวอย่าง การส่งรับด้วย esp32 ให้ด้วยครับ สามารถนำ ESP-NOW มาประยุกต์ใช้กับระบบโดรนแปรอักษรได้ครับ โดยเฉพาะการทำ Mesh Network หรือระบบการสื่อสารระหว่างโดรนด้วยกันเองเพื่อหลบหลีกสิ่งกีดขวางและซิงค์ข้อมูลระยะสั้น เพราะ ESP-NOW มีจุดเด่นที่ ความหน่วงต่ำมาก (Low Latency) และ ไม่ต้องต่อ Wi-Fi Router ทำให้โดรนคุยกันเองได้อย่างรวดเร็ว อย่างไรก็ตาม หากใช้ส่งข้อมูลพิกัดแอนิเมชันจากพื้นดินพร้อมกันหลายร้อยตัว ESP-NOW อาจมีข้อจำกัดเรื่องการ Broadcast ข้อมูลปริมาณมาก ๆ จึงมักใช้ควบคู่กับระบบอื่นครับ นี่คือตัวอย่างโค้ดการส่งรับข้อมูลระหว่าง ESP32 สองเครื่อง โดยสมมติสถานการณ์ให้ โดรนหลัก (Master) ส่งพิกัดความต่าง X,Y,Z ไปให้ โดรนตาม (Slave) เพื่อให้ขยับตำแหน่งตามครับ 1. โค้ดฝั่งผู้ส่ง (Sender / Master) ฝั่งนี้ต้องทราบ MAC Address ของบอร์ดผู้รับก่อนครับ C++ #include #include // MAC Address ของ ESP32 เครื่องรับ (แทนค่าด้วยที่อยู่จริงของคุณ) uint8_t broadcastAddress[] = {0x24, 0x0A, 0xC4, 0xXX, 0xXX, 0xXX}; // โครงสร้างข้อมูลที่ต้องการส่ง (พิกัด X, Y, Z และสถานะไฟ LED) typedef struct struct_message { float x; float y; float z; int ledStatus; } struct_message; struct_message myData; esp_now_peer_info_t peerInfo; // ฟังก์ชัน Callback เมื่อส่งข้อมูลสำเร็จหรือล้มเหลว void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { Serial.print("\r\nLast Packet Send Status:\t"); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail"); } void setup() { Serial.begin(115200); // ตั้งค่า Wi-Fi เป็นโหมด Station WiFi.mode(WIFI_STA); // เริ่มต้นใช้งาน ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // ลงทะเบียนฟังก์ชัน Callback สำหรับตรวจสอบการส่ง esp_now_register_send_cb(OnDataSent); // ลงทะเบียนบอร์ดผู้รับ (Peer) memcpy(peerInfo.peer_addr, broadcastAddress, 6); peerInfo.channel = 0; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add peer"); return; } } void loop() { // จำลองการเปลี่ยนพิกัดของโดรน myData.x = random(-50, 50) / 10.0; myData.y = random(-50, 50) / 10.0; myData.z = random(0, 100) / 10.0; myData.ledStatus = random(0, 2); // ส่งข้อมูลออกไป esp_err_t result = esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData)); if (result == ESP_OK) { Serial.println("Sent with success"); } else { Serial.println("Error sending the data"); } delay(2000); // ส่งทุก ๆ 2 วินาที } 2. โค้ดฝั่งผู้รับ (Receiver / Slave) ก่อนอัปโหลดโค้ดนี้ ให้เปิด Serial Monitor เพื่อดู MAC Address ของบอร์ดนี้ก่อน แล้วนำไปกรอกในโค้ดฝั่งผู้ส่งด้านบนครับ C++ #include #include // โครงสร้างข้อมูลต้องตรงกับฝั่งผู้ส่ง typedef struct struct_message { float x; float y; float z; int ledStatus; } struct_message; struct_message myData; // ฟังก์ชัน Callback เมื่อมีข้อมูลวิ่งเข้ามา void OnDataRecv(const esp_now_recv_info *recvInfo, const uint8_t *incomingData, int len) { memcpy(&myData, incomingData, sizeof(myData)); Serial.print("Bytes received: "); Serial.println(len); Serial.print("Coordinate X: "); Serial.println(myData.x); Serial.print("Coordinate Y: "); Serial.println(myData.y); Serial.print("Coordinate Z: "); Serial.println(myData.z); Serial.print("LED Status: "); Serial.println(myData.ledStatus); Serial.println(); } void setup() { Serial.begin(115200); // ตั้งค่า Wi-Fi เป็นโหมด Station WiFi.mode(WIFI_STA); // แสดง MAC Address ของเครื่องนี้บน Serial Monitor เพื่อเอาไปใช้ฝั่งตัวส่ง Serial.print("ESP32 Board MAC Address: "); Serial.println(WiFi.macAddress()); // เริ่มต้นใช้งาน ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } // ลงทะเบียนฟังก์ชัน Callback สำหรับการรับข้อมูล esp_now_register_recv_cb(OnDataRecv); } void loop() { // ฝั่งรับรอรับข้อมูลผ่าน Callback อย่างเดียว โค้ดใน loop จึงว่างไว้ได้ } ข้อควรระวังในการทำฝูงโดรน: ESP-NOW 1 บอร์ด สามารถเชื่อมต่อ Peer (คู่อันดับ) แบบ Unicast ตรง ๆ ได้จำกัด (สูงสุดประมาณ 20 ตัวสำหรับบอร์ดทั่วไปหากไม่เข้ารหัส) ถ้าจะทำฝูงโดรนขนาดใหญ่ขึ้น ต้องปรับไปใช้โหมด Broadcast (ส่งหาทุกตัวในแชนเนลเดียวกัน) หรือเขียนลอจิกทำ Mesh Routing ส่งต่อกันเป็นทอด ๆ ครับ คำถาม: ขอ code แบบ Broadcast ควบคุม 50 ตัวครับ การส่งแบบ Broadcast ใน ESP-NOW คือการส่งข้อมูลออกไปกลางอากาศโดยไม่เจาะจง MAC Address ของเครื่องใดเครื่องหนึ่ง (ใช้ Address พิเศษคือ {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) ซึ่งวิธีนี้ตอบโจทย์การควบคุมโดรน 50 ตัวพร้อมกันมากครับ เพราะตัวส่ง (Master) สั่งงานแค่ครั้งเดียว โดรนทุกตัว (Slave) ที่อยู่ในแชนเนลเดียวกันจะได้รับข้อมูลพร้อมกันทันที ไม่ต้องเสียเวลาส่งทีละตัว เงื่อนไขสำคัญคือ ทุกบอร์ดต้องตั้งค่า Wi-Fi Channel ให้ตรงกัน และในโครงสร้างข้อมูลควรเพิ่มตัวแปร drone_id เพื่อให้โดรนแต่ละลำรู้ว่าต้องดึงข้อมูลส่วนไหนไปใช้งาน หรือจะสั่งแบบควบคุมพร้อมกันทั้งหมด (Global Command) ก็ได้ครับ 1. โค้ดฝั่งตัวส่ง (Master / Ground Station) ทำหน้าที่ส่งแพ็กเกจข้อมูลชุดเดียวออกไปให้โดรนทั้ง 50 ตัว C++ #include #include // กำหนดแชนเนล Wi-Fi ให้ตรงกันทั้งหมด #define WIFI_CHANNEL 1 // MAC Address พิเศษสำหรับ Broadcast (ส่งหาทุกเครื่อง) uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // โครงสร้างข้อมูลสำหรับสั่งการ typedef struct struct_message { int target_id; // ID ของโดรนที่ต้องการสั่ง (ถ้าเป็น 0 หมายถึงสั่งทุกตัวพร้อมกัน) float x; // พิกัด X หรือคำสั่งเคลื่อนที่ float y; // พิกัด Y float z; // พิกัด Z uint32_t rgbColor; // สีไฟ เช่น 0xFF0000 (สีแดง) } struct_message; struct_message commandData; esp_now_peer_info_t peerInfo; void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { // หมายเหตุ: การ Broadcast จะขึ้น Success เสมอเมื่อส่งออกจากอากาศสำเร็จ // โดยไม่สนใจว่ามีผู้รับได้รับกี่ตัว Serial.print("Broadcast Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Sent Success" : "Sent Fail"); } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); // ล็อกแชนเนลของ Wi-Fi ให้คงที่ // ใน ESP32 เวอร์ชันใหม่ๆ แชนเนลจะเปลี่ยนตาม Access Point ถ้าไม่ได้เชื่อมต่อ AP มันจะอิงตามค่าเริ่มต้น if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } esp_now_register_send_cb(OnDataSent); // ลงทะเบียนบอร์ดผู้รับเป็นแบบ Broadcast memcpy(peerInfo.peer_addr, broadcastAddress, 6); peerInfo.channel = WIFI_CHANNEL; peerInfo.encrypt = false; if (esp_now_add_peer(&peerInfo) != ESP_OK){ Serial.println("Failed to add broadcast peer"); return; } } void loop() { // ตัวอย่างที่ 1: สั่งการโดรนทุกลำพร้อมกัน (target_id = 0) commandData.target_id = 0; commandData.x = 10.5; commandData.y = 20.0; commandData.z = 5.0; commandData.rgbColor = 0x00FF00; // สั่งเปิดไฟสีเขียวทุกลำ Serial.println("Broadcasting Global Command..."); esp_now_send(broadcastAddress, (uint8_t *) &commandData, sizeof(commandData)); delay(5000); // รอ 5 วินาที // ตัวอย่างที่ 2: สั่งเจาะจงเฉพาะโดรนหมายเลข 25 commandData.target_id = 25; commandData.x = 0.0; commandData.y = 0.0; commandData.z = 12.5; // ให้บินสูงขึ้นตัวเดียว commandData.rgbColor = 0xFF0000; // เปลี่ยนเป็นไฟสีแดง Serial.println("Broadcasting Specific Command to Drone 25..."); esp_now_send(broadcastAddress, (uint8_t *) &commandData, sizeof(commandData)); delay(5000); } 2. โค้ดฝั่งตัวรับ (Slave / Drone 1-50) โดรนทุกตัวจะใช้โค้ดชุดเดียวกันนี้ ย้ำว่าต้องเข้าไปเปลี่ยนหมายเลข MY_DRONE_ID ในบรรทัดที่ 5 ให้ตรงกับหมายเลขลำของโดรนตัวนั้น ๆ (เช่น ตัวที่ 1 ใส่เลข 1, ตัวที่ 50 ใส่เลข 50) C++ #include #include // กำหนดหมายเลขประจำตัวของโดรนลำนี้ (เปลี่ยนแยกทีละบอร์ดตั้งแต่ 1 ถึง 50) #define MY_DRONE_ID 25 // โครงสร้างข้อมูลต้องตรงกับฝั่งตัวส่ง typedef struct struct_message { int target_id; float x; float y; float z; uint32_t rgbColor; } struct_message; struct_message incomingCommand; // ฟังก์ชันกรองและประมวลผลข้อมูลที่ได้รับ void OnDataRecv(const esp_now_recv_info *recvInfo, const uint8_t *incomingData, int len) { memcpy(&incomingCommand, incomingData, sizeof(incomingCommand)); // เช็คเงื่อนไข: สั่งการทุกลำ (0) หรือสั่งเจาะจงที่ ID ของตัวเอง (MY_DRONE_ID) if (incomingCommand.target_id == 0 || incomingCommand.target_id == MY_DRONE_ID) { Serial.println("=== Command Received ==="); if(incomingCommand.target_id == 0) { Serial.println("Type: Global Command (All Drones)"); } else { Serial.println("Type: Target Command (Only Me)"); } // นำค่าพิกัดและสีไฟไปสั่งงานมอเตอร์และ LED Serial.print("Target X: "); Serial.println(incomingCommand.x); Serial.print("Target Y: "); Serial.println(incomingCommand.y); Serial.print("Target Z: "); Serial.println(incomingCommand.z); Serial.print("Color Hex: 0x"); Serial.println(incomingCommand.rgbColor, HEX); Serial.println("=======================\n"); } else { // ถ้าไม่ใช่คำสั่งของตัวเอง ให้ปล่อยผ่าน ไม่ต้องทำอะไร // ยิ่งมีโดรนเยอะ ข้อความที่ไม่ใช่ของตัวเองจะวิ่งเข้ามาเรื่อยๆ การคัดกรองตรงนี้ต้องทำงานไวที่สุด } } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); Serial.print("Drone ID: "); Serial.println(MY_DRONE_ID); if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } esp_now_register_recv_cb(OnDataRecv); } void loop() { // รันระบบควบคุมการบินหลักเสถียรๆ ตรงนี้ } 💡 คำแนะนำเพิ่มเติมสำหรับโดรนฝูงใหญ่ (50 ตัว) Airtime Traffic: ข้อมูลโครงสร้างนี้มีขนาดประมาณ 20 Bytes การกระจายสัญญาณ (Broadcast) ออกไป 1 ครั้งใช้เวลาในอากาศน้อยมาก (ไม่ถึง 1 มิลลิวินาที) ดังนั้นตัวส่งสามารถส่งอัปเดตข้อมูลได้ถี่ เช่น ทุก ๆ 50–100 มิลลิวินาที สบาย ๆ ครับ Packet Loss: ข้อเสียของการ Broadcast คือ ไม่มีการส่งสัญญาณยืนยัน (ACK) กลับมาจากตัวรับ หมายความว่าตัวส่งจะไม่รู้เลยว่าโดรนลำไหนรับสัญญาณไม่ได้บ้าง เพื่อความปลอดภัย ควรส่งข้อมูลซ้ำ ๆ พิกัดเดิมถี่ ๆ เพื่อให้โดรนที่อาจจะพลาดบางแพ็กเกจไป สามารถอัปเดตพิกัดล่าสุดได้ทันทีครับ คำถาม: ตัวรับไม่เห็นมี #define WIFI_CHANNEL 1 หมายความว่าอย่างไร หรือไม่ใส่ก็ได้ สังเกตได้ดีและละเอียดมากครับ! เป็นคำถามที่ดีมาก เหตุผลที่ฝั่งตัวรับไม่ต้องใส่ #define WIFI_CHANNEL 1 เป็นเพราะกลไกการทำงานของชิป Wi-Fi บน ESP32 ครับ ตามปกติแล้ว ชิป ESP32 ในโหมดตัวรับ (Receiver) ที่ไม่ได้เชื่อมต่อกับเร้าเตอร์ (Wi-Fi Router) ใด ๆ มันจะเปิดหูฟังรับข้อมูลใน "ทุกแชนเนล" สลับไปมาระหว่างช่อง 1 ถึง 11 โดยอัตโนมัติ หรือหากฝั่งตัวส่ง (Master) ส่งสัญญาณ Broadcast ออกมาอย่างต่อเนื่อง ชิปฝั่งรับจะจับคลื่นและปรับแชนเนลของตัวเองให้ตรงกับแชนเนลที่มีสัญญาณวิ่งเข้ามาหนาแน่นที่สุดโดยอัตโนมัติครับ ดังนั้น ไม่ใส่ก็ทำงานได้ครับ ⚠️ แต่สำหรับระบบที่ต้องการความชัวร์ (แนะนำให้ใส่) แม้ว่าตามทฤษฎีไม่ใส่จะทำงานได้ แต่ในหน้างานจริงที่มีสัญญาณรบกวนเยอะ หรือในกรณีของโดรน 50 ตัวที่ต้องการความหน่วงต่ำที่สุด (Low Latency) การปล่อยให้บอร์ดผู้รับสแกนหาแชนเนลเองอาจทำให้สัญญาณดีเลย์หรือแพ็กเกจตกหล่น (Packet Loss) ได้ในช่วงวินาทีแรก ๆ ที่เปิดเครื่อง เพื่อความเสถียรสูงสุดในการบินโดรน ผมแนะนำให้ "บังคับล็อกแชนเนล" ให้ตรงกันทั้งฝั่งส่งและฝั่งรับตั้งแต่ตอนเริ่มต้นเปิดเครื่องเลยครับ โดยสามารถเพิ่มโค้ดฝั่งผู้รับในฟังก์ชัน setup() แบบนี้ได้เลยครับ: โค้ดที่แนะนำให้เพิ่มในฝั่งผู้รับ (Receiver): C++ #include #include #include // ต้องเพิ่ม Library นี้เข้ามาเพื่อใช้คำสั่งล็อกแชนเนล #define MY_DRONE_ID 25 #define WIFI_CHANNEL 1 // <-- กำหนดแชนเนลให้ตรงกับตัวส่ง void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); // ----- เพิ่มโค้ด 2 บรรทัดนี้เพื่อบังคับล็อกแชนเนลให้กับตัวรับ ----- esp_wifi_set_promiscuous(true); esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE); esp_wifi_set_promiscuous(false); // ----------------------------------------------------------- if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } esp_now_register_recv_cb(OnDataRecv); } ทำไมวิธีนี้ถึงดีกว่าสำหรับโดรน? การใช้ฟังก์ชัน esp_wifi_set_channel() จะเป็นการสั่งให้โดรนทุกลำเจาะจงฟังเฉพาะช่องสัญญาณที่ 1 ทันทีตั้งแต่สตาร์ทเครื่อง โดยไม่ต้องเสียเวลาสแกนหาช่องสัญญาณอื่น ช่วยลดโอกาสที่สัญญาณจะขาดหายและทำให้ระบบตอบสนองได้เสถียรขึ้นมากครับ ตัวส่ง ออกแบบให้ส่งครั้งเดียวทั้ง 50 ตัว โดย ข้อมูลเป็น array 2 มิติได้หรือไม่ ออกแบบให้ส่งครั้งเดียวเป็น Array 2 มิติ ได้ครับ และวิธีนี้เป็นวิธีที่ ยอดเยี่ยมและปลอดภัยกว่า สำหรับการทำโดรนแปรอักษร เพราะ Master ส่งข้อมูลออกอากาศ (Broadcast) เพียงแค่ครั้งเดียว แต่อัดข้อมูลของโดรนทุกตัว (1 ถึง 50) ไปในแพ็กเกจนั้นเลย โดรนทุกลำจะได้รับข้อมูลพร้อมกัน และดึงข้อมูลเฉพาะแถว (Index) ที่เป็นของตัวเองไปใช้งาน ข้อดีคือ โดรนทุกลำจะขยับตัวพร้อมกันอย่างสมบูรณ์ (Perfect Synchronization) ไม่เกิดการเหลื่อมล้ำของเวลา ⚠️ ข้อจำกัดสำคัญที่ต้องคำนวณ (Buffer Size) ESP-NOW มีข้อจำกัดที่ ขนาดของแพ็กเกจ (Payload) สูงสุดห้ามเกิน 250 Bytes ต่อการส่ง 1 ครั้ง ลองมาคำนวณขนาดข้อมูลกันครับ: ถ้าข้อมูลโดรน 1 ลำ ประกอบด้วย พิกัด X,Y,Z (ใช้ float ตัวละ 4 Bytes) และ สถานะไฟ LED (ใช้ byte หรือ uint8_t 1 Byte) ข้อมูลต่อ 1 ลำ = 4+4+4+1=13 Bytes ถ้ามีโดรน 50 ตัว = 13×50=650 Bytes ❌ เกิน 250 Bytes (ส่งในรอบเดียวไม่ได้) 💡 วิธีแก้: แบ่งกลุ่มส่ง (Grouping / Chunking) เราจะสับ Array 2 มิติออกเป็น 3 กลุ่ม (Chunks) กลุ่มละ 17-18 ตัว เพื่อให้ขนาดไม่เกิน 250 Bytes แล้วส่งเรียงกันอย่างรวดเร็ว โดรนจะดูว่าข้อมูลที่มาเป็นของกลุ่มตัวเองหรือไม่ ถ้าใช่ค่อยดึงไปใช้งานครับ 1. โค้ดฝั่งตัวส่ง (Master / Ground Station) C++ #include #include #include #define WIFI_CHANNEL 1 #define TOTAL_DRONES 50 #define DRONES_PER_CHUNK 17 // แบ่งส่งรอบละ 17 ตัว uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // โครงสร้างข้อมูลย่อยของโดรน 1 ตัว (รวม 13 Bytes) typedef struct { float x; float y; float z; uint8_t ledColor; // ใช้รหัสสีแทน เช่น 1=แดง, 2=เขียว, 3=น้ำเงิน เพื่อประหยัดพื้นที่ } DroneData; // โครงสร้างแพ็กเกจที่จะส่งออกอากาศ typedef struct { int chunk_id; // กลุ่มที่ส่ง (0, 1, 2) int start_index; // ID เริ่มต้นของกลุ่มนี้ DroneData group_data[DRONES_PER_CHUNK]; // Array ของโดรน 17 ตัว (17 x 13 = 221 Bytes) } ChunkPacket; ChunkPacket packet; // Array 2 มิติหลักบน Master เก็บพิกัดโดรนทั้ง 50 ตัว [ลำที่][ข้อมูล] // ในงานจริง ค่าพวกนี้จะถูกอ่านมาจาก SD Card หรือคำนวณมาจากโปรแกรมแอนิเมชัน DroneData masterFrame[TOTAL_DRONES]; void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); esp_wifi_set_promiscuous(true); esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE); esp_wifi_set_promiscuous(false); if (esp_now_init() != ESP_OK) return; esp_now_peer_info_t peerInfo = {}; memcpy(peerInfo.peer_addr, broadcastAddress, 6); peerInfo.channel = WIFI_CHANNEL; peerInfo.encrypt = false; esp_now_add_peer(&peerInfo); } void loop() { // 1. จำลองการอัปเดตข้อมูล Array 2 มิติ (Master Frame) ทุกๆ รอบบิน for(int i = 0; i < TOTAL_DRONES; i++) { masterFrame[i].x = 10.0 + i; masterFrame[i].y = 20.0 - i; masterFrame[i].z = 5.5; masterFrame[i].ledColor = 1; } // 2. แบ่งกระจายข้อมูลออกเป็น 3 รอบ (Chunks) เพื่อไม่ให้เกิน 250 Bytes int current_drone = 0; for (int chunk = 0; chunk < 3; chunk++) { packet.chunk_id = chunk; packet.start_index = current_drone; // ก๊อปปี้ข้อมูลจาก Array หลัก ใส่ลงแพ็กเกจย่อย for (int i = 0; i < DRONES_PER_CHUNK; i++) { if (current_drone < TOTAL_DRONES) { packet.group_data[i] = masterFrame[current_drone]; current_drone++; } } // ส่งออกอากาศทีละก้อน (ขนาดก้อนละประมาณ 230 Bytes ซึ่งปลอดภัย) esp_now_send(broadcastAddress, (uint8_t *) &packet, sizeof(packet)); delay(5); // หน่วงเวลาสั้นๆ 5ms ป้องกันช่องสัญญาณหนาแน่นเกินไป } delay(100); // อัปเดตเฟรมถัดไปทุกๆ 100ms (10 เฟรมต่อวินาที) } 2. โค้ดฝั่งตัวรับ (Slave / โดรนแต่ละลำ) โดรนทุกลำรับแพ็กเกจเดียวกัน แต่จะใช้คณิตศาสตร์คำนวณเปรียบเทียบ Index เพื่อดึงข้อมูลเฉพาะแถวของตัวเองออกมา C++ #include #include #include #define MY_DRONE_ID 25 // ** ลำตัวรับที่ 25 (นับแบบเลขอาร์เรย์คือ index ที่ 24) ** #define MY_INDEX (MY_DRONE_ID - 1) #define WIFI_CHANNEL 1 #define DRONES_PER_CHUNK 17 typedef struct { float x; float y; float z; uint8_t ledColor; } DroneData; typedef struct { int chunk_id; int start_index; DroneData group_data[DRONES_PER_CHUNK]; } ChunkPacket; ChunkPacket incomingPacket; void OnDataRecv(const esp_now_recv_info *recvInfo, const uint8_t *incomingData, int len) { memcpy(&incomingPacket, incomingData, sizeof(incomingPacket)); // ตรวจสอบว่าข้อมูลก้อนที่ลอยมาในอากาศนี้ มีข้อมูลของลำเราอยู่ด้านในหรือไม่ // เช่น ถ้าเป็น Chunk 1 เริ่มต้นที่ index 17 ถึง 33 และเราคือ index 24 แปลว่าอยู่ในก้อนนี้! if (MY_INDEX >= incomingPacket.start_index && MY_INDEX < (incomingPacket.start_index + DRONES_PER_CHUNK)) { // คำนวณหาตำแหน่งลอคอลอินเด็กซ์ภายในก้อนนั้น int local_index = MY_INDEX - incomingPacket.start_index; // ดึงข้อมูลแถวของตัวเองออกมาใช้งาน float myX = incomingPacket.group_data[local_index].x; float myY = incomingPacket.group_data[local_index].y; float myZ = incomingPacket.group_data[local_index].z; uint8_t myColor = incomingPacket.group_data[local_index].ledColor; Serial.printf("Drone %d Recv -> X: %.2f, Y: %.2f, Z: %.2f, Color: %d\n", MY_DRONE_ID, myX, myY, myZ, myColor); } } void setup() { Serial.begin(115200); WiFi.mode(WIFI_STA); esp_wifi_set_promiscuous(true); esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE); esp_wifi_set_promiscuous(false); if (esp_now_init() != ESP_OK) return; esp_now_register_recv_cb(OnDataRecv); } void loop() { // ควบคุมการทรงตัวของโดรนตามพิกัดล่าสุดที่ได้รับ } สรุปหลักการ วิธีนี้ Master จะมองโดรน 50 ตัวเป็นตาราง Array 2 มิติ เวลาจะส่งก็ทำการหั่นเค้กออกเป็น 3 ชิ้นชิ้นละ 17 ตัว แล้วโยนโครมออกไปในอากาศ โดรนทุกลำที่แอบฟังอยู่ก็จะรีบวิ่งมาดูเค้กชิ้นนั้น ถ้าเห็นว่ามีชื่อตัวเองแปะอยู่ในชิ้นนั้น (เช็คจาก start_index) ก็จะตักเอาเฉพาะส่วนของตัวเองไปบินครับ