ข้ามไปยังเนื้อหา
Tayakorn
← คู่มือทั้งหมด
devอัปเดต 2026-08-31อ่านฟรี

คู่มือ Mesh Networking — สื่อสารในที่ที่ไม่มีเสาสัญญาณ

เข้าใจว่าเครือข่ายที่ไม่มีเสาหาเส้นทางกันเองได้ยังไง ตั้งแต่คลื่นวิทยุจนถึง routing หลายคลื่น อ่านจบแล้วจำลองเครือข่ายเองได้ด้วย Python บนโน้ตบุ๊กตัวเดียว ไม่ต้องมีวิทยุ

#mesh-network#routing#wireless#olsr#atak

คู่มือที่พาคุณจากคนไม่เคยแตะคลื่นวิทยุและไม่เคยอ่าน 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

สามลิงก์นั้นคือสถานการณ์แบบนี้ และผลที่ได้ขัดกับสัญชาตญาณเต็ม ๆ

ลิงก์สภาพจริงRSSIETX
A-Bใกล้ ไม่มีอะไรบัง−551.05 ดีที่สุด
A-Cสัญญาณแรงกว่า แต่มีคนใช้ย่านนี้เยอะ−52 แรงที่สุด2.10 แย่ที่สุด
A-Dสัญญาณอ่อนกว่ามาก แต่ย่านว่าง−781.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 ปริศนาที่จะไขในบทหลัง

จากประโยคแก่นเดียวนี้ มีคำถามสามข้อที่ตอบไม่ได้ด้วยความรู้แค่บทนี้ จดไว้แล้วไปต่อ

  1. ถ้าค่าเน่าตลอด แล้วจะรู้ได้ยังไงว่าเพื่อนบ้านคนไหนยังอยู่ → บทที่ 5
  2. ถ้าเส้นทางเปลี่ยนทุกครั้งที่ค่าขยับนิดเดียว เครือข่ายจะไม่สั่นตลอดเวลาหรือ → บทที่ 9
  3. ถ้าทุก 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 ลำดับที่ควรทำถ้าจะลงมือจริง

  1. จำลองตรรกะให้ผ่านก่อน — flapping ต้องหยุด เส้นทางต้องซ่อมตัวเองได้ อย่าเพิ่งแตะฮาร์ดแวร์
  2. ทดสอบสองโหนดบนโต๊ะ — พิสูจน์ว่ารูปแบบข้อความส่งถึงกันจริงและ ATAK อ่านออก (บทที่ 13)
  3. ทดสอบสามโหนดที่บังคับให้ต้องกระโดด — วางให้ตัวริมสองตัวคุยกันตรงไม่ได้ นี่คือการทดสอบ mesh ครั้งแรกที่มีความหมาย
  4. ออกภาคสนามพร้อมเครื่องมือวัด — บันทึก ETX จริงเทียบกับที่โมเดลทำนาย ส่วนต่างตรงนี้คือความรู้ที่หาจากที่อื่นไม่ได้
  5. แก้โมเดลให้ตรงกับที่วัดได้ แล้ววนกลับข้อ 1

⚠️ ข้อ 5 คือข้อที่คนข้ามบ่อยที่สุด ถ้าออกภาคสนามแล้วไม่เอาผลกลับมาแก้โมเดล การจำลองรอบต่อไปก็ยังโกหกเรื่องเดิม

15.3 กลับไปที่ประโยคเดียวของเล่มนี้

ลิงก์ในเมชไม่ใช่สาย แต่เป็นค่าประมาณที่เน่าตามเวลา

ทุกกลไกที่ผ่านมาคือคำตอบต่อประโยคนี้ในมุมต่างกัน — hello สู้กับความเน่า · ETX สู้กับความเป็นค่าประมาณ · hysteresis ยอมรับว่าค่าที่แกว่งไม่ใช่ความจริงที่เปลี่ยน · หลายคลื่นเปลี่ยนสนามแข่งทั้งสนาม · และ stale ใน CoT เขียนความจริงข้อนี้ลงไปในข้อมูลตรง ๆ ว่า "นี่คือสิ่งที่ฉันรู้ล่าสุด ไม่ใช่สิ่งที่เป็นอยู่ตอนนี้"

ถ้าจะเหลือประโยคเดียวจากทั้งเล่ม ขอให้เป็นประโยคนี้ เพราะเวลาเจอปัญหาที่เล่มนี้ไม่ได้พูดถึง คำถามแรกที่ควรถามคือ "ตรงนี้ระบบกำลังเชื่ออะไรที่มันไม่มีทางรู้แน่นอน และมันเชื่อมานานแค่ไหนแล้ว"

อ่านต่อ: System Design#

เมชแก้ปัญหาว่าข้อมูลจะเดินทางถึงกันได้ยังไงเมื่อไม่มีโครงสร้างพื้นฐาน แต่พอข้อมูลถึงปลายทางแล้ว คำถามถัดไปคือระบบที่รับมันไว้ควรถูกออกแบบยังไง — เก็บที่ไหน ขยายตัวยังไง และพังแล้วกู้กลับมาแบบไหน

System Design Handbook — ออกแบบระบบให้รองรับโลกจริงตั้งแต่เซิร์ฟเวอร์เดียวถึง production

ถ้าเนื้อหานี้มีประโยชน์ —เลี้ยงกาแฟสักแก้ว