คู่มือ Mesh Networking — สื่อสารในที่ที่ไม่มีเสาสัญญาณ
เข้าใจว่าเครือข่ายที่ไม่มีเสาหาเส้นทางกันเองได้ยังไง ตั้งแต่คลื่นวิทยุจนถึง routing หลายคลื่น อ่านจบแล้วจำลองเครือข่ายเองได้ด้วย Python บนโน้ตบุ๊กตัวเดียว ไม่ต้องมีวิทยุ
คู่มือที่พาคุณจากคนไม่เคยแตะคลื่นวิทยุและไม่เคยอ่าน routing table ไปจนเข้าใจว่าเครือข่ายที่ไม่มีเสาสัญญาณตัดสินใจส่งข้อมูลผ่านใครได้ยังไง — และทำไมการตัดสินใจนั้นถึงยากกว่าที่คิดมาก
- ระดับ: เริ่มจากศูนย์ (ไม่ต้องมีพื้น RF ไม่ต้องเคยตั้ง router)
- เวลาอ่าน: ~80 นาที
- ภาษา: ไทย พร้อมโค้ดที่รันได้จริงด้วย Python เปล่า ๆ ไม่ต้องลงไลบรารีเพิ่ม
LEVEL 0 · ปูพื้นจากศูนย์
เมื่อไม่มีเสาสัญญาณให้พึ่ง#
ลองนึกภาพนี้ให้ชัดก่อน เพราะทั้งเล่มจะวนกลับมาที่มันตลอด
ทีมกู้ภัยหกคนเข้าไปในหุบเขาที่ไฟกำลังไหม้ โทรศัพท์ทุกเครื่องขึ้นว่าไม่มีสัญญาณ — ไม่ใช่เพราะเครื่องเสีย แต่เพราะเสาสัญญาณที่ใกล้ที่สุดอยู่ห่างออกไปยี่สิบกิโลเมตรและมีภูเขาขวางอยู่ ทุกคนยังต้องรู้ว่าอีกห้าคนอยู่ตรงไหน ไฟลามไปทางไหนแล้ว และใครกำลังเดินเข้าไปในทิศที่ลมกำลังพัดไฟมา
คำถามของเล่มนี้คือคำถามเดียว: ทำให้ทั้งหกคนคุยกันได้ยังไง ในเมื่อไม่มีใครเป็นศูนย์กลาง
1.1 ทำไมเครือข่ายที่เราคุ้นเคยถึงใช้ไม่ได้
เครือข่ายที่คุณใช้ทุกวันมีโครงสร้างแบบ ดาว — มีตัวกลางหนึ่งตัวที่ทุกคนต่อเข้าหา
มือถือคุณไม่เคยคุยกับมือถือเพื่อนโดยตรงสักครั้งเดียว มันคุยกับเสา เสาคุยกับเครือข่ายผู้ให้บริการ แล้วเครือข่ายค่อยส่งต่อลงมาที่เสาอีกต้นที่เพื่อนคุณเกาะอยู่ WiFi ที่บ้านก็เหมือนกัน โน้ตบุ๊กคุยกับ router ตัวเดียว ไม่ได้คุยกับทีวีที่นั่งอยู่ข้าง ๆ โดยตรง
โครงสร้างแบบนี้ทำงานดีมากจนเราลืมไปว่ามันมีเงื่อนไข: ตัวกลางต้องมีอยู่ และทุกคนต้องเอื้อมถึงมัน พอเงื่อนไขนั้นหายไป — เสาไฟไหม้ ไฟดับ น้ำท่วมสถานี หรือแค่เดินเข้าไปในที่ที่ไม่เคยมีใครลากไฟเข้าไป — เครือข่ายทั้งระบบกลายเป็นศูนย์ ไม่ใช่ช้าลง แต่เป็นศูนย์
1.2 ทางออกที่ชื่อว่า mesh
Mesh network คือเครือข่ายที่ไม่มีตัวกลาง ทุกเครื่องเป็นทั้งผู้ใช้และผู้ส่งต่อในตัวเดียวกัน
ถ้าคนที่ 1 กับคนที่ 6 อยู่ไกลกันเกินกว่าวิทยุจะคุยกันตรง ๆ ได้ ข้อความจะไม่ตาย — มันจะกระโดดผ่านคนที่ 3 ที่บังเอิญยืนอยู่ตรงกลาง แล้วต่อไปที่คนที่ 6 การกระโดดหนึ่งครั้งเรียกว่า hop และความสามารถในการหาเส้นทางกระโดดเองโดยไม่มีใครสั่ง คือหัวใจทั้งหมดของเล่มนี้
💡 คำที่จะเจอตลอดเล่ม — โหนด (node) = อุปกรณ์หนึ่งตัวในเครือข่าย (วิทยุ + คอมพิวเตอร์เล็ก ๆ ในกล่องเดียว) · ลิงก์ (link) = ความสามารถในการคุยกันตรง ๆ ระหว่างโหนดสองตัว · เส้นทาง (route) = ลำดับ hop ที่ข้อมูลต้องเดินเพื่อไปถึงปลายทาง
1.3 ทำไมมันถึงไม่ใช่เรื่องง่าย
ถ้าโจทย์มีแค่ "หาทางไปให้ถึง" มันคงจบตั้งแต่ยุค 1970 อัลกอริทึมหาเส้นทางสั้นที่สุดในกราฟเป็นเรื่องที่แก้ได้นานแล้ว ปัญหาจริงคือสามข้อที่กราฟในตำราไม่มี
หนึ่ง — โหนดขยับตลอดเวลา คนเดิน รถวิ่ง โดรนบิน เส้นทางที่คำนวณไว้เมื่อสามสิบวินาทีก่อนอาจไม่มีอยู่แล้ว
สอง — ลิงก์ไม่ได้มีแค่ "มี" กับ "ไม่มี" สายแลนเสียบแล้วคือเสียบ แต่คลื่นวิทยุมีสภาพเป็นเฉด — ส่งได้บ้างไม่ได้บ้าง ดีตอนเช้าแย่ตอนบ่าย ดีตอนยืนแย่ตอนนั่ง
สาม — ทุกคนใช้อากาศก้อนเดียวกัน ในเครือข่ายมีสาย การที่ A คุยกับ B ไม่รบกวน C คุยกับ D เพราะเป็นคนละเส้น แต่ในอากาศ ทุกคนที่อยู่ในระยะได้ยินกันหมด และการพูดพร้อมกันแปลว่าไม่มีใครได้ยินอะไรเลย
สามข้อนี้คือเหตุผลที่ทั้งเล่มต้องยาวถึงสิบห้าบท และเป็นเหตุผลที่บทหน้าต้องเริ่มจากคลื่นวิทยุก่อน — เพราะถ้าไม่เข้าใจว่าคลื่นทำตัวยังไง ข้อสองกับข้อสามจะเป็นแค่คำพูดลอย ๆ ที่จำได้แต่ใช้ไม่เป็น
คลื่นวิทยุเท่าที่ routing ต้องใช้#
บทนี้ไม่ได้สอน RF ทั้งศาสตร์ — สอนเฉพาะสี่เรื่องที่ถ้าไม่รู้แล้วจะอ่านบทหลัง ๆ ไม่รู้เรื่อง ถ้าคุณเคยเรียนสายสื่อสารมาแล้ว ข้ามไปบทที่ 3 ได้เลย
2.1 สัญญาณอ่อนลงตามระยะ และอ่อนลงเร็วกว่าที่คิด
สิ่งแรกที่ต้องเลิกเชื่อคือภาพวงกลมสีฟ้ารอบเสาสัญญาณที่เราเห็นในโฆษณา ซึ่งบอกเป็นนัยว่า "ในวงนี้ใช้ได้ นอกวงใช้ไม่ได้" ความจริงคือสัญญาณอ่อนลงอย่างต่อเนื่องตั้งแต่เมตรแรก
สูตรที่ใช้กันคือ free-space path loss — กำลังที่หายไประหว่างทางเมื่อไม่มีอะไรขวางเลย ลองรันดูที่ความถี่ 900 MHz
import math
def fspl_db(dist_m, freq_mhz):
"""กำลังที่หายไป (dB) เมื่อไม่มีอะไรขวาง — d เป็นกิโลเมตร f เป็นเมกะเฮิรตซ์"""
return 20*math.log10(dist_m/1000) + 20*math.log10(freq_mhz) + 32.44
for d in (100, 500, 1000, 5000, 10000):
print(f"{d:>6} m : {fspl_db(d, 900):6.1f} dB")ผลรันจริง
100 m : 71.5 dB
500 m : 85.5 dB
1000 m : 91.5 dB
5000 m : 105.5 dB
10000 m : 111.5 dBเทียบทุกบรรทัดกับจุดตั้งต้นที่ 100 เมตร — ไกลขึ้น 5 เท่า จ่ายเพิ่ม 14 dB · ไกลขึ้น 10 เท่า จ่ายเพิ่ม 20 dB · ไกลขึ้น 100 เท่า จ่ายเพิ่ม 40 dB
💡 dB คืออะไร — หน่วยที่บอก "กี่เท่า" ไม่ใช่ "เท่าไหร่" ทุก ๆ 3 dB คือครึ่งหนึ่ง และ 10 dB คือหนึ่งในสิบ · ดังนั้น 20 dB ที่จ่ายเพิ่มตอนไกลขึ้น 10 เท่า แปลว่าสัญญาณเหลือ หนึ่งในร้อย ของเดิม
ตัวเลขชุดนี้ให้บทเรียนที่ขัดสัญชาตญาณ: การเพิ่มระยะทางเป็นสองเท่าไม่ได้ทำให้แย่ลงสองเท่า แต่แย่ลงสี่เท่า และนี่คือกรณีดีที่สุดที่ไม่มีอะไรขวางเลย
2.2 ความถี่ต่ำไปได้ไกลกว่า แต่แลกมาด้วยอย่างอื่น
ในสูตรเดียวกัน ลองเปลี่ยนความถี่แทน ที่ระยะ 1 กิโลเมตรเท่ากันหมด:
150 MHz : 76.0 dB
433 MHz : 85.2 dB
900 MHz : 91.5 dB
2400 MHz : 100.0 dB ← ย่านเดียวกับ WiFi/Bluetooth ที่บ้าน
5800 MHz : 107.7 dBต่างกัน 31.7 dB ระหว่างหัวกับท้าย — ประมาณ หนึ่งพันห้าร้อยเท่า พลิกอีกด้าน ถ้ากำหนดว่าอุปกรณ์ทนการสูญเสียได้ 120 dB ระยะที่ไปถึงจะเป็น:
150 MHz : 159.19 กม.
900 MHz : 26.53 กม.
2400 MHz : 9.95 กม.
5800 MHz : 4.12 กม.⚠️ ตัวเลขชุดหลังนี้เป็นค่าในสุญญากาศ บนโลกจริงมันน้อยกว่านี้มาก เพราะโลกโค้ง มีต้นไม้ มีภูเขา และอากาศเองก็ดูดกลืนพลังงาน อย่าเอาไปอ้างว่า "วิทยุ 150 MHz ยิงได้ 159 กิโลเมตร" — ใช้มันเป็นการเปรียบเทียบระหว่างความถี่เท่านั้น
ถ้าความถี่ต่ำดีกว่าทุกทาง ทำไมไม่ใช้ต่ำสุดไปเลย? เพราะความถี่ต่ำมี แบนด์วิดท์ ให้ใช้น้อยกว่ามาก — พูดง่าย ๆ คือไปได้ไกลแต่พูดได้ช้า ย่าน 150 MHz เหมาะกับข้อความสั้น ๆ อย่างพิกัด ส่วนวิดีโอจากกล้องต้องใช้ย่านสูงที่ไปได้ใกล้กว่า
จำประโยคนี้ไว้ให้ดี เพราะมันคือเมล็ดของบทที่จะเป็นจุดพีคของเล่ม: ไม่มีความถี่ไหนดีที่สุด มีแต่ความถี่ที่เหมาะกับงานคนละแบบ
2.3 สิ่งกีดขวางไม่ได้บังแค่สายตา
คลื่นวิทยุทะลุกำแพงบาง ๆ ได้ แต่ต้องจ่ายค่าผ่านทาง กำแพงอิฐกินไป 10–15 dB ผนังคอนกรีตเสริมเหล็กกินได้ถึง 20–30 dB ส่วนใบไม้เปียกน้ำ — ซึ่งเจอตลอดในงานภาคสนาม — ดูดกลืนได้มากอย่างน่าตกใจเพราะน้ำสั่นพ้องกับคลื่นย่าน GHz ได้ดี
ที่โหดกว่านั้นคือ ร่างกายคน ตัวคนเป็นถุงน้ำเดินได้ การหันหลังให้เพื่อนอาจทำให้สัญญาณลดลง 5–10 dB นั่นแปลว่าลิงก์ในเครือข่ายกู้ภัยเปลี่ยนค่าทุกครั้งที่มีคนหันตัว ไม่ใช่ทุกครั้งที่มีคนเดินไปไกล
2.4 คลื่นเดียวกันในเวลาเดียวกัน = ไม่มีใครได้ยิน
เรื่องสุดท้ายและสำคัญที่สุดสำหรับ routing: อากาศเป็นทรัพยากรร่วม
ถ้าโหนดสองตัวส่งพร้อมกันในย่านความถี่เดียวกันและอยู่ในระยะได้ยินกัน สัญญาณจะทับกันจนผู้รับถอดรหัสไม่ออกทั้งคู่ เรียกว่า การชนกัน (collision) ผลคือไม่ใช่แค่ช้าลง แต่ข้อมูลหายทั้งสองฝั่งและต้องส่งใหม่
นี่คือความต่างที่ใหญ่ที่สุดจากเครือข่ายมีสาย และเป็นสาเหตุของปรากฏการณ์ที่จะทำให้คุณหงุดหงิดในบทที่ 10: เส้นทางที่มีหลาย hop ได้ความเร็วไม่ถึงหนึ่งในสามของที่วิทยุทำได้ ไม่ใช่เพราะแต่ละ hop ช้า แต่เพราะ hop ที่อยู่ใกล้กันแย่งอากาศก้อนเดียวกัน
แพ็กเก็ตเดินทางยังไง#
ก่อนจะให้โหนดหาเส้นทางเองได้ ต้องรู้ก่อนว่า "เส้นทาง" ในเครือข่ายหน้าตาเป็นยังไง บทนี้ปูพื้นเน็ตเวิร์กเท่าที่จำเป็น ถ้าคุณเคยเขียนโค้ดที่เรียก API ข้ามเครื่องมาแล้ว ก็ยังควรอ่าน เพราะสิ่งที่จะเน้นคือมุมที่นักพัฒนาส่วนใหญ่ไม่เคยต้องมอง
3.1 ข้อมูลถูกหั่นเป็นซอง
ข้อมูลไม่ได้ไหลเป็นสายน้ำ แต่ถูกหั่นเป็นชิ้นเล็ก ๆ เรียกว่า แพ็กเก็ต (packet) แต่ละชิ้นมีสองส่วน: หัวซอง (header) ที่เขียนว่ามาจากไหนจะไปไหน และ เนื้อใน (payload) ที่เป็นข้อมูลจริง
การหั่นแบบนี้ทำให้เกิดอย่างหนึ่งที่สำคัญกับเล่มนี้มาก: แต่ละแพ็กเก็ตเดินทางอิสระจากกัน สองแพ็กเก็ตที่ออกจากเครื่องเดียวกันห่างกันเสี้ยววินาที อาจไปคนละเส้นทาง ถึงไม่พร้อมกัน หรือชิ้นหนึ่งถึงอีกชิ้นหายก็ได้
3.2 router ไม่รู้เส้นทางทั้งเส้น
นี่คือจุดที่คนส่วนใหญ่เข้าใจผิด และเป็นแบบจำลองที่ต้องรื้อทิ้ง
ความเข้าใจผิดที่พบบ่อยคือ router รู้ว่าแพ็กเก็ตต้องเดินทางผ่านเครื่องไหนบ้างจนถึงปลายทาง ความจริงคือ router รู้แค่ก้าวถัดไปก้าวเดียว ตารางในหัวมันหน้าตาประมาณนี้
ปลายทาง ส่งต่อให้ใคร
10.0.0.0/24 ส่งออกทางพอร์ต 1 (ต่อตรง)
192.168.5.0/24 ส่งให้ 10.0.0.9
ที่เหลือทั้งหมด ส่งให้ 10.0.0.1ไม่มีบรรทัดไหนบอกว่า "เส้นทางไป 192.168.5.0 คือผ่าน A แล้ว B แล้ว C" มันบอกแค่ว่า โยนให้ 10.0.0.9 แล้วเชื่อว่าหมอนั่นรู้ทางต่อ
💡 อุปมาที่ใช้ได้จริง — เหมือนถามทางกลางเมืองแล้วได้คำตอบว่า "เลี้ยวซ้ายตรงไฟแดงหน้า แล้วไปถามใหม่" ทุกแยกให้คำตอบทีละแยก ไม่มีใครวาดแผนที่ทั้งเส้นให้ · จุดที่อุปมานี้ใช้ไม่ได้: คนบอกทางไม่เคยเปลี่ยนคำตอบทุกสิบวินาที แต่ในเมชมันเปลี่ยน และนั่นคือทั้งเล่มนี้
การที่ทุกโหนดรู้แค่ก้าวเดียวคือสิ่งที่ทำให้ mesh เป็นไปได้เลย ถ้าทุกโหนดต้องรู้เส้นทางเต็มเส้นไปยังทุกปลายทาง เครือข่ายที่โหนดขยับตลอดเวลาจะพังทันทีที่มีใครขยับ
3.3 hop, TTL และการวนไม่รู้จบ
ถ้าทุกคนรู้แค่ก้าวเดียว จะมั่นใจได้ยังไงว่าแพ็กเก็ตไม่วนอยู่ในวง A ส่งให้ B, B ส่งกลับให้ A ตลอดกาล
คำตอบง่ายกว่าที่คิด: ทุกแพ็กเก็ตพกตัวนับชื่อ TTL (time to live) ติดตัว ทุกครั้งที่ผ่านโหนดหนึ่งตัว ค่านี้ลดลงหนึ่ง พอถึงศูนย์ก็ถูกทิ้ง — เป็นการยอมรับตั้งแต่ต้นว่า เครือข่ายจะเข้าใจผิดเรื่องเส้นทางเป็นครั้งคราว และต้องมีตาข่ายรองรับ ไม่ใช่พยายามทำให้ไม่เคยผิดเลย
แนวคิด "ยอมให้ผิดแล้วมีตัวหยุด" นี้จะกลับมาอีกหลายรอบในเล่ม
3.4 ทำไมตารางถึงต้องมีคนเติม
ตารางเส้นทางไม่ได้เกิดเอง ในเครือข่ายบริษัท คนไอทีพิมพ์เข้าไปเอง หรือ router คุยกันด้วยโปรโตคอลที่ออกแบบมาสำหรับโลกที่สายไม่ขยับ
ในเมชไม่มีทั้งสองอย่าง ไม่มีคนไอทีเดินตามทีมกู้ภัยเข้าไปในหุบเขาเพื่อพิมพ์ตาราง และเส้นทางเปลี่ยนเร็วกว่าที่โปรโตคอลของโลกมีสายจะตามทัน
คำถามที่เหลือของเล่มนี้จึงมีข้อเดียว: ตารางนั้นถูกเติมด้วยอะไร และเติมด้วยข้อมูลที่สดแค่ไหน บทหน้าจะตอบครึ่งแรก และมันคือบทที่สำคัญที่สุดในเล่ม
แบบจำลองความคิด: ลิงก์คือความน่าจะเป็น#
บทนี้สำคัญที่สุดในเล่ม ถ้าอ่านได้บทเดียวให้อ่านบทนี้ เพราะอีกสิบเอ็ดบทที่เหลือคือการไต่ประโยคเดียวที่จะเขียนอยู่ข้างล่างนี้
4.1 ประโยคแก่น
ลิงก์ในเมชไม่ใช่สาย แต่เป็นค่าประมาณที่เน่าตามเวลา
ในเครือข่ายมีสาย ลิงก์มีสองสถานะ: เสียบหรือไม่เสียบ ถามเมื่อไหร่ก็ได้คำตอบเดิม
ในเมช คำถามว่า "A ต่อกับ B ได้ไหม" ไม่มีคำตอบว่าใช่หรือไม่ใช่ มีแต่คำตอบว่า "เมื่อสิบวินาทีที่แล้ว ส่งไปร้อยครั้งถึงเก้าสิบสองครั้ง" ซึ่งเป็นคำตอบที่มีวันหมดอายุติดมาด้วย
สังเกตว่าประโยคนั้นมีสองส่วนที่ต่างกันคนละเรื่อง — ค่าประมาณ (ไม่ใช่ 0/1 แต่เป็นเปอร์เซ็นต์) และ เน่าตามเวลา (ยิ่งเก่ายิ่งเชื่อไม่ได้) ทุกกลไกที่จะเจอในเล่มนี้แก้อย่างใดอย่างหนึ่งในสองข้อนี้เสมอ
4.2 พิสูจน์ด้วยตาเอง — ทำไมสัญญาณแรงถึงหลอกได้
ทีนี้มาดูว่า "ค่าประมาณ" ต่างจาก "ความแรงสัญญาณ" ยังไง เพราะนี่คือกับดักแรกที่ทุกคนตกกัน
ค่าที่วิทยุรายงานว่าสัญญาณแรงแค่ไหนเรียกว่า RSSI มีหน่วยเป็น dBm ยิ่งใกล้ศูนย์ยิ่งแรง (−52 แรงกว่า −78) สัญชาตญาณบอกว่าเลือกเพื่อนบ้านที่ RSSI แรงที่สุดสิ ลองรันดูว่าเกิดอะไรขึ้น
import random
def measure(deliver_fwd, deliver_rev, probes=100, seed=1):
"""ยิง probe จริง 100 ครั้งแล้วนับที่ถึง — ไม่ได้คำนวณจากสูตร แต่สุ่มตามความน่าจะเป็นที่กำหนด"""
rnd = random.Random(seed)
got_fwd = sum(1 for _ in range(probes) if rnd.random() < deliver_fwd)
got_rev = sum(1 for _ in range(probes) if rnd.random() < deliver_rev)
df, dr = got_fwd/probes, got_rev/probes
etx = 1/(df*dr) if df and dr else float("inf")
return got_fwd, got_rev, etx
# ชื่อ RSSI อัตราถึงขาไป อัตราถึงขากลับ
links = [("A-B", -55, 0.98, 0.97),
("A-C", -52, 0.70, 0.65),
("A-D", -78, 0.92, 0.90)]
for name, rssi, f, r in links:
gf, gr, etx = measure(f, r)
print(f"{name} RSSI {rssi} ถึง {gf}/{gr} ETX {etx:.2f}")ผลรันจริง
A-B RSSI -55 ถึง 97/98 ETX 1.05
A-C RSSI -52 ถึง 67/71 ETX 2.10
A-D RSSI -78 ถึง 92/95 ETX 1.14สามลิงก์นั้นคือสถานการณ์แบบนี้ และผลที่ได้ขัดกับสัญชาตญาณเต็ม ๆ
| ลิงก์ | สภาพจริง | RSSI | ETX |
|---|---|---|---|
| A-B | ใกล้ ไม่มีอะไรบัง | −55 | 1.05 ดีที่สุด |
| A-C | สัญญาณแรงกว่า แต่มีคนใช้ย่านนี้เยอะ | −52 แรงที่สุด | 2.10 แย่ที่สุด |
| A-D | สัญญาณอ่อนกว่ามาก แต่ย่านว่าง | −78 | 1.14 |
ลิงก์ที่สัญญาณแรงที่สุด (−52 dBm) เป็นลิงก์ที่แย่ที่สุด ในสามเส้น ส่วนลิงก์ที่อ่อนกว่ามันถึง 26 dB กลับดีกว่าเกือบเท่าตัว
เพราะอะไร? เพราะ RSSI ตอบคำถามว่า "ได้ยินดังแค่ไหน" ไม่ใช่ "สื่อสารสำเร็จไหม" ในย่านที่มีคนใช้เยอะ คุณได้ยินเพื่อนดังมาก และก็ได้ยินคนอื่นดังมากพร้อมกันด้วย เสียงทับกันจนถอดรหัสไม่ออก — ดังไม่ได้แปลว่าชัด
4.3 ETX — ตัวเลขที่วัดสิ่งที่เราต้องการจริง ๆ
ค่าที่โผล่ในคอลัมน์สุดท้ายคือ ETX (expected transmission count) แปลตรงตัวว่า "คาดว่าต้องส่งกี่ครั้งกว่าจะสำเร็จหนึ่งครั้ง"
สูตรมันสั้นมาก และเหตุผลเบื้องหลังสวยกว่าตัวสูตร:
ETX = 1 / (อัตราถึงขาไป × อัตราถึงขากลับ)ทำไมต้องคูณสองทิศ? เพราะการส่งสำเร็จหนึ่งครั้งต้องการสองเหตุการณ์: ข้อมูลไปถึงปลายทาง และ ใบตอบรับกลับมาถึงต้นทาง ถ้าอย่างใดอย่างหนึ่งหาย ผู้ส่งจะไม่รู้ว่าสำเร็จ แล้วส่งซ้ำอยู่ดี
ค่า ETX เท่ากับ 1 คือลิงก์สมบูรณ์แบบ ส่งครั้งเดียวถึง · เท่ากับ 2 คือโดยเฉลี่ยต้องส่งสองครั้ง · ยิ่งมากยิ่งแย่ และข้อดีที่สุดของมันคือ บวกกันข้าม hop ได้อย่างมีความหมาย — เส้นทางสาม hop ที่ ETX 1.05 + 1.14 + 1.05 = 3.24 เทียบกับเส้นทางสอง hop ที่ 2.10 + 2.10 = 4.20 ได้ตรง ๆ แปลว่าเส้นทางที่ยาวกว่าแต่ลิงก์ดี ชนะเส้นทางที่สั้นกว่าแต่ลิงก์แย่
นี่คือคำตอบแรกของปริศนาที่ปลูกไว้ในบทที่ 1 ว่าทำไม "เส้นทางสั้นที่สุด" ถึงไม่ใช่คำตอบ
4.4 ครึ่งหลังของประโยค: ค่าที่เน่า
ทุกตัวเลขในตารางข้างบนมีสิ่งที่มองไม่เห็นติดมาด้วย — อายุ
ค่า ETX 1.05 นั้นคำนวณจาก probe ร้อยครั้งที่ยิงไปแล้ว แปลว่ามันบอกอดีต ไม่ได้บอกปัจจุบัน ถ้าระหว่างนั้นมีคนเดินไปยืนขวาง หรือมีใครเปิดวิทยุตัวใหม่ในย่านเดียวกัน ตัวเลขนั้นก็ผิดทันทีโดยไม่มีใครรู้
ตรงนี้คือแก่นของ trade-off ที่จะวนกลับมาทุกบทที่เหลือ:
- ยิง probe ถี่ขึ้น → ตัวเลขสดขึ้น → แต่กินอากาศที่ควรใช้ส่งข้อมูลจริง และเปลืองแบตของอุปกรณ์ที่ต้องอยู่ในสนามทั้งวัน
- ยิง probe ห่างขึ้น → ประหยัดขึ้น → แต่ตัดสินใจด้วยข้อมูลเก่า ส่งไปตามเส้นทางที่พังไปแล้ว
ไม่มีคำตอบที่ถูกต้อง มีแต่จุดที่เลือกไว้ และการเลือกจุดนั้นคือสิ่งที่แยกโปรโตคอลแต่ละตัวออกจากกัน — ซึ่งเป็นเรื่องของบทที่ 8
4.5 ปริศนาที่จะไขในบทหลัง
จากประโยคแก่นเดียวนี้ มีคำถามสามข้อที่ตอบไม่ได้ด้วยความรู้แค่บทนี้ จดไว้แล้วไปต่อ
- ถ้าค่าเน่าตลอด แล้วจะรู้ได้ยังไงว่าเพื่อนบ้านคนไหนยังอยู่ → บทที่ 5
- ถ้าเส้นทางเปลี่ยนทุกครั้งที่ค่าขยับนิดเดียว เครือข่ายจะไม่สั่นตลอดเวลาหรือ → บทที่ 9
- ถ้าทุก hop แย่งอากาศกันเอง การมีเส้นทางที่ดีจะช่วยอะไรได้จริงหรือ → บทที่ 10 และคำตอบเต็มอยู่ที่บทที่ 11 ซึ่งเป็นจุดพีคของเล่ม
LEVEL 1 · พื้นฐานที่ใช้ทุกวัน
เพื่อนบ้านคือใคร และรู้ได้ยังไงว่ายังอยู่#
บทที่ 4 ทิ้งคำถามไว้ว่า ถ้าค่าทุกอย่างเน่าตลอด แล้วโหนดจะรู้ได้ยังไงว่าเพื่อนบ้านคนไหนยังอยู่ บทนี้ตอบข้อนั้น และคำตอบเรียบง่ายจนน่าประหลาดใจ
5.1 ไม่มีใครแจ้งลาออก
ในระบบที่มีสาย ถ้าคุณดึงสายออก อีกฝั่งรู้ทันทีว่าลิงก์ตาย เพราะไฟฟ้าที่เคยวิ่งอยู่หายไปในระดับฮาร์ดแวร์
ในเมชไม่มีสัญญาณแบบนั้น โหนดที่หายไปเพราะแบตหมด ตกเขา หรือเดินออกนอกระยะ ไม่มีโอกาสบอกลาใคร มันแค่เงียบไป และความเงียบกับ "ยังอยู่แต่ไม่มีอะไรจะพูด" หน้าตาเหมือนกันเป๊ะ
วิธีแก้จึงพลิกกลับด้าน: แทนที่จะรอฟังว่าใครตาย ให้ทุกคนประกาศว่าตัวเองยังอยู่เป็นจังหวะ ข้อความสั้น ๆ นี้เรียกว่า hello
5.2 hello, neighbor table และเส้นตายที่ต้องเลือกเอง
ทุกโหนดจดว่า "ได้ยิน hello จากใครล่าสุดเมื่อไหร่" ไว้ในตารางที่เรียกว่า neighbor table แล้วตั้งกติกาว่าถ้าเงียบเกินเส้นตายเมื่อไหร่ให้ถือว่าคนนั้นหายไป
NOW = 100.0 # เวลาปัจจุบัน (วินาทีนับจากเปิดเครื่อง)
HELLO_EVERY = 2.0 # ส่ง hello ทุกกี่วินาที
MISS_LIMIT = 3 # ขาดกี่ใบถือว่าเพื่อนบ้านหายไป
neighbors = {"B": 99.5, "C": 96.0, "D": 92.0} # ได้ยิน hello ล่าสุดเมื่อไหร่
deadline = HELLO_EVERY * MISS_LIMIT
for name, last in sorted(neighbors.items()):
age = NOW - last
print(f"{name}: ได้ยินล่าสุด {age:.1f} วิที่แล้ว -> {'ยังอยู่' if age <= deadline else 'ถือว่าหายไป'}")ผลรันจริง
B: ได้ยินล่าสุด 0.5 วิที่แล้ว -> ยังอยู่
C: ได้ยินล่าสุด 4.0 วิที่แล้ว -> ยังอยู่
D: ได้ยินล่าสุด 8.0 วิที่แล้ว -> ถือว่าหายไปสังเกตว่า C เงียบไปแล้วสองใบ แต่ยังไม่ถูกตัดออก เพราะเส้นตายตั้งไว้ที่สามใบ (2 × 3 = 6 วินาที) การเผื่อแบบนี้ไม่ใช่ความใจดี แต่เป็นการยอมรับว่า hello หายเองได้ — ในบทที่ 4 เราเพิ่งเห็นว่าลิงก์ที่ดีที่สุดยังส่งไม่ถึง 2–3% ถ้าตัดเพื่อนบ้านออกทันทีที่ขาดใบเดียว เครือข่ายจะสั่นตลอดเวลาโดยที่ไม่มีใครหายไปไหนจริง ๆ
5.3 ทำไม hello ถึงเป็นได้มากกว่าการเช็กชื่อ
hello ทำงานสองอย่างในใบเดียว และอย่างที่สองสำคัญกว่า
อย่างแรกคือบอกว่ายังอยู่ อย่างที่สองคือ มันคือ probe ที่ใช้คำนวณ ETX — ถ้าเพื่อนบ้านส่ง hello ทุก 2 วินาที แล้วในหนึ่งนาทีคุณควรได้ยิน 30 ใบ แต่ได้จริง 27 ใบ นั่นแปลว่าอัตราส่งถึงขาเข้าคือ 90% โดยไม่ต้องยิงอะไรเพิ่มเลย
ที่เหลือคืออัตราขาออก ซึ่งคุณวัดเองไม่ได้เพราะไม่รู้ว่าฝั่งโน้นได้ยินคุณกี่ใบ วิธีมาตรฐานคือ ให้แต่ละโหนดใส่ตัวเลขนี้ลงไปใน hello ของตัวเอง — hello ของ B จึงมีข้อความประมาณว่า "ฉันได้ยิน A 27 จาก 30 ใบ" พอ A ได้ยิน ก็ได้ตัวเลขขาออกของตัวเองมาเติมสมการ ETX ครบพอดี
💡 นี่คือลูกเล่นที่สวยที่สุดอันหนึ่งในสายนี้: ข้อมูลที่ใช้ตัดสินใจ ถูกผลิตขึ้นจากกลไกที่มีอยู่แล้ว โดยไม่ต้องจ่ายค่าอากาศเพิ่มแม้แต่ไบต์เดียว
5.4 ราคาของความสด
เส้นตายที่เลือกไว้ในโค้ดข้างบนกำหนดสองอย่างพร้อมกัน: ตรวจจับได้เร็วแค่ไหน และ ผิดพลาดบ่อยแค่ไหน
- hello ถี่ + เส้นตายสั้น → รู้ว่าใครหายภายในไม่กี่วินาที แต่โหนดที่แค่โดนบังชั่วคราวจะถูกตัดทิ้งบ่อย
- hello ห่าง + เส้นตายยาว → นิ่งกว่า แต่ยังส่งข้อมูลเข้าเส้นทางที่ตายไปแล้วอีกหลายสิบวินาที
ค่าที่ใช้กันจริงในโปรโตคอลสาย mesh อยู่ราว 1–3 วินาทีต่อ hello และเส้นตายที่ 3–6 เท่าของช่วงนั้น ตัวเลขที่แน่นอนขึ้นกับว่าโหนดขยับเร็วแค่ไหน — คนเดินกับโดรนบินไม่ควรใช้ค่าเดียวกัน
เส้นทางสั้นที่สุด ไม่ใช่เส้นทางที่ดีที่สุด#
ตอนนี้ทุกโหนดรู้จักเพื่อนบ้านและรู้ค่า ETX ของลิงก์รอบตัวแล้ว ถึงเวลาแปลงมันเป็นเส้นทาง
6.1 อัลกอริทึมเก่าแก่ที่ยังใช้อยู่
การหาเส้นทางถูกที่สุดในกราฟมีคำตอบตั้งแต่ปี 1959 ชื่อ Dijkstra หลักการคือค่อย ๆ ขยายวงจากต้นทาง เลือกโหนดที่ไปถึงได้ถูกที่สุดก่อนเสมอ แล้วอัปเดตเพื่อนบ้านของมัน
จุดสำคัญไม่ได้อยู่ที่ตัวอัลกอริทึม แต่อยู่ที่ น้ำหนักที่ใส่เข้าไป — เปลี่ยนน้ำหนัก คำตอบเปลี่ยนทันที ทั้งที่โค้ดเหมือนเดิมทุกบรรทัด
6.2 พิสูจน์ด้วยตาเอง
import heapq
# ลิงก์ (u, v, ETX) — ค่ามาจากการวัดแบบบทที่ 4
LINKS = [("A","C",2.10), ("C","F",2.10), # เส้นตรง 2 hop ผ่านย่านที่คนใช้เยอะ
("A","D",1.14), ("D","B",1.20), ("B","F",1.10)] # เส้นอ้อม 3 hop ลิงก์ดีทุกเส้น
def dijkstra(links, src):
g = {}
for u, v, w in links:
g.setdefault(u, []).append((v, w))
g.setdefault(v, []).append((u, w))
dist = {src: 0.0}; prev = {}; pq = [(0.0, src)]
while pq:
d, u = heapq.heappop(pq)
if d > dist.get(u, float("inf")): continue
for v, w in g.get(u, []):
nd = d + w
if nd < dist.get(v, float("inf")):
dist[v] = nd; prev[v] = u; heapq.heappush(pq, (nd, v))
return dist, prev
def path(prev, dst):
out = [dst]
while dst in prev:
dst = prev[dst]; out.append(dst)
return " -> ".join(reversed(out))
dist, prev = dijkstra(LINKS, "A")
best = path(prev, "F"); hops = best.count("->")
short, chosen = (2.10+2.10)*100, dist["F"]*100
print(f" ถ้านับ hop : A -> C -> F 2 hop ETX รวม {2.10+2.10:.2f}")
print(f" ถ้านับ ETX : {best} {hops} hop ETX รวม {dist['F']:.2f}")
print(f" ส่ง 100 ชิ้น: เส้น 2 hop = {short:.0f} ครั้ง · เส้น {hops} hop = {chosen:.0f} ครั้ง"
f" ประหยัดกว่า {(1-chosen/short)*100:.0f}%")ผลรันจริง
ถ้านับ hop : A -> C -> F 2 hop ETX รวม 4.20
ถ้านับ ETX : A -> D -> B -> F 3 hop ETX รวม 3.44
ส่ง 100 ชิ้น: เส้น 2 hop = 420 ครั้ง · เส้น 3 hop = 344 ครั้ง ประหยัดกว่า 18%เส้นทางที่ยาวกว่าหนึ่ง hop ใช้การออกอากาศน้อยกว่า 76 ครั้งต่อข้อมูล 100 ชิ้น นี่คือคำตอบเต็มของปริศนาที่ปลูกไว้ตั้งแต่บทที่ 1 — และเหตุผลที่โปรโตคอลเมชรุ่นแรก ๆ ที่นับ hop อย่างเดียวถึงทำงานได้แย่ในสนามจริงจนต้องถูกแทนที่
6.3 ทำไมการบวกถึงถูกต้องตามความหมาย
การเอาค่ามาบวกกันข้าม hop ฟังดูเป็นเรื่องเทคนิค แต่มันมีความหมายทางกายภาพจริง
ETX ของหนึ่งลิงก์ = จำนวนครั้งที่คาดว่าต้องออกอากาศเพื่อให้ผ่านลิงก์นั้นสำเร็จหนึ่งครั้ง ⇒ ผลรวมของทั้งเส้นทาง = จำนวนครั้งที่ต้องออกอากาศรวมทั้งเส้น ซึ่งเป็นสิ่งที่เราอยากประหยัดจริง ๆ เพราะทุกครั้งที่ออกอากาศคือการกินอากาศที่คนอื่นต้องใช้ และกินแบตที่มีจำกัด
เทียบกับการนับ hop ที่บวกได้เหมือนกัน แต่ผลรวมไม่ได้แปลว่าอะไรเลยนอกจาก "จำนวนเครื่องที่แตะข้อมูลนี้"
ตารางเส้นทางเกิดขึ้นได้ยังไง#
บทที่ 6 คำนวณเส้นทางโดยสมมติว่ามีข้อมูลลิงก์ทั้งเครือข่ายอยู่ในมือแล้ว บทนี้ถามว่าข้อมูลนั้นมาถึงได้ยังไง และมีสองสำนักคิดที่ตอบคนละแบบ
7.1 สำนักที่หนึ่ง: ทุกคนเห็นแผนที่ทั้งใบ (link-state)
แต่ละโหนดประกาศออกไปทั้งเครือข่ายว่า "ฉันมีเพื่อนบ้านชื่ออะไรบ้าง ลิงก์ละเท่าไหร่" ทุกคนเก็บประกาศของทุกคน แล้วต่างคนต่างมีแผนที่ทั้งใบเหมือนกัน จากนั้นรัน Dijkstra บนแผนที่นั้นเอง
- ข้อดี: ทุกโหนดเห็นภาพเดียวกัน จึงตัดสินใจสอดคล้องกันและลู่เข้าเร็วเมื่อมีอะไรเปลี่ยน
- ข้อเสีย: ค่าใช้จ่ายในการกระจายประกาศโตตามจำนวนโหนด และทุกครั้งที่ลิงก์ขยับก็ต้องประกาศใหม่
7.2 สำนักที่สอง: ทุกคนรู้แค่ที่เพื่อนบ้านบอก (distance-vector)
แต่ละโหนดไม่เก็บแผนที่เลย เก็บแค่ตารางว่า "ปลายทาง X ฉันไปถึงด้วยต้นทุนเท่าไหร่ และต้องส่งให้ใคร" แล้วบอกตารางนี้ให้เพื่อนบ้านฟังเป็นระยะ ใครได้ยินก็เอาไปบวกต้นทุนลิงก์ตัวเองแล้วอัปเดตตารางของตัว
ประหยัดกว่ามาก แต่แลกมาด้วยโรคประจำตัวที่ชื่อว่า count-to-infinity
7.3 พิสูจน์ด้วยตาเอง — เมื่อทุกคนเชื่อข่าวลือของกันและกัน
MAXH = 8 # เพดานที่ประกาศว่า 'ไปไม่ถึง'
b_next, b_hops = "C", 3 # A เพิ่งตาย · B หันไปเชื่อข่าวจาก C แทน
c_next, c_hops = "B", 2
for rnd in range(1, 7):
print(f" รอบ {rnd}: B บอกว่าถึง A ใน {b_hops} hop (ผ่าน {b_next}) · C บอกว่า {c_hops} hop (ผ่าน {c_next})")
if b_hops >= MAXH or c_hops >= MAXH:
print(f"\n ชนเพดาน {MAXH} -> ประกาศว่า A ไปไม่ถึง")
break
c_hops = b_hops + 1; c_next = "B"
b_hops = c_hops + 1; b_next = "C"ผลรันจริง
รอบ 1: B บอกว่าถึง A ใน 3 hop (ผ่าน C) · C บอกว่า 2 hop (ผ่าน B)
รอบ 2: B บอกว่าถึง A ใน 5 hop (ผ่าน C) · C บอกว่า 4 hop (ผ่าน B)
รอบ 3: B บอกว่าถึง A ใน 7 hop (ผ่าน C) · C บอกว่า 6 hop (ผ่าน B)
รอบ 4: B บอกว่าถึง A ใน 9 hop (ผ่าน C) · C บอกว่า 8 hop (ผ่าน B)
ชนเพดาน 8 -> ประกาศว่า A ไปไม่ถึงA ตายไปตั้งแต่ต้น แต่ B กับ C ใช้เวลาสี่รอบกว่าจะยอมรับ เพราะแต่ละฝ่ายเห็นอีกฝ่ายบอกว่า "ฉันไปถึงได้นะ" โดยไม่รู้ว่าเส้นทางที่อีกฝ่ายอ้างนั้นวิ่งผ่านตัวเองอยู่
💡 อุปมา — เหมือนสองคนยืนยันข่าวลือให้กันไปมา: "ได้ยินจากเขา" / "ก็ได้ยินจากคุณนั่นแหละ" · ไม่มีใครในวงรู้ว่าต้นทางของข่าวคือตัวเอง
การชนเพดานคือทางออกดิบ ๆ ที่ต้องมี — เป็นแนวคิดเดียวกับ TTL ในบทที่ 3 คือ ยอมรับว่าระบบจะเข้าใจผิด แล้ววางตัวหยุดไว้ ไม่ใช่พยายามทำให้ไม่ผิดเลย โปรโตคอลรุ่นใหม่ใช้วิธีที่ฉลาดกว่านั้น (แนบลำดับเวลา หรือแนบเส้นทางทั้งเส้นมาด้วย) แต่ทุกวิธีล้วนแก้ปัญหาเดียวกันนี้
LEVEL 2 · ระดับกลาง
proactive กับ reactive — จ่ายก่อนหรือจ่ายตอนใช้#
สองสำนักในบทที่แล้วต่างกันที่ เก็บอะไร บทนี้ต่างกันที่ เก็บเมื่อไหร่ และมันคือการเลือกที่กระทบทุกอย่าง
8.1 สองวิธีจ่าย
Proactive = รู้ไว้ก่อน ทุกโหนดคุยกันตลอดเวลาเพื่อให้ตารางพร้อมใช้เสมอ พอมีข้อมูลจะส่งก็ส่งได้ทันที · โปรโตคอลตระกูลนี้คือ OLSR และ Babel
Reactive = ค่อยหาตอนต้องใช้ ไม่ส่งอะไรเลยจนกว่าจะมีคนอยากคุย แล้วค่อยยิงคำถามกระจายออกไปหาเส้นทาง · ตัวแทนคือ AODV
8.2 ราคาที่จ่ายมีหน้าตายังไง
NODES = 20
HELLO_BYTES = 64 # ขนาด hello หนึ่งใบ
LSA_BYTES = 220 # ประกาศสภาพลิงก์ของตัวเองหนึ่งใบ
LINK_BPS = 250_000 # ความเร็วอากาศจริงของวิทยุย่านต่ำ
for every in (1, 2, 5, 10, 30):
per_sec = NODES * (HELLO_BYTES / every) + NODES * (LSA_BYTES / (every * 5))
bits = per_sec * 8
print(f"hello ทุก {every:>2} วิ -> {bits:>7,.0f} bps = {bits/LINK_BPS*100:>4.1f}% ของสายอากาศ")ผลรันจริง
hello ทุก 1 วิ -> 17,280 bps = 6.9% ของสายอากาศ
hello ทุก 2 วิ -> 8,640 bps = 3.5% ของสายอากาศ
hello ทุก 5 วิ -> 3,456 bps = 1.4% ของสายอากาศ
hello ทุก 10 วิ -> 1,728 bps = 0.7% ของสายอากาศ
hello ทุก 30 วิ -> 576 bps = 0.2% ของสายอากาศตัวเลขนี้บอกอะไรที่สำคัญมาก: ค่า overhead ของ proactive ไม่ได้แพงอย่างที่กลัวกัน เครือข่าย 20 โหนดที่คุยกันทุก 2 วินาทีใช้อากาศไป 3.5% เท่านั้น
แล้วทำไมยังมีคนใช้ reactive? เพราะในเครือข่ายที่มีโหนดเยอะแต่คุยกันจริงไม่กี่คู่ การรักษาเส้นทางไปยังทุกปลายทางที่ไม่มีใครใช้คือการเผาอากาศทิ้งเปล่า ๆ และค่านี้โตตามจำนวนโหนดคูณกับความถี่ในการประกาศ — ที่ 20 โหนดคือ 3.5% แต่ที่ 200 โหนดในพื้นที่เดียวกัน มันไม่ใช่แค่โตสิบเท่า เพราะประกาศของทุกคนต้องถูกส่งต่อผ่านทุกคนด้วย
8.3 ราคาของ reactive คือเวลา ไม่ใช่แบนด์วิดท์
ปลายทางห่าง 2 hop -> รอ 160 ms ก่อนไบต์แรกจะออก
ปลายทางห่าง 4 hop -> รอ 320 ms
ปลายทางห่าง 6 hop -> รอ 480 ms(คิดจากการยิงถามและรอคำตอบ hop ละราว 40 มิลลิวินาที ไป-กลับ)
สำหรับการโหลดไฟล์ ครึ่งวินาทีไม่มีใครสังเกต แต่สำหรับ การกดปุ่มพูดวิทยุแล้วเสียงต้องออกทันที ครึ่งวินาทีคือความต่างระหว่างใช้ได้กับใช้ไม่ได้ และงานภาคสนามส่วนใหญ่เป็นแบบหลัง
8.4 ทำไมของจริงถึงเลือกทางสายกลาง
โปรโตคอลที่ใช้งานจริงทุกวันนี้แทบไม่มีตัวไหนบริสุทธิ์ทั้งสองแบบ ท่ามาตรฐานคือ proactive กับเพื่อนบ้านใกล้ ๆ · reactive กับปลายทางไกล ๆ — เพราะเพื่อนบ้านใกล้คือคนที่คุณคุยด้วยบ่อยที่สุดและข้อมูลเน่าเร็วที่สุด ส่วนปลายทางไกลนาน ๆ ใช้ที ยอมรอได้
นี่คือตัวอย่างแรกของหลักการที่จะกลับมาอีกในบทที่ 12: อย่าเลือกข้างเมื่อคุณเลือกได้ทั้งสองข้างในสถานการณ์คนละแบบ
เมื่อโหนดขยับ — ทำให้เส้นทางนิ่ง#
ทุกอย่างที่ผ่านมาสมมติว่าค่าที่วัดได้นิ่งพอจะตัดสินใจ บทนี้รื้อสมมติฐานนั้นทิ้ง
9.1 ปัญหาที่เกิดจากการทำงานถูกต้องเกินไป
สมมติมีสองเส้นทางที่ดีพอ ๆ กัน — ETX 1.30 กับ 1.35 ค่าที่วัดได้แกว่งขึ้นลงตามธรรมชาติของคลื่นอยู่แล้ว บางรอบ 1.28 บางรอบ 1.42
ถ้าโหนดทำตามกฎ "เลือกเส้นที่ ETX ต่ำสุด" อย่างซื่อสัตย์ มันจะสลับเส้นทางไปมาทุกครั้งที่ค่าแกว่งข้ามกัน ปรากฏการณ์นี้เรียกว่า route flapping และมันแพงกว่าที่คิด เพราะทุกครั้งที่เส้นทางเปลี่ยน แพ็กเก็ตที่กำลังเดินทางอยู่จะมาถึงไม่เรียงลำดับ ต้องประกาศเส้นทางใหม่ให้เพื่อนบ้านรู้ และการประกาศนั้นก็กินอากาศอีก
ระบบไม่ได้พัง — มันทำตามที่สั่งเป๊ะ ๆ นั่นแหละคือปัญหา
9.2 พิสูจน์ด้วยตาเอง
ทางแก้ชื่อ hysteresis: อย่าเพิ่งย้ายเพราะอีกเส้นดีกว่านิดเดียว ให้ย้ายก็ต่อเมื่อมันดีกว่าเกินเกณฑ์ที่ตั้งไว้
import random
rnd = random.Random(7)
def sample(base): # ETX ที่วัดได้ในรอบนั้น แกว่ง ±0.15
return round(base + rnd.uniform(-0.15, 0.15), 3)
def run(hysteresis):
"""hysteresis = ต้องดีกว่าของเดิมกี่เปอร์เซ็นต์ถึงจะยอมย้าย"""
current, switches = "B", 0
for _ in range(40):
etx = {"B": sample(1.30), "C": sample(1.35)}
other = "C" if current == "B" else "B"
if etx[other] < etx[current] * (1 - hysteresis):
current = other; switches += 1
return switches
for h in (0.00, 0.05, 0.10, 0.20):
print(f"เกณฑ์ต้องดีกว่า {h*100:>4.0f}% -> ย้ายเส้นทาง {run(h):>2} ครั้งใน 40 รอบ")ผลรันจริง
เกณฑ์ต้องดีกว่า 0% -> ย้ายเส้นทาง 14 ครั้งใน 40 รอบ
เกณฑ์ต้องดีกว่า 5% -> ย้ายเส้นทาง 10 ครั้งใน 40 รอบ
เกณฑ์ต้องดีกว่า 10% -> ย้ายเส้นทาง 0 ครั้งใน 40 รอบ
เกณฑ์ต้องดีกว่า 20% -> ย้ายเส้นทาง 0 ครั้งใน 40 รอบไม่มีเกณฑ์เลย = ย้าย 14 ครั้งใน 40 รอบ ทั้งที่ไม่มีอะไรในโลกจริงเปลี่ยนสักอย่าง · ตั้งเกณฑ์ที่ 10% = นิ่งสนิท
9.3 ราคาของความนิ่ง
hysteresis ไม่ได้ฟรี ถ้าเส้นทางปัจจุบันแย่ลงจริง ๆ แต่ยังไม่แย่พอจะข้ามเกณฑ์ โหนดจะยังส่งผ่านเส้นทางที่แย่กว่าต่อไป — คุณจ่ายด้วยคุณภาพเพื่อซื้อความนิ่ง เกณฑ์ 20% ทำให้นิ่งกว่า 10% ก็จริง แต่ก็แปลว่ายอมทนเส้นทางที่แย่กว่าของที่ดีที่สุดอยู่ถึงหนึ่งในห้า
และนี่ยังไม่ใช่ราคาเดียว การกรองค่าให้เรียบ (เช่นเฉลี่ยย้อนหลังหลายรอบ) ทำให้ค่านิ่งขึ้นแต่ตอบสนองช้าลง — โหนดที่หายไปจริงจะถูกตัดออกช้ากว่าเดิม
⚠️ สังเกตว่าเราวนกลับมาที่ trade-off เดิมของบทที่ 4 อีกครั้ง: ความสด แลกกับ ความนิ่ง ทั้งเล่มนี้คือการหาจุดยืนบนเส้นนั้นซ้ำแล้วซ้ำอีกในบริบทต่างกัน
ความจุที่หายไปกับตัวเอง#
บทสั้น แต่เป็นบทที่จะทำให้คุณหงุดหงิดที่สุดในเล่ม เพราะมันบอกว่าทุกอย่างที่ทำมาเก้าบทยังแก้ปัญหาใหญ่ที่สุดไม่ได้
10.1 ตัวเลขที่ทำใจยาก
ในบทที่ 2 เราเห็นแล้วว่าอากาศเป็นของใช้ร่วม โหนดที่อยู่ในระยะได้ยินกันส่งพร้อมกันไม่ได้ ทีนี้ลองดูว่ามันแปลเป็นอะไรเมื่อข้อมูลต้องวิ่งผ่านโซ่หลาย hop
สิ่งที่หลายคนเดา (และเดาผิด) คือ "n hop ก็ได้หนึ่งในสาม n" ความจริงดีกว่านั้น เพราะโหนดที่อยู่ห่างกันพอส่งพร้อมกันได้โดยไม่กวนกัน แม้ใช้คลื่นเดียวกัน — เรียกว่า spatial reuse
RADIO_MBPS = 20.0
REUSE_GAP = 3 # hop ที่ห่างกันตั้งแต่ 3 ช่วงขึ้นไป ส่งพร้อมกันได้แม้ใช้คลื่นเดียวกัน
def chain(hops):
contending = min(hops, REUSE_GAP) # เกินสามช่วงแล้วไม่เพิ่มการแย่งอีก
return RADIO_MBPS / contending
for h in (1, 2, 3, 4, 6, 10, 20):
print(f"{h:>2} hop -> {chain(h):>5.2f} Mbps ({chain(h)/RADIO_MBPS*100:>3.0f}% ของความเร็ววิทยุ)")ผลรันจริง
1 hop -> 20.00 Mbps (100% ของความเร็ววิทยุ)
2 hop -> 10.00 Mbps ( 50% ของความเร็ววิทยุ)
3 hop -> 6.67 Mbps ( 33% ของความเร็ววิทยุ)
4 hop -> 6.67 Mbps ( 33% ของความเร็ววิทยุ)
6 hop -> 6.67 Mbps ( 33% ของความเร็ววิทยุ)
10 hop -> 6.67 Mbps ( 33% ของความเร็ววิทยุ)
20 hop -> 6.67 Mbps ( 33% ของความเร็ววิทยุ)อ่านตารางนี้ให้ครบสองด้าน ข่าวดี: ความเร็วไม่ได้ลดลงเรื่อย ๆ ตามความยาว — โซ่ 20 hop ได้เท่ากับโซ่ 3 hop ⇒ เครือข่ายเมชขยายพื้นที่ออกไปได้โดยไม่ทรุดลงไม่รู้จบ · ข่าวร้าย: มันติดเพดานที่ หนึ่งในสาม ตั้งแต่ hop ที่สาม และไม่มีทางกลับขึ้นไปได้อีกเลย
10.2 ทำไมถึงเป็นแบบนั้น
เพราะเมื่อโหนด B รับข้อมูลจาก A แล้วจะส่งต่อให้ C มันต้องออกอากาศในย่านเดียวกับที่ A กำลังใช้อยู่ ถ้า A ยังส่งชิ้นถัดไปอยู่ ทั้งคู่จะชนกัน — และ C เองก็ส่งต่อให้ D ไม่ได้ในจังหวะเดียวกันด้วย
แต่พอไกลออกไปกว่านั้น การรบกวนจะจางจนไม่สำคัญ ⇒ hop ที่ 1 กับ hop ที่ 4 ส่งพร้อมกันได้จริง นั่นคือที่มาของเลข สาม ในโค้ด: ทุกช่วงสาม hop มีหนึ่งช่วงที่ได้ออกอากาศ
💡 อุปมา — เหมือนส่งของต่อ ๆ กันในทางเดินแคบ คนที่ยืนติดกันขยับพร้อมกันไม่ได้ แต่คนที่อยู่ห่างกันสามช่วงตัวขยับพร้อมกันได้สบาย ⇒ ไม่ว่าแถวจะยาวแค่ไหน อัตราการส่งของก็คงที่ · จุดที่อุปมาใช้ไม่ได้: ในทางเดินจริง ระยะที่ปลอดภัยคือระยะที่มองเห็น แต่ในอากาศ ระยะที่รบกวนกันไกลกว่าระยะที่คุยกันรู้เรื่อง ซึ่งเป็นเหตุผลว่าทำไมของจริงถึงแย่กว่าโมเดลนี้
10.3 ของจริงแย่กว่าโมเดลนี้อีก
ตัวเลข "หนึ่งในสาม" คือเพดานในอุดมคติ ถ้ามีใครสักคนจัดตารางการส่งได้อย่างสมบูรณ์แบบ
งานวัดจริงที่เป็นงานอ้างอิงของสายนี้ — Li, Blake, De Couto, Lee, Morris, Capacity of Ad Hoc Wireless Networks (MobiCom 2001) — สรุปไว้สองบรรทัดที่ควรจำ
- โซ่ในอุดมคติทำได้ 1/3 ของความเร็ว single-hop ตรงกับโมเดลข้างบน
- แต่ 802.11 จริงทำได้แค่ 1/7 เพราะระยะรบกวน (ราว 550 เมตรในการทดลองนั้น) ไกลกว่าระยะสื่อสาร (250 เมตร) เกือบเท่าตัว และกลไก backoff ของ 802.11 เองก็จัดคิวได้ไม่ดีในโครงแบบโซ่
- ที่สำคัญกว่าตัวเลข: กราฟของเขาแบน — โซ่ 20 และ 50 โหนดได้เท่ากับโซ่ 10 โหนด ⇒ ยืนยันว่าเพดานมีจริง ไม่ใช่ความชันที่ลงไปเรื่อย ๆ
⚠️ ดังนั้นเวลาออกแบบจริง อย่าใช้ 1/3 เป็นตัวเลขวางแผน ให้เผื่อไว้ที่ 1/7 ของความเร็วบนกล่อง แล้วค่อยดีใจถ้าได้มากกว่านั้น
10.4 สิ่งที่บทนี้ทิ้งไว้
ทุกอย่างในเก้าบทที่ผ่านมา — ETX, Dijkstra, hysteresis — ช่วยให้คุณเลือกเส้นทางที่ดีที่สุดจากที่มี แต่ไม่มีอันไหนดันเพดานหนึ่งในสามนั้นขึ้นได้เลยแม้แต่บิตเดียว
เพดานนี้ไม่ได้มาจากอัลกอริทึมที่ยังไม่ฉลาดพอ มันมาจากการที่ทุก hop ในโซ่ใช้อากาศก้อนเดียวกัน และดูเหมือนเป็นกฎธรรมชาติที่ไม่มีทางเลี่ยง
บทหน้าจะบอกว่ามันเลี่ยงได้ ด้วยการเปลี่ยนสิ่งที่คำว่า "ก้อนเดียวกัน" หมายถึง
LEVEL 3 · ระดับสูง
หลายคลื่นความถี่ — ทางออกที่เปลี่ยนสมการ#
นี่คือบทที่สำคัญที่สุดในครึ่งหลังของเล่ม และเป็นเหตุผลที่เครือข่ายเมชสมัยใหม่ทำสิ่งที่รุ่นก่อนทำไม่ได้
11.1 เบาะแสจากบทที่แล้ว
เพดานหนึ่งในสามของบทที่ 10 มาจากกติกาข้อเดียว: hop ที่ห่างกันไม่ถึงสามช่วง ส่งพร้อมกันไม่ได้ เพราะใช้ย่านความถี่เดียวกัน
สังเกตคำท้ายประโยค แล้วทางออกก็โผล่มาเอง — ถ้า hop ที่ 1 ใช้คลื่น A และ hop ที่ 2 ใช้คลื่น B ทั้งสองส่งพร้อมกันได้ทันทีทั้งที่ยืนติดกัน เพราะอยู่คนละย่าน ไม่ได้ยินกัน
พูดอีกแบบคือ การเพิ่มช่องคลื่นเข้ามา ทำให้ระยะเว้นที่ต้องการหดลง และถ้ามีช่องมากพอจนระยะเว้นเหลือหนึ่ง ก็แปลว่าไม่มีการแย่งกันเหลืออยู่เลย
11.2 พิสูจน์ด้วยตาเอง
RADIO_MBPS = 20.0
REUSE_GAP = 3 # โซ่คลื่นเดียว: ต้องเว้น 3 ช่วง hop ถึงจะส่งพร้อมกันได้
def chain(hops, channels):
"""มี k ช่องคลื่นสลับวน -> ระยะเว้นที่ต้องการลดลง k เท่า
ถ้า k >= REUSE_GAP แปลว่า hop ติดกันก็ยังส่งพร้อมกันได้หมด ไม่เหลือการแย่งเลย"""
gap = -(-REUSE_GAP // channels) # ปัดขึ้น
return RADIO_MBPS / min(hops, gap)
print(f"{'hop':>4}{'1 ช่อง':>13}{'2 ช่อง':>13}{'3 ช่อง':>13}{'3 ช่องได้เพิ่ม':>16}")
for h in (2, 3, 4, 6, 10):
a, b, c = (chain(h, k) for k in (1, 2, 3))
print(f"{h:>4}{a:>9.2f} Mbps{b:>9.2f} Mbps{c:>9.2f} Mbps{c/a:>13.1f}x")ผลรันจริง
hop 1 ช่อง 2 ช่อง 3 ช่อง 3 ช่องได้เพิ่ม
2 10.00 Mbps 10.00 Mbps 20.00 Mbps 2.0x
3 6.67 Mbps 10.00 Mbps 20.00 Mbps 3.0x
4 6.67 Mbps 10.00 Mbps 20.00 Mbps 3.0x
6 6.67 Mbps 10.00 Mbps 20.00 Mbps 3.0x
10 6.67 Mbps 10.00 Mbps 20.00 Mbps 3.0xสองอย่างที่ควรจ้องในตารางนี้
หนึ่ง — สามช่องคลื่นทำให้เพดานหายไปเลย คอลัมน์สุดท้ายได้ 20 Mbps เท่ากับ single-hop ทุกความยาวโซ่ เพราะระยะเว้นที่ต้องการหดจากสามเหลือหนึ่ง ⇒ ไม่มี hop คู่ไหนต้องรอใครอีก
สอง — และนี่คือข้อที่ต่างจากบทที่แล้วอย่างสิ้นเชิง ตัวคูณไม่จางลงเมื่อโซ่ยาวขึ้น ที่ 10 hop ก็ยังได้ 3 เท่าเท่ากับที่ 3 hop · เทียบกับบทที่ 10 ที่ไม่ว่าจะปรับ routing เก่งแค่ไหนก็ติด 6.67 Mbps อยู่อย่างนั้น
การเพิ่มช่องคลื่นซื้อความจุกลับมาได้ตรง ๆ ซึ่งไม่มีอัลกอริทึม routing ตัวไหนทำได้ — และมันคือความต่างระหว่าง "เลือกของที่มีให้ดีที่สุด" กับ "เปลี่ยนว่ามีอะไรให้เลือก"
⚠️ ตัวเลขชุดนี้เป็นเพดานเชิงอุดมคติแบบเดียวกับบทที่ 10 ของจริงต่ำกว่านี้ เพราะการสลับคลื่นมีต้นทุนเวลาของมันเอง และ 802.11 เองก็จัดคิวได้ไม่ดีอยู่แล้วตามที่เปเปอร์ในบทที่แล้ววัดไว้ · ใช้ตารางนี้เปรียบเทียบ สัดส่วน ไม่ใช่เอาไปวางแผนความจุจริง
11.3 ทำไมมันถึงไม่ใช่แค่ "ใส่วิทยุเพิ่ม"
ถ้ามันง่ายขนาดนั้น ทุกคนคงทำกันหมดแล้ว ความยากย้ายที่ไป ไม่ได้หายไป
หนึ่ง — ต้องมีใครสักคนตัดสินว่า hop ไหนใช้คลื่นไหน และการตัดสินนั้นต้องสอดคล้องกันทั้งเครือข่าย ถ้า B ตั้งใจส่งให้ C ด้วยคลื่น 2 แต่ C ไปฟังคลื่น 3 อยู่ ก็ไม่มีการสื่อสารเกิดขึ้นเลย นี่คือปัญหาการจัดสรรทรัพยากรที่ยากกว่า Dijkstra มาก เพราะคำตอบที่ดีที่สุดของโหนดหนึ่งขึ้นกับคำตอบของเพื่อนบ้าน ซึ่งขึ้นกับคำตอบของเพื่อนบ้านของเพื่อนบ้านอีกที
สอง — คลื่นแต่ละย่านมีบุคลิกไม่เหมือนกัน ย้อนกลับไปดูตัวเลขบทที่ 2: ที่งบกำลังเท่ากัน 900 MHz ไปได้ 26 กม. ส่วน 5800 MHz ไปได้ 4 กม. ⇒ การสลับ hop หนึ่งไปใช้ย่านสูงอาจแปลว่าลิงก์นั้นขาดไปเลย ไม่ใช่แค่เร็วขึ้น การเลือกคลื่นจึงเปลี่ยนโครงสร้างของกราฟที่ Dijkstra ทำงานอยู่ — คุณกำลังเลือกเส้นทางบนแผนที่ที่เปลี่ยนรูปตามการเลือกของตัวเอง
สาม — จำนวนช่องที่ใช้ได้จริงน้อยกว่าที่กฎหมายอนุญาต เพราะช่องที่อยู่ติดกันเกินไปยังรบกวนกันอยู่ และบางย่านมีคนอื่นใช้อยู่ก่อนแล้ว
11.4 ทำไมบทนี้ถึงเป็นจุดพีค
ย้อนกลับไปดูสิ่งที่เล่มนี้บอกไว้ตั้งแต่บทที่ 2: "ไม่มีความถี่ไหนดีที่สุด มีแต่ความถี่ที่เหมาะกับงานคนละแบบ"
ตอนนั้นมันฟังเหมือนคำพูดปลอบใจ ตอนนี้มันคือกลยุทธ์: ใช้ย่านต่ำสำหรับ hop ที่ต้องข้ามระยะไกลและส่งแค่พิกัด ใช้ย่านสูงสำหรับ hop ที่อยู่ใกล้กันแต่ต้องส่งวิดีโอ แล้ววางสองอย่างนี้บนเครือข่ายเดียวกัน
เครือข่ายที่วิ่งคลื่นเดียวต้องเลือกว่าจะไกลหรือจะเร็ว เครือข่ายหลายคลื่นไม่ต้องเลือก — และนั่นคือคำตอบสุดท้ายของปริศนาข้อที่สามที่ปลูกไว้ในบทที่ 4
เมื่อค่าเดียวตัดสินไม่ได้#
หลังบทที่แล้ว เรามีมิติให้คิดมากกว่าหนึ่ง — ETX บอกคุณภาพลิงก์ แต่ไม่รู้เรื่องความจุของคลื่นที่เลือก และไม่รู้เลยว่าโหนดกลางทางแบตเหลือเท่าไหร่
12.1 แปลงทุกอย่างให้เป็นต้นทุนก่อน
เคล็ดลับคือ อย่าพยายามเปรียบเทียบของคนละหน่วย ให้แปลงทุกอย่างเป็นต้นทุนก่อน แล้วค่อยถ่วงน้ำหนัก
ROUTES = [
("A-D-B-F", 3.44, 20.0, 35), # ETX ดีที่สุด แต่ผ่านโหนดที่แบตใกล้หมด
("A-C-F", 4.20, 20.0, 95), # ลิงก์แย่กว่า แต่โหนดแบตเต็ม
("A-E-F", 3.60, 2.0, 90), # ย่านความถี่ต่ำ ไปได้ไกลแต่ช้ากว่าสิบเท่า
]
def score(etx, mbps, batt, w_etx=1.0, w_cap=1.0, w_batt=0.5):
"""ยิ่งน้อยยิ่งดี — ทุกมิติถูกแปลงเป็นต้นทุนก่อนถ่วงน้ำหนัก"""
cost_etx = etx # เป็นต้นทุนอยู่แล้ว
cost_cap = 20.0 / mbps # ช้ากว่าฐาน 20 Mbps กี่เท่า
cost_batt = (100 - batt) / 100 * 4 # แบตยิ่งน้อย ต้นทุนยิ่งสูง
return w_etx*cost_etx + w_cap*cost_cap + w_batt*cost_batt
for name, etx, mbps, batt in ROUTES:
print(f"{name:<10}ETX {etx:>5.2f} {mbps:>5.1f} Mbps แบต {batt:>3}% -> คะแนน {score(etx,mbps,batt):>6.2f}")
print("")
for w in (0.0, 0.5, 2.0):
pick = min(ROUTES, key=lambda r: score(r[1], r[2], r[3], w_batt=w))
print(f"ให้แบตหนัก {w:>3.1f} -> เลือก {pick[0]}")ผลรันจริง
A-D-B-F ETX 3.44 20.0 Mbps แบต 35% -> คะแนน 5.74
A-C-F ETX 4.20 20.0 Mbps แบต 95% -> คะแนน 5.30
A-E-F ETX 3.60 2.0 Mbps แบต 90% -> คะแนน 13.80
ให้แบตหนัก 0.0 -> เลือก A-D-B-F
ให้แบตหนัก 0.5 -> เลือก A-C-F
ให้แบตหนัก 2.0 -> เลือก A-C-Fถ้าดู ETX อย่างเดียว A-D-B-F ชนะ (3.44 ต่อ 4.20) แต่พอนับแบตเข้ามาด้วย คำตอบพลิกเป็น A-C-F ทันที เพราะการส่งงานทั้งหมดผ่านโหนดที่เหลือแบต 35% แปลว่าอีกไม่นานเส้นทางนั้นจะหายไปทั้งเส้น
12.2 น้ำหนักคือการประกาศว่าอะไรสำคัญ
บรรทัดที่ควรจ้องคือสามบรรทัดสุดท้าย อัลกอริทึมไม่เปลี่ยนเลย โค้ดไม่เปลี่ยนสักบรรทัด แต่คำตอบเปลี่ยนเพราะน้ำหนักเปลี่ยน
นี่คือจุดที่วิศวกรรมกลายเป็นการตัดสินใจเชิงนโยบาย: ค่าน้ำหนักคือการเขียนลงไปเป็นตัวเลขว่า ในภารกิจนี้ อะไรสำคัญกว่าอะไร ภารกิจสี่ชั่วโมงกับภารกิจสามวันควรใช้น้ำหนักคนละชุด และนั่นเป็นเรื่องที่คนออกแบบระบบต้องตอบ ไม่ใช่เรื่องที่อัลกอริทึมตอบได้เอง
12.3 กับดักที่ต้องระวัง
⚠️ metric ที่ซับซ้อนขึ้น ทำให้เกิด flapping ง่ายขึ้น เพราะตอนนี้มีตัวแปรสามตัวที่แกว่งได้พร้อมกัน แทนที่จะมีตัวเดียว ⇒ ทุกครั้งที่เพิ่มมิติ ต้องกลับไปทบทวน hysteresis ในบทที่ 9 ใหม่เสมอ
⚠️ มิติที่วัดไม่ได้จริง อย่าใส่ ถ้าโหนดรายงานแบตไม่ตรง (ซึ่งเกิดบ่อยมากกับแบตลิเธียมที่แรงดันไม่เป็นเส้นตรง) การใส่แบตเข้าสมการทำให้ตัดสินใจแย่ลง ไม่ใช่ดีขึ้น · ค่าที่ผิดแต่ดูน่าเชื่อ อันตรายกว่าไม่มีค่าเลย
CoT และ ATAK — เอาทั้งทีมขึ้นแผนที่เดียวกัน#
สิบสองบทที่ผ่านมาทำให้ข้อมูลเดินทางถึงกันได้ บทนี้ตอบว่าแล้วจะส่งอะไร เพื่อให้คนในสนามเห็นภาพเดียวกัน
13.1 ปัญหาที่ไม่ใช่ปัญหาเครือข่าย
ต่อให้เครือข่ายสมบูรณ์แบบ ถ้าแต่ละเครื่องพูดคนละภาษาก็ยังใช้งานไม่ได้ วิทยุยี่ห้อหนึ่งส่งพิกัดเป็น JSON อีกยี่ห้อส่งเป็นไบนารีของตัวเอง — สองทีมที่ถือคนละยี่ห้อจึงเห็นกันไม่ได้ ทั้งที่คลื่นถึงกันดี
ATAK (Android Team Awareness Kit) คือแอปแผนที่ที่แสดงตำแหน่งของทุกคนในทีมแบบเรียลไทม์ และภาษาที่มันใช้ชื่อ CoT (Cursor on Target) ซึ่งเป็น XML รูปแบบง่าย ๆ ที่กลายเป็นภาษากลางของงานสายนี้ไปแล้ว
13.2 พิสูจน์ด้วยตาเอง — สร้างข้อความจริงหนึ่งใบ
import xml.etree.ElementTree as ET
def make_cot(uid, lat, lon, cot_type="a-f-G-U-C", callsign="RESCUE-1"):
"""สร้างข้อความ Cursor on Target หนึ่งใบ"""
ev = ET.Element("event", {
"version": "2.0", "uid": uid, "type": cot_type,
"time": "2026-08-31T09:15:00Z", # เวลาที่สร้าง
"start": "2026-08-31T09:15:00Z", # เริ่มมีผล
"stale": "2026-08-31T09:20:00Z", # หมดอายุ — ครบแล้วหมุดจางหายเอง
"how": "m-g",
})
ET.SubElement(ev, "point", {"lat": f"{lat}", "lon": f"{lon}", "hae": "120.0",
"ce": "9999999.0", "le": "9999999.0"})
detail = ET.SubElement(ev, "detail")
ET.SubElement(detail, "contact", {"callsign": callsign})
ET.SubElement(detail, "__group", {"name": "Cyan", "role": "Team Member"})
return ET.tostring(ev, encoding="unicode")
xml = make_cot("RESCUE-1-abc123", 13.6512, 100.4961)
root = ET.fromstring(xml)
pt = root.find("point")
print(f"{root.find('detail/contact').get('callsign')} อยู่ที่ {pt.get('lat')}, {pt.get('lon')}")
print(f"ชนิด {root.get('type')} · หมดอายุ {root.get('stale')} · ขนาด {len(xml.encode('utf-8'))} ไบต์")ผลรันจริง
RESCUE-1 อยู่ที่ 13.6512, 100.4961
ชนิด a-f-G-U-C · หมดอายุ 2026-08-31T09:20:00Z · ขนาด 334 ไบต์13.3 สามอย่างในรูปแบบนี้ที่ออกแบบมาเพื่อเครือข่ายแบบเมชโดยเฉพาะ
หนึ่ง — stale คือการยอมรับว่าข้อมูลเน่า ทุกข้อความบอกวันหมดอายุของตัวเองมาด้วย ถ้าไม่มีใบใหม่มาก่อนถึงเวลานั้น หมุดบนแผนที่จะจางหายไปเอง · นี่คือประโยคแก่นของบทที่ 4 ที่ถูกเขียนลงไปในรูปแบบข้อมูลโดยตรง — ระบบไม่แสร้งว่ารู้ตำแหน่งปัจจุบันของทุกคน มันแสดงแค่สิ่งที่รู้ล่าสุดพร้อมบอกว่ารู้มานานแค่ไหน
สอง — 334 ไบต์ต่อใบ เล็กพอที่จะยัดลงวิทยุย่านต่ำที่ช้ามากได้ ถ้าทีมหกคนส่งตำแหน่งทุก 5 วินาที นั่นคือ 6 × 334 × 8 ÷ 5 = ราว 3,200 bps ซึ่งวิทยุ 250 kbps ในบทที่ 8 รับไหวสบาย ๆ
สาม — uid คงที่ต่อคน ไม่ใช่ต่อข้อความ ทำให้ใบใหม่ทับใบเก่าได้เอง แผนที่จึงไม่มีหมุดซ้ำของคนเดียวกันเรียงกันเป็นแถว
13.4 ข้อควรระวังที่กระทบ routing ตรง ๆ
ข้อความตำแหน่งเป็นข้อมูลที่ ส่งซ้ำได้ ไม่จำเป็นต้องถึงทุกใบ — ใบที่หายไปจะถูกแทนที่ด้วยใบถัดไปในอีกห้าวินาทีอยู่แล้ว การพยายามรับประกันว่าทุกใบต้องถึง (ส่งซ้ำจนกว่าจะได้ใบตอบรับ) จึงเป็นการเผาอากาศทิ้งเปล่า ๆ กับข้อมูลที่กำลังจะหมดอายุอยู่แล้ว
ขณะที่เสียงพูดหรือคำสั่งภารกิจเป็นคนละเรื่อง — ใบที่หายคือเนื้อหาที่หายจริง
⇒ เครือข่ายภาคสนามที่ออกแบบดีจะแยกชั้นความสำคัญของข้อมูล แล้วปฏิบัติกับมันคนละแบบ ไม่ใช่ยัดทุกอย่างลงท่อเดียวแล้วหวังว่าจะพอ
LEVEL 4 · ระดับเทพ
ทดสอบโดยไม่มีวิทยุสักตัว#
ถ้าอ่านมาถึงตรงนี้แล้วอยากลองทำจริง อุปสรรคแรกคือคุณไม่มีวิทยุสิบตัวและไม่มีภูเขาให้เดินขึ้น บทนี้บอกว่าไม่จำเป็นต้องมี
14.1 ทำไมการจำลองถึงคุ้มกว่าที่คิด
ปัญหาสำคัญเกือบทั้งหมดในเล่มนี้ — flapping, count-to-infinity, ความจุที่หายไปกับตัวเอง, เส้นทางย้ายเมื่อโหนดขยับ — เป็นปัญหาเชิงตรรกะ ไม่ใช่ปัญหาเชิงคลื่น ⇒ ทดสอบได้บนโน้ตบุ๊กตัวเดียว
สิ่งที่จำลองไม่ได้คือคลื่นจริงในภูมิประเทศจริง แต่นั่นเป็นงานของขั้นถัดไป ไม่ใช่ข้ออ้างที่จะไม่ทดสอบตรรกะก่อน
14.2 ตัวจำลองทั้งตัว
import math, heapq
RANGE_M = 300.0 # ระยะที่ยังคุยกันพอได้
class Node:
def __init__(self, name, x, y, vx, vy):
self.name, self.x, self.y, self.vx, self.vy = name, x, y, vx, vy
def step(self, dt):
self.x += self.vx*dt; self.y += self.vy*dt
def etx_between(a, b):
"""ระยะ -> ETX · เกินระยะ = ไม่มีลิงก์"""
d = math.hypot(a.x-b.x, a.y-b.y)
if d > RANGE_M: return None
ratio = max(0.05, 1.0 - (d/RANGE_M)**2) # ใกล้ = ส่งถึงเกือบทุกใบ
return round(1.0/(ratio*ratio), 2)
def route(nodes, src, dst):
idx = {n.name: n for n in nodes}
dist = {src: 0.0}; prev = {}; pq = [(0.0, src)]
while pq:
d, u = heapq.heappop(pq)
if u == dst: break
if d > dist.get(u, float("inf")): continue
for n in nodes:
if n.name == u: continue
w = etx_between(idx[u], n)
if w is None: continue
nd = d + w
if nd < dist.get(n.name, float("inf")):
dist[n.name] = nd; prev[n.name] = u; heapq.heappush(pq, (nd, n.name))
if dst not in dist: return None, None
p = [dst]
while p[-1] in prev: p.append(prev[p[-1]])
return list(reversed(p)), dist[dst]
nodes = [Node("A", 0, 0, 0, 0), # ฐาน อยู่กับที่
Node("B", 250, 0, 0, 8), # เดินขึ้นเหนือ 8 เมตรต่อวินาที
Node("E", 240, -120, 0, 0), # อยู่กับที่ เป็นทางสำรอง
Node("C", 470, -60, 0, 0),
Node("D", 690, -80, 0, 0)]
for t in range(0, 61, 10):
p, cost = route(nodes, "A", "D")
shown = " -> ".join(p) if p else "ส่งไม่ถึง"
print(f"{t:>3} วิ {shown:<22} ETX {round(cost,2) if cost else '-'}")
for n in nodes: n.step(10)ผลรันจริง
0 วิ A -> B -> C -> D ETX 21.09
10 วิ A -> B -> E -> C -> D ETX 33.43
20 วิ A -> E -> C -> D ETX 36.99
30 วิ A -> E -> C -> D ETX 36.99
40 วิ A -> E -> C -> D ETX 36.99
50 วิ A -> E -> C -> D ETX 36.99
60 วิ A -> E -> C -> D ETX 36.99อ่านผลทีละบรรทัด: วินาทีที่ 0 เส้นทางวิ่งผ่าน B ตรง ๆ · วินาทีที่ 10 B เดินห่างจนลิงก์แย่ลง เส้นทางยาวขึ้นเป็น 4 hop เพื่อเลี่ยงลิงก์ที่กำลังเสื่อม · วินาทีที่ 20 B หลุดออกนอกระยะ เครือข่ายย้ายไปใช้ E เป็นการถาวรและนิ่งตั้งแต่นั้น
เครือข่ายซ่อมเส้นทางตัวเองโดยไม่มีใครสั่ง — และคุณเพิ่งเห็นมันเกิดขึ้นบนหน้าจอตัวเองโดยไม่ต้องมีวิทยุสักตัว
14.3 ต่อยอดจากตรงนี้ยังไง
ตัวจำลองข้างบนคือโครงที่เติมได้ทีละชั้น และแต่ละชั้นคือบทหนึ่งในเล่มนี้:
- เพิ่มการหลุดหายของแพ็กเก็ตแบบสุ่มตามค่า ETX แล้ววัดว่าส่งถึงจริงกี่เปอร์เซ็นต์ (บทที่ 4)
- เพิ่ม hello กับเส้นตาย แล้วดูว่ากว่าจะรู้ว่า B หายใช้เวลากี่วินาที (บทที่ 5)
- เพิ่ม hysteresis แล้วนับจำนวนครั้งที่เส้นทางเปลี่ยนตลอดการทดลอง (บทที่ 9)
- เพิ่มช่องคลื่นแล้ววัดความจุตามสูตรบทที่ 11 — และลองปรับ
REUSE_GAPดูว่าเพดานขยับตามไหม (บทที่ 10–11)
เคล็ดลับ: เพิ่มทีละชั้นและวัดผลทุกครั้ง ถ้าเพิ่มสองชั้นพร้อมกันแล้วผลเปลี่ยน คุณจะไม่รู้ว่าชั้นไหนทำ
จากจำลองสู่สนามจริง#
บทสุดท้าย ว่าด้วยสิ่งที่สิบสี่บทที่ผ่านมาไม่ได้บอก และเป็นสิ่งที่ตัวจำลองไม่มีวันบอกคุณได้
15.1 ห้าอย่างที่โมเดลโกหกเสมอ
หนึ่ง — ระยะไม่ใช่วงกลม ทุกโค้ดในเล่มนี้ตัดสินว่ามีลิงก์หรือไม่จากระยะทางล้วน ๆ ซึ่งไม่มีอยู่จริงในธรรมชาติ ของจริงเป็นหย่อม ๆ ตามภูมิประเทศ บางทีคุณคุยกับคนที่อยู่ไกล 2 กม. ได้ แต่คุยกับคนที่อยู่หลังเนินห่าง 200 เมตรไม่ได้
สอง — ลิงก์ไม่สมมาตร A ได้ยิน B ชัด ไม่ได้แปลว่า B ได้ยิน A ชัด เพราะกำลังส่ง เสาอากาศ และสภาพรอบตัวของสองฝั่งไม่เท่ากัน · นี่คือเหตุผลที่ ETX ต้องคูณสองทิศตั้งแต่บทที่ 4 ไม่ใช่วัดทิศเดียวแล้วยกกำลังสอง
สาม — มีคนอื่นในอากาศเสมอ สนามจริงมีวิทยุของหน่วยอื่น มีสัญญาณรบกวนจากอุปกรณ์ที่ไม่มีใครรู้ว่ามาจากไหน ตัวจำลองมีแต่โหนดของเราเอง
สี่ — แบตหมดเร็วกว่าที่คำนวณ ความเย็น ความร้อน และการส่งกำลังสูงกินแบตเร็วกว่าตัวเลขบนกระดาษเสมอ
ห้า — คนใช้งานไม่ทำตามที่ออกแบบไว้ โหนดที่คุณออกแบบให้เป็นตัวส่งต่อกลางทาง อาจถูกใครสักคนหยิบใส่กระเป๋าแล้วเดินออกจากพื้นที่
15.2 ลำดับที่ควรทำถ้าจะลงมือจริง
- จำลองตรรกะให้ผ่านก่อน — flapping ต้องหยุด เส้นทางต้องซ่อมตัวเองได้ อย่าเพิ่งแตะฮาร์ดแวร์
- ทดสอบสองโหนดบนโต๊ะ — พิสูจน์ว่ารูปแบบข้อความส่งถึงกันจริงและ ATAK อ่านออก (บทที่ 13)
- ทดสอบสามโหนดที่บังคับให้ต้องกระโดด — วางให้ตัวริมสองตัวคุยกันตรงไม่ได้ นี่คือการทดสอบ mesh ครั้งแรกที่มีความหมาย
- ออกภาคสนามพร้อมเครื่องมือวัด — บันทึก ETX จริงเทียบกับที่โมเดลทำนาย ส่วนต่างตรงนี้คือความรู้ที่หาจากที่อื่นไม่ได้
- แก้โมเดลให้ตรงกับที่วัดได้ แล้ววนกลับข้อ 1
⚠️ ข้อ 5 คือข้อที่คนข้ามบ่อยที่สุด ถ้าออกภาคสนามแล้วไม่เอาผลกลับมาแก้โมเดล การจำลองรอบต่อไปก็ยังโกหกเรื่องเดิม
15.3 กลับไปที่ประโยคเดียวของเล่มนี้
ลิงก์ในเมชไม่ใช่สาย แต่เป็นค่าประมาณที่เน่าตามเวลา
ทุกกลไกที่ผ่านมาคือคำตอบต่อประโยคนี้ในมุมต่างกัน — hello สู้กับความเน่า · ETX สู้กับความเป็นค่าประมาณ · hysteresis ยอมรับว่าค่าที่แกว่งไม่ใช่ความจริงที่เปลี่ยน · หลายคลื่นเปลี่ยนสนามแข่งทั้งสนาม · และ stale ใน CoT เขียนความจริงข้อนี้ลงไปในข้อมูลตรง ๆ ว่า "นี่คือสิ่งที่ฉันรู้ล่าสุด ไม่ใช่สิ่งที่เป็นอยู่ตอนนี้"
ถ้าจะเหลือประโยคเดียวจากทั้งเล่ม ขอให้เป็นประโยคนี้ เพราะเวลาเจอปัญหาที่เล่มนี้ไม่ได้พูดถึง คำถามแรกที่ควรถามคือ "ตรงนี้ระบบกำลังเชื่ออะไรที่มันไม่มีทางรู้แน่นอน และมันเชื่อมานานแค่ไหนแล้ว"
อ่านต่อ: System Design#
เมชแก้ปัญหาว่าข้อมูลจะเดินทางถึงกันได้ยังไงเมื่อไม่มีโครงสร้างพื้นฐาน แต่พอข้อมูลถึงปลายทางแล้ว คำถามถัดไปคือระบบที่รับมันไว้ควรถูกออกแบบยังไง — เก็บที่ไหน ขยายตัวยังไง และพังแล้วกู้กลับมาแบบไหน
System Design Handbook — ออกแบบระบบให้รองรับโลกจริงตั้งแต่เซิร์ฟเวอร์เดียวถึง production
ถ้าเนื้อหานี้มีประโยชน์ —เลี้ยงกาแฟสักแก้ว