คาบ 9 — WiFi และเครือข่ายพื้นฐาน

บอร์ดของเราออกจากโต๊ะทำงาน แล้วไปมีที่อยู่ในเครือข่าย

คาถาประจำคาบ: ต่อไม่ติดไม่ใช่เรื่องดวง — มันมีลำดับขั้น และทุกขั้นวัดได้

ดูของจริงก่อน — เมนู Wi-Fi Setting ที่มากับเครื่อง

Wi-Fi Setting AIoT-Class -48 dBm Office-2.4G -63 dBm Lab-Guest -79 dBm CAFE_5G -86 dBm Scan Connect 1 · สแกนหาคลื่น 2 · เลือก + ใส่รหัส 3 · ได้เลข IP เห็นชื่อวง + ความแรง รอ ไม่ใช่ค้าง ไอคอนบนแถบบนติด แปลว่ามีที่อยู่แล้ว

เมนูนี้อยู่บนบอร์ดมาตั้งแต่คาบ 1 แล้ว วันนี้เราจะ สร้างมันขึ้นมาเองด้วย Python และทำให้มันบอกได้มากกว่าที่หน้าจอเดิมบอก

สามขั้นบนจอนี้คือสามบรรทัดในโค้ดของเรา — wifi.scan() · wifi.connect() · wifi.ip()

ทำไม · คืออะไร · ทำยังไง — แผนที่ของคาบนี้

คำถาม คำตอบของคาบนี้ อยู่ช่วงไหน
Why คาบ 2 ก็ต่อ WiFi ติดไปแล้ว ทำไมต้องกลับมาเรียนเรื่องเดิมอีก เพราะ "ต่อเน็ตติด" กับ "ต่อเน็ตใช้ได้" เป็นคนละเรื่อง และวันที่ของจริงไม่ทำงาน คนที่ตอบได้ว่า ขาดตรงไหนในห้าช่วง คือคนที่แก้ได้ · ระบบที่บอกได้ว่าพังตรงไหน มีค่ากว่าระบบที่บอกแค่ว่าพัง ครึ่งแรก · dBm · DHCP · ping สองปลายทาง
What มีอะไรให้ใช้บ้าง โมดูล wifi ทั้งแปดชื่อ — หกตัวที่โครงหลักเรียกจริง บวก disconnect() กับ softap() ที่ต้องเคยลองมือ สไลด์บัญชีแปดชื่อ + สไลด์สองตัวที่โครงหลักไม่ได้เรียก
How ประกอบยังไงให้ใช้งานได้จริง สแกน → เรียงตาม RSSI → ต่อ (บรรทัดเดียวที่บล็อกได้ถึง 85 วินาที ต้องบอกผู้ใช้ก่อน) → ping สองปลายทางแล้วอ่านผลเป็นคู่ แปดไฟล์ตัวอย่าง + ใบฝึก

ปลายทางที่จับต้องได้ — หน้าสถานะเครือข่ายสองแผงบนจอเดิมของคาบ 8: ตาราง สี่คอลัมน์ (SSID · dBm · ช่อง · รหัส) เรียงจากแรงไปอ่อน และแผงขวาที่มี ไฟสถานะลิงก์สองดวง · SSID · IP · ping สองปลายทาง · มาตรวัด dBm พร้อมพิสัย ที่อัปเดตตัวเองทุก 3 วินาที พร้อมปุ่ม สแกนใหม่ ให้สั่งได้เอง

คาบ 8 จอของเรารายงานสิ่งที่อยู่บนบอร์ด · คาบนี้จอเริ่มรายงานสิ่งที่อยู่นอกบอร์ด

เป้าหมายของคาบนี้ — ต่อยอดแดชบอร์ดคาบ 8

IMU Chart Compass CapSense Pot NETWORK STATUS SSID · IP · ping ms การ์ดที่เราจะเพิ่มวันนี้ คาบ 8 = บอร์ดรู้จักตัวเอง · คาบ 9 = บอร์ดรู้จักโลกที่มันอยู่ จากนี้ไปอีกสามคาบ ข้อมูลจะเดินออกจากบอร์ดไปหาคนอื่น
  1. อ่านค่า RSSI เป็น dBm ได้ และบอกได้ว่า −45 กับ −85 ต่างกันแค่ไหน (ไม่ใช่ "ต่างกันนิดหน่อย")
  2. เรียก wifi.scan() แล้ว แกะข้อมูลจาก tuple ได้ถูกช่อง
  3. รู้ว่า wifi.connect() ที่คาบ 2 เคยเรียกไปแล้ว ข้างในมันทำอะไรอยู่ ตอนที่มันเงียบไปเป็นนาที
  4. วินิจฉัยการเชื่อมต่อด้วย wifi.ping() — แยกให้ออกว่าปัญหาอยู่ในห้องเราหรืออยู่นอกห้อง
  5. ประกอบทั้งหมดเป็น การ์ดสถานะเครือข่ายบนหน้าจอเดิมจากคาบ 8
  6. เรียกได้ครบทั้ง แปดชื่อ ของโมดูล wifi และบอกได้ว่าแต่ละตัวมีไว้ทำอะไร

คาบ 2 น้อง ๆ ต่อเน็ตติดไปแล้ว และเห็น wifi.connect() wifi.ip() wifi.is_connected() wifi.scan() wifi.status() ผ่านตามาครบ — แต่คาบนั้นคือการพาทัวร์ทั้งเส้น ไม่ได้ลงลึกทีละตัว วันนี้จึงเป็นการ เปิดฝากล่องเดิมออกดู: บรรทัดที่เคยรันผ่านนั้นวิ่งผ่านอะไรบ้างกว่าจะได้เลข IP ทำไมบางครั้งช้าจนน่าตกใจ ตัวเลขที่คืนมาแปลว่าอะไร และมันโกหกเราตรงไหนได้บ้าง

ปลายทางของคาบนี้ — หน้าสถานะเครือข่ายของทีม

หน้าจอจากการรันโค้ดเฉลย รุ่นก่อนปรับหน้าจอ บน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ไม่ใช่ภาพวาดและไม่ใช่ mock-up · รุ่นปัจจุบันเปลี่ยนแผงซ้ายเป็น ui.Table และเพิ่มไฟสถานะกับมาตรวัดในแผงขวาแล้ว ภาพชุดใหม่ยังไม่ได้ถ่าย

โครงของหน้าจอรุ่นปัจจุบัน อ่านจากซ้ายไปขวา

  • ซ้าย ตาราง สี่คอลัมน์ SSID · dBm · ช่อง · รหัส เรียงจากแรงไปอ่อน — หัวตารางบวกสามแถวพอดีความสูง 288
  • ขวา ไฟสถานะสองดวง (ต่ออยู่ / ยังไม่ต่อ) แล้วต่อด้วย ping สองปลายทาง · SSID กับ IP อยู่แถบบน
  • ล่างขวา มาตรวัด dBm ของวงที่ทีมต่ออยู่ — แถบที่ขยับวางทับไม้บรรทัดที่บอกพิสัย -90 ถึง -40
  • บนขวาคือปุ่ม สแกนใหม่ — หน้าสถานะที่แตะสั่งอะไรไม่ได้เลย คือจอ ไม่ใช่แผงควบคุม

ต่อเน็ตติดกับต่อเน็ตใช้ได้เป็นคนละเรื่อง หน้านี้แยกสองเรื่องนั้นให้เห็นในจอเดียว

เข้าใจฮาร์ดแวร์ · ทำไมความแรงสัญญาณถึงเป็นเลขติดลบ

หน้าจอเครื่องนี้ไม่ได้เขียนว่า "สัญญาณ 66%" แต่เขียนว่า −74 dBm ซึ่งเป็นหน่วยที่วัดกำลังจริง ๆ ของคลื่นที่เสาอากาศรับได้

PdBm=10log⁡10 ⁣(P1 mW)P_{\text{dBm}} = 10\log_{10}\!\left(\frac{P}{1\ \text{mW}}\right)

อ่านเป็นภาษาคน: เอากำลังที่รับได้มาเทียบกับ 1 มิลลิวัตต์ แล้วบีบด้วยลอการิทึม เพราะช่วงค่ามันกว้างมากจนเขียนเป็นเลขธรรมดาไม่ไหว

ตัวเลขจริง: ที่ −67 dBm กำลังที่เสาอากาศรับได้คือ 10−6.710^{-6.7} mW ≈ 0.2 นาโนวัตต์ — เล็กกว่ามิลลิวัตต์มหาศาล ลอการิทึมจึงออกมาติดลบเสมอ

0 dBm = 1 mW พอดี ซึ่งแรงกว่าที่ WiFi รับได้จริงหลายล้านเท่า เราจึงไม่มีวันเห็นเลขบวกบนบอร์ด

ภาพ: TaBaZzz / Wikimedia Commons — CC BY-SA 4.0 · หน้าจอสถานะสัญญาณที่รายงานเป็น dBm

เลขติดลบไม่ได้แปลว่าผิดปกติ — มันคือธรรมชาติของหน่วยที่วัดของเล็ก ๆ เทียบกับของใหญ่

สเกล dBm — ทุก 10 dB คือสิบเท่า ไม่ใช่สิบหน่วย

ยิ่งไปทางขวา สัญญาณยิ่งอ่อน — และอ่อนแบบทวีคูณ -30 -40 -50 -60 -70 -80 -90 5 ขีด 4 ขีด 3 ขีด 2 ขีด 1 ขีด 1 µW 1 nW 1 pW −50 แรงกว่า −80 อยู่ 1000 เท่า จุดสีน้ำเงิน = เราเดินออกห่างจากเราเตอร์ทีละก้าว

เกณฑ์ห้าขีดข้างบนไม่ได้คิดขึ้นเอง — เป็น เกณฑ์ชุดเดียวกับที่หน้าจอ Wi-Fi Setting ของบอร์ดใช้จริง (−50 / −60 / −70 / −80 / −90)

ตัวเลขที่ช่างเครือข่ายใช้กันหน้างาน: ดีกว่า −60 คือสบายทุกงาน · −67 คือขีดจำกัดของงานที่ต้องต่อเนื่องอย่างวิดีโอ/เสียง · ต่ำกว่า −80 อย่าไว้ใจ ต่อติดวันนี้พรุ่งนี้อาจหลุด

ทุก 3 dB คือประมาณ 2 เท่า และทุก 10 dB คือ 10 เท่า — จำสองข้อนี้แล้วอ่านเลข dBm ได้ทันทีโดยไม่ต้องกดเครื่องคิดเลข

เวลาทีมรายงานว่า "สัญญาณอ่อนไปนิดเดียว" ให้ถามกลับว่ากี่ dBm — ต่างกัน 20 dB คือต่างกันร้อยเท่า

2.4 GHz กับ 5 GHz — เร็วกว่าแต่ไปได้ใกล้กว่า

FSPL(dB)=20log⁡10(d)+20log⁡10(f)+32.44\text{FSPL(dB)} = 20\log_{10}(d) + 20\log_{10}(f) + 32.44

เมื่อ dd เป็นกิโลเมตร และ ff เป็นเมกะเฮิรตซ์

อ่านเป็นภาษาคน: คลื่นที่แผ่ออกไปในที่โล่งจะจางลงตามระยะและตามความถี่ — ระยะเป็นสองเท่า หายไป 6 dB

ตัวเลขจริงที่ระยะ 10 เมตร
· ที่ 2437 MHz (ช่อง 6 ย่าน 2.4 GHz) → 60.2 dB
· ที่ 5180 MHz (ช่อง 36 ย่าน 5 GHz) → 66.7 dB
ต่างกัน 6.5 dB คือ เหลือกำลังราวหนึ่งในสี่ ที่ระยะเท่ากันเป๊ะ

สูตรนี้เป็นอุดมคติ — ที่โล่ง ไม่มีผนัง ไม่มีคน ในห้องเรียนจริงตัวเลขจะแย่กว่านี้เสมอ เพราะผนังคอนกรีตกินอีก 10–15 dB และร่างกายคนกินอีกหลาย dB

ภาพ: Sss41 / Wikimedia Commons — CC BY-SA 3.0 · Free-space path loss ของย่านความถี่ 802.11

ทั้งสองภาพ: Kirlf / Wikimedia Commons — CC BY-SA 4.0 · ซ้ายคือสเปกตรัมย่าน 2.4 GHz ที่วัดจริง ให้เห็นว่าช่องสัญญาณทับกันจริงแค่ไหน ไม่ใช่เรียงกันสวย ๆ อย่างในผัง · ขวาคือย่าน 5 GHz ที่วัดจริงในสเกลเดียวกัน เทียบกันแล้วเห็นทันทีว่าทำไมสแกนที่ 5 GHz ถึงเจอเพื่อนบ้านน้อยกว่า

5 GHz ไม่ได้ "ดีกว่า" — มันแลกระยะทางกับความเร็ว เลือกให้ตรงกับงาน ไม่ใช่เลือกเลขที่มากกว่า

เกร็ด: ทำไมบางครั้งสแกนแล้วรอนานผิดปกติ

ย่าน 2.4 GHz มี 14 ช่อง และซ้อนทับกันอย่างที่เห็น — ในทางปฏิบัติใช้ได้จริงแค่ 3 ช่องที่ไม่ทับกันคือ 1, 6, 11

ย่าน 5 GHz มีช่องมากกว่านั้นหลายเท่า และบางช่องต้อง ฟังเงียบ ๆ ก่อนว่ามีเรดาร์ใช้อยู่ไหม จึงจะส่งได้

ภาพ: Michael Gauthier, Wireless Networking in the Developing World / Wikimedia Commons — CC BY-SA 3.0

การสแกนหนึ่งครั้งคือการ ไล่ฟังทีละช่อง ช่องละไม่กี่สิบมิลลิวินาที ยิ่งมีช่องเยอะ ยิ่งใช้เวลานาน นี่คือเหตุผลที่บอร์ดหยุดนิ่งระหว่างสแกน ไม่ใช่เพราะมันค้าง

เชื่อมกับวันนี้: wifi.scan() ของเรา บล็อกได้ถึง 10 วินาที — โค้ดบรรทัดถัดไปจะไม่ทำงานเลยจนกว่าสแกนจบ ดังนั้นในท่าที่ 2 เราจะขึ้นข้อความ กำลังสแกน บนจอ ก่อน เรียกมัน ไม่ใช่หลัง ไม่งั้นผู้ใช้จะเห็นจอว่างเปล่าแล้วนึกว่าโปรแกรมพัง

การเข้าร่วมเครือข่าย — สามจังหวะก่อนจะได้คุยกัน

RUCKUS Education Services · 16:36 · อังกฤษ
ดูช่วง 2:00–8:00 ก็พอ เห็นทั้งกระบวนการพร้อมเฟรมจริงจาก packet capture

1 · Scanning บอร์ดฟังหรือถามหาว่ามี AP ไหนอยู่แถวนี้ — wifi.scan() ทำงานอยู่ตรงนี้

2 · Authentication ขั้นตอนแนะนำตัว

3 · Association AP ตอบรับให้เข้าร่วม แล้วจึงเริ่มส่งข้อมูลได้

ภาพ: Superspritz / Wikimedia Commons — CC BY-SA 4.0 · ลำดับการเชื่อมต่อ 802.11

ตรงจุดที่ภาพเขียนว่า Data Transfer บอร์ดยัง ยังไม่มีเลข IP — ยังคุยกับใครนอกห้องไม่ได้

กลไกเต็ม — จากคลื่นในอากาศ ถึงเลข IP บนจอ

บอร์ดของเรา Access Point เราเตอร์ + DHCP 1 · ขอดูว่ามีใครอยู่แถวนี้บ้าง 2 · ตอบกลับ: ชื่อวง ความแรง ช่องสัญญาณ 3 · ขอเข้าร่วม + พิสูจน์รหัสผ่าน 4 · ขอเลขที่อยู่ (DHCP DISCOVER / REQUEST) 5 · ให้เลขมา: 192.168.1.42 + gateway + DNS ขั้นที่ 1-3 คือ WiFi · ขั้นที่ 4-5 คือ IP — คนละเรื่องกัน และพังคนละแบบ

wifi.connect() คืน True เมื่อผ่านครบทั้งห้าขั้น ถ้าติดขั้นไหนก็ตาม เราได้ False เหมือนกันหมด — จึงต้องมี ping ไว้แยกแยะ

DHCP — บอร์ดไม่ได้ตั้งเลข IP ให้ตัวเอง

CMSystemsBe · 1:11 · อังกฤษ
แอนิเมชันลูปสั้น เปิดคาไว้ได้ตลอดช่วงที่อธิบาย

สี่คำที่ต้องจำ DORA

Discover — มีใครแจกเลขบ้าง
Offer — เอาเลขนี้ไปไหม
Request — ขอเลขนี้
Acknowledge — เอาไปเลย

ภาพ: Gelmo96 / Wikimedia Commons — CC BY-SA 4.0 · ลำดับข้อความของ DHCP

เลขที่ได้มาเป็นการ ยืมมาชั่วคราว (lease) ไม่ใช่ของเราถาวร — ปิดบอร์ดแล้วเปิดใหม่ อาจได้เลขคนละตัว นี่คือเหตุผลที่โค้ดของเราต้องอ่าน wifi.ip() ทุกครั้งหลังต่อ ห้ามจำเลขเก่าไว้ใช้

ถ้า DHCP ในห้องเต็มหรือพัง เราจะ "ต่อ WiFi ติด" แต่ "ไม่มีเลข IP" — อาการนี้ผู้เรียนจะเจอจริง และจะดูเหมือนต่อไม่ติดทั้งที่ไม่ใช่

IP · netmask · gateway — สามค่าที่ต้องอ่านให้เป็น

ภาพ: Michel Bakni / Wikimedia Commons — CC BY-SA 4.0 · โครงสร้างเลขที่อยู่ IPv4
ค่า ตัวอย่าง มันบอกอะไร
IP address 192.168.1.42 เลขประจำตัวของบอร์ดในวงแลนนี้ — wifi.ip() คืนค่านี้
netmask 255.255.255.0 เส้นแบ่งว่าเลขส่วนไหนคือ "วง" ส่วนไหนคือ "ตัวเครื่อง" — ในตัวอย่างนี้คือทุกเครื่องที่ขึ้นต้น 192.168.1.
gateway 192.168.1.1 ประตูออกจากวง ถ้าจะคุยกับเลขนอกวง ต้องฝากประตูนี้ส่งให้

32 บิตแบ่งเป็นสี่ช่อง ช่องละ 8 บิต จึงมีค่าได้ 0–255 ต่อช่อง · ข้อจำกัดที่ต้องรู้วันนี้: MicroPython บนบอร์ดนี้ให้แค่ wifi.ip() — ยังไม่มี netmask และ gateway ให้อ่าน โค้ดของเราจึงต้อง เดา gateway จากเลข IP (เอาสามช่องแรกต่อด้วย .1)

การเดาแบบนี้ถูกในวงแลนส่วนใหญ่ แต่ไม่ใช่ทุกวง — ท่าที่ 5 จะสอนวิธีตรวจว่าเราเดาถูกหรือเปล่า

ping ได้เกตเวย์ แต่ 8.8.8.8 เงียบ — แปลว่าอะไร

บอร์ด 192.168.1.42 เกตเวย์ 192.168.1.1 อินเทอร์เน็ต 8.8.8.8 3 ms timeout ยิงสองปลายทาง แล้วอ่านผลเป็นคู่ — นี่คือการวินิจฉัย ไม่ใช่การเดา เกตเวย์ผ่าน · เน็ตผ่าน ปกติดี ทำงานต่อได้ เกตเวย์ผ่าน · เน็ตไม่ผ่าน ปัญหาอยู่เหนือเราเตอร์ ไม่ใช่ที่เรา เกตเวย์ไม่ผ่าน ลิงก์ WiFi หรือเลขเกตเวย์ที่เดาผิด

wifi.ping(ip, timeout_ms) คืนเวลาไป-กลับเป็นมิลลิวินาที หรือ −1 ถ้าไม่มีคำตอบภายในเวลาที่ตั้งไว้

การยิงสองปลายทางแล้วอ่านผลเป็น คู่ ทำให้ตอบได้ทันทีว่าต้องไปแก้ที่ไหน — นี่คือท่าพื้นฐานที่วิศวกรเครือข่ายใช้ทุกวัน และเป็นเหตุผลที่การ์ดของเรามีสองบรรทัด ไม่ใช่บรรทัดเดียว

ระบบที่บอกได้ว่า "พังตรงไหน" มีค่ากว่าระบบที่บอกแค่ว่า "พัง"

DNS — ทำไม wifi.ping() ถึงรับแต่ตัวเลข

ภาพ: Aaron Filbert / Wikimedia Commons — CC BY-SA 4.0 · สถาปัตยกรรมการค้นชื่อโดเมน

เครื่องคอมพิมพ์ google.com ได้เพราะมี ตัวแปลชื่อ (resolver) ถามระบบ DNS ต่อเป็นทอด ๆ จนได้เลข IP แล้วค่อยส่งแพ็กเก็ตไปที่เลขนั้น — ชื่อโดเมนไม่เคยเดินทางในเครือข่าย มีแต่เลขที่เดินทาง ส่วน MicroPython บนบอร์ดนี้ยังไม่มีตัวแปลชื่อเปิดให้ Python เรียก

wifi.ping("google.com")     # ValueError รับเฉพาะเลข IP
wifi.ping("8.8.8.8", 1500)  # ถูกต้อง

8.8.8.8 คือเซิร์ฟเวอร์ DNS สาธารณะของ Google เราเลือกมันเพราะจำง่ายและแทบไม่เคยล่ม ไม่ใช่เพราะกำลังใช้บริการ DNS ของมัน

ข้อจำกัดนี้เป็นช่องว่างของโมดูล ไม่ใช่ของฮาร์ดแวร์ — ตัวชิปคุย DNS ได้ แค่ยังไม่มีใครเปิดประตูฝั่ง Python ให้

ข้อมูลของเราถูกห่อกี่ชั้นกว่าจะออกจากบอร์ด

ภาพเคลื่อนไหว: Moeenrahi / Wikimedia Commons — CC BY-SA 4.0 · การห่อหุ้มข้อมูลตามชั้นโพรโทคอล

ping หนึ่งครั้งถูกห่อทีละชั้นก่อนกลายเป็นคลื่นวิทยุ: ข้อมูลของเรา → หัว IP (เลขต้นทาง-ปลายทาง) → หัวชั้นลิงก์ (MAC) → บิตที่ส่งออกอากาศ ปลายทางแกะย้อนกลับตามลำดับเดิม

ที่ต้องรู้วันนี้สองข้อ: แต่ละชั้นเพิ่มขนาดข้อมูล (มีค่าใช้จ่ายคงที่ต่อแพ็กเก็ต) และ แต่ละชั้นพังได้เอกเทศ — คลื่นแรงดีแต่ไม่มี IP เกิดขึ้นได้จริง · คาบ 10 เราจะเพิ่มชั้น MQTT ทับลงบนกองนี้

wifi.connect() จัดการชั้นล่างให้ · wifi.ping() วัดชั้นกลาง · คาบหน้าเราจะเขียนชั้นบนสุดเอง

เรื่องที่เราให้ 70% น้อง ๆ เขียน 30%

70% — เฟิร์มแวร์ + ไดรเวอร์ WiFi ทำให้แล้ว 30% — งานของเรา หกในแปดตัวของโมดูล wifi ที่โครงวันนี้เรียก wifi.scan() → list ของ tuple wifi.connect(ssid, pw) → bool wifi.is_connected() wifi.ip() wifi.status() wifi.ping(ip, timeout_ms) → ms หรือ -1 สิ่งที่เราต้องตัดสินใจเอง เลือกวงไหนจากที่สแกนเจอ · เรียงยังไง แปลง dBm เป็นภาพที่คนอ่านเข้าใจ ยิง ping ไปที่ไหน บ่อยแค่ไหน แปลผลเป็นข้อความที่ช่วยคนแก้ปัญหาได้

ไดรเวอร์ WiFi หนักกว่าโค้ดทั้งคาบนี้หลายพันบรรทัด — เราไม่ได้เขียนมัน เรา ใช้มันให้เป็น และนั่นคือทักษะที่ตรงกับงานจริงมากกว่า

โมดูล wifi มี แปดชื่อ ไม่ใช่หก — อีกสองตัวคือ wifi.disconnect() กับ wifi.softap() ซึ่งโครงหลักไม่ได้เรียก แต่เราจะลงมือทั้งคู่ในสไลด์ถัดไป เพราะตัวหนึ่งคือวิธีพิสูจน์ว่าลิงก์หลุดจริง และอีกตัวคือทางออกเมื่อในห้องไม่มีวงให้ต่อ

สามข้อที่เฟิร์มแวร์ยังทำได้ไม่ครบ — ต้องรู้ก่อนเขียน

1 · wifi.status() มีช่อง ssid กับ rssi — แต่มันคืนค่าตายตัวเสมอ ssid คืนสตริงว่าง และ rssi คืน 0 ทุกครั้ง ไม่ว่าจะต่ออยู่กับวงไหน · ความแรงจริงต้องเอามาจาก wifi.scan() 2 · wifi.connect() ล็อกโหมดความปลอดภัยเป็น WPA3/WPA2 ตายตัว เครือข่ายเปิดไม่มีรหัสผ่าน ต่อจาก Python ไม่ได้ ทั้งที่เมนูบนจอต่อได้ · ถ้า AP ห้องเปิด ต้องขอวงที่มีรหัส 3 · หน้า Wi-Fi Setting ที่มากับเครื่อง แสดงค่าที่ "แต่งขึ้น" บางช่อง netmask ถูกฮาร์ดโค้ดเป็น 255.255.255.0 และ gateway/DNS ถูกเดาเป็น a.b.c.1 — ไม่ได้ถามระบบจริง ถ้าเฟิร์มแวร์เปิด wifi.ifconfig() ให้ โค้ดของน้อง ๆ จะแม่นกว่าหน้าจอที่มากับเครื่อง

ข้อ 1 กับข้อ 2 เป็น กับดักที่ทำให้เสียเวลาทั้งคาบถ้าไม่รู้ก่อน — จะนั่งแก้โค้ดที่ไม่ได้ผิด

ข้อ 3 เป็นเรื่องที่น่าสนใจกว่านั้น: หน้าจอที่ดูน่าเชื่อถือ อาจแสดงค่าที่ไม่ได้มาจากการวัดจริง การรู้ว่า ตัวเลขบนจอมาจากไหน คือส่วนหนึ่งของงานวิศวกรรม ไม่ใช่การจับผิด

ในโค้ดวันนี้เราก็เดาเกตเวย์เหมือนกัน — ต่างกันตรงที่เราจะ ping เพื่อพิสูจน์ ว่าเดาถูก แล้วแสดงผลตามความจริง

โมดูล wifi ทั้งแปดชื่อ — ตารางที่เปิดค้างไว้ได้ทั้งคาบ

เรียกอย่างไร คืนอะไร สิ่งที่ต้องรู้ก่อนใช้
wifi.scan() list ของ tuple (ssid, rssi, security, channel) คืน ไม่เกิน 20 วง ต่อครั้ง วงที่ 21 หายไปเงียบ ๆ · บล็อก 3-10 วินาที · ถ้าบอร์ดกำลังสแกนของตัวเองอยู่ มันจะรอให้เสร็จก่อนแล้วค่อยถามใหม่ ไม่โยน error ใส่เรา
wifi.connect(ssid, password) True / False รับ สองอาร์กิวเมนต์แบบตำแหน่งเท่านั้น · บล็อกได้ถึงราว 85 วินาทีเมื่อรหัสผิด · ล็อกโหมดเป็น WPA3/WPA2 ตายตัว จึง ต่อวงเปิดที่ไม่มีรหัสผ่านจาก Python ไม่ได้
wifi.disconnect() None ตัดลิงก์แล้วบอกคอร์จอให้ลดไอคอนลง · เรียกตอนยังไม่เคยต่อก็ไม่ error เงียบ ๆ ผ่านไป
wifi.status() dict 5 คีย์ mode connected ip ssid rssi mode คืน "off" / "idle" / "sta" เป็นช่องเดียวที่บอกมากกว่า is_connected() · แต่ ssid คืนสตริงว่างและ rssi คืน 0 เสมอ ทั้งคู่เป็นค่าตายตัวในเฟิร์มแวร์
wifi.is_connected() True / False ถามสถานะจริงจากไดรเวอร์ทุกครั้ง ไม่ใช่ค่าที่จำไว้
wifi.ip() str ยังไม่ต่อจะได้ "0.0.0.0" ซึ่ง เป็นสตริงที่ไม่ว่าง จึงเป็นจริงใน if · ต้องเทียบค่าตรง ๆ ห้ามเขียน if wifi.ip():
wifi.ping(host, timeout=5000) int มิลลิวินาที หรือ -1 รับ เลข IP เท่านั้น ใส่ชื่อโฮสต์ได้ ValueError · ถ้ายังไม่ต่อเน็ตได้ OSError ไม่ใช่ -1
wifi.softap(ssid, password) True / False บอร์ดกลายเป็นตัวปล่อยสัญญาณเองที่ 192.168.4.1 · ไม่ใส่อาร์กิวเมนต์จะได้ชื่อ PSoC-Edge-MPY รหัส micropython · WPA2 ช่อง 1

หกแถวแรกคือของที่โครงหลักวันนี้เรียกจริง สองแถวล่างเราจะลองมือในสไลด์ถัดไป

สองตัวที่โครงหลักไม่ได้เรียก — และทำไมยังต้องรู้จัก

# --- disconnect(): วิธีพิสูจน์ว่า "หลุด" หน้าตาเป็นอย่างไรจริง ๆ ---
wifi.disconnect()                 # คืน None ไม่ใช่ True
print(wifi.is_connected())        # False
print(wifi.ip())                  # "0.0.0.0"  <- ไม่ว่าง จึงเป็นจริงใน if

# --- softap(): เมื่อในห้องไม่มีวงให้ต่อ บอร์ดปล่อยวงของตัวเองได้ ---
wifi.softap("bento-team01", "12345678")   # True แล้วบอร์ดอยู่ที่ 192.168.4.1
โหมด sta — ที่เราใช้กันทั้งคาบ บอร์ด AP ของห้อง บอร์ดไปขอเข้าร่วมวงที่มีอยู่แล้ว ได้เลข IP มาจาก DHCP ของห้อง โหมด softap — บอร์ดเป็นเจ้าของวงเอง บอร์ด มือถือเรา บอร์ดอยู่ที่ 192.168.4.1 เสมอ ไม่ต้องเดา ไม่มีทางออกอินเทอร์เน็ต — คุยได้แค่ในวงนี้

disconnect() ทำให้เรา สร้างสถานะ "หลุด" ขึ้นมาดูได้ตามสั่ง ไม่ต้องรอให้เน็ตห้องล่มเอง — คาบ 12 ต้องเขียนโค้ดที่ทนการหลุด ถ้าไม่เคยเห็นว่าหลุดแล้วค่าไหนเปลี่ยน จะเขียนเงื่อนไขถูกได้ยาก

softap() เป็นทางออกจริงเมื่อวงของห้องไม่มี — เครื่องมือช่างและกล้องติดรถจำนวนมากตั้งค่าครั้งแรกด้วยวิธีนี้ ข้อแลกเปลี่ยนคือ วิทยุมีชุดเดียว เป็น AP แล้วจะเป็นลูกข่ายพร้อมกันไม่ได้ ping() ออกเน็ตจึงใช้ไม่ได้ในโหมดนี้ · ลองไฟล์ examples/s09/07_disconnect_rejoin.py↗ กับ examples/s09/08_softap_fallback.py↗

แกะโค้ดจริง — ท่าที่ 1 วางสองแผงให้ครบก่อนเข้าลูป

ui.screen()
time.sleep_ms(200)
ui.Label("สถานะเครือข่ายของทีม", x=24, y=8, color=COL_TEXT, value=24)
l_ssid = ui.Label("SSID: -", x=336, y=12, color=COL_TEXT, value=20)
l_ip = ui.Label("IP: -", x=24, y=48, color=COL_TEXT, value=20)
l_tick = ui.Label("กำลังสแกน", x=336, y=48, color=COL_DIM, value=20)
btn_scan = ui.Button("สแกนใหม่", x=552, y=8, w=216, h=88, color=0x3A4150, value=20)

tbl = ui.Table(x=24, y=104, w=480, h=288, cols=4)   # แถวละ 72 px: หัว + 3 แถว = 288
tbl.col_width(0, 144)        # กว้างพอสำหรับชื่อ 12 ตัวอักษร ซึ่งเป็นเพดานที่เราตัดไว้
tbl.col_width(1, 96)
tbl.col_width(2, 80)
tbl.col_width(3, 128)        # "มีรหัส" คือข้อความที่ยาวที่สุดในคอลัมน์นี้
SSID dBm ช่อง รหัส AIoT-Class -48 6 มีรหัส Office-2.4 -67 1 มีรหัส Guest-Zone-1 -74 11 เปิด สถานะลิงก์ ต่ออยู่ ยังไม่ต่อ SSID · IP · ping x2 -48 dBm (84%) -90 -40 16 widgets จากเพดาน 64

ทุก widget ถูกสร้าง ก่อน เข้าลูป ในลูปแค่เปลี่ยนข้อความกับค่า · ตารางเดียวแทน ui.Label สิบตัว — col_width คือสิ่งเดียวที่ต้องตั้งให้ถูก ช่องแคบกว่าข้อความ = ตัดบรรทัด = แถวสูงสองเท่า แถวสุดท้ายตกขอบเงียบ ๆ · ปุ่ม สแกนใหม่ อยู่บนแถบหัวเรื่อง เพราะ มุมขวาล่างเฟิร์มแวร์ถือไว้ให้ปุ่ม Console widget ที่ไปทับมันจะถูกบังจนกดไม่โดน

16 widgets จาก 64 — เหลือที่ให้ทีมเติมของตัวเองได้อีกมาก

แกะโค้ดจริง — ท่าที่ 2 มันคือ tuple ไม่ใช่ dict

# --- ท่าที่ 2 + 3 อยู่ในฟังก์ชันเดียว เพราะปุ่ม "สแกนใหม่" ต้องเรียกซ้ำได้ทั้งชุด ---
def rescan():
    nets = wifi.scan()                              # บล็อกราว 3-10 วินาที
    nets.sort(key=lambda net: net[1], reverse=True) # net[1] คือ rssi
ของจริงที่ scan() คืนมา ("AIoT-Class", -48, 4, 6) net[0] = ssid net[1] = rssi net[2] = security net[3] = channel เรียงด้วย net[1] มากไปน้อย = แรงไปอ่อน สิ่งที่ตัวอย่างที่มากับเครื่องเขียนผิด net['ssid'] net['rssi'] TypeError: tuple indices must be integers, not str ชุดตัวอย่างที่มากับเฟิร์มแวร์ ไฟล์ network/01_wifi_scan_connect.py บรรทัด 33

sort() แก้ลิสต์เดิมในที่ · key=lambda net: net[1] สั่งให้เรียงตามช่องที่สอง · reverse=True ทำให้ค่ามากมาก่อน — RSSI ค่ามากคือแรงกว่า เพราะ −48 มากกว่า −79

ทำไมสองบรรทัดนี้ต้องอยู่ในฟังก์ชัน: ปุ่ม สแกนใหม่ ต้องทำงานทั้งชุด — สแกน เรียง ล้างตาราง เทใหม่ ถ้าปล่อยสองบรรทัดนี้ไว้กลางไฟล์ ปุ่มจะเรียกได้แค่ครึ่งเดียวของงาน

ตัวอย่างที่มากับเฟิร์มแวร์เขียนผิดจริง — ไฟล์ network/01_wifi_scan_connect.py บรรทัด 33 ยังเขียน net['ssid'] อยู่จนวันนี้ บทเรียนคือ โค้ดตัวอย่างไม่ใช่เอกสารอ้างอิง อย่าลอกไปใช้

แกะโค้ดจริง — ท่าที่ 3 เทลงตาราง แล้ววัดวงของทีมด้วย dBm

    tbl.clear_items()                           # ท่าที่ 3 (ยังอยู่ใน rescan)
    tbl.add_row("SSID", "dBm", "ช่อง", "รหัส")    # หัวสั้น เพราะช่องแคบ
    for i in range(min(TOP_N, len(nets))):
        ssid, rssi, security, channel = nets[i] # แกะสี่ช่องพร้อมกัน
        tbl.add_row(ssid[:12], str(rssi), str(channel),
                    "เปิด" if security == 0 else "มีรหัส")
        ui.poll()
    for ssid, rssi, security, channel in nets:  # แล้วค่อยขยับมาตรวัดฝั่งขวา
        if ssid == WIFI_SSID:                   # มาตรวัดเล่าเรื่องวงของทีมเอง
            pct, col = signal_of(rssi)
            bar_rssi.value(rssi)                # Bar รับพิสัย -90..-40 ตรง ๆ ได้
            l_rssi.color(col)
            l_rssi.text("ความแรง " + str(rssi) + " dBm (" + str(pct) + "%)")

q=clamp⁡ ⁣(RSSI−(−90)(−40)−(−90)×100,  0,  100)q=\operatorname{clamp}\!\left(\frac{\text{RSSI}-(-90)}{(-40)-(-90)}\times100,\;0,\;100\right) — ยืดช่วง −90 ถึง −40 dBm ให้เป็น 0 ถึง 100 เพื่อบอก คุณภาพเป็นเปอร์เซ็นต์ ข้างตัวเลข dBm · ตัวเลขจริง: −67 dBm → (−67+90)×2=46(-67+90)\times 2 = 46

−90 dBm = แท่งว่าง · −40 dBm = แท่งเต็ม · เลขในวงเล็บข้างค่า dBm คือคุณภาพเป็น % -48 dBm 84 -67 dBm 46 -86 dBm 8 แท่งกระเพื่อมเล็กน้อยตลอดเวลา เพราะคลื่นจริงไม่เคยนิ่ง — คนเดินผ่าน ประตูเปิดปิด ก็ทำให้เลขขยับได้ 3-5 dB

ui.Bar ไม่ต้องแปลงอะไรเลย เพราะตั้ง min=-90, max=-40 ได้ตรง ๆ — ที่ต้องแปลงคือเลขที่ คน จะอ่าน ไม่ใช่เลขที่ widget จะรับ · min(TOP_N, len(nets)) กันพังตอนสแกนเจอน้อยกว่าสามวง

signal_of() คืนทั้งเปอร์เซ็นต์และสีพร้อมกัน เพราะมาจากตัวเลขเดียว — แยกกันคำนวณเมื่อไร วันหนึ่งมันจะไม่ตรงกัน

แกะโค้ดจริง — ท่าที่ 4 บรรทัดที่บล็อกได้ 85 วินาที

l_tick.text("กำลังต่อ 85 วิ")            # ท่าที่ 4: บอกก่อน แล้วค่อยเรียกของที่บล็อก
ui.poll()
ok = wifi.connect(WIFI_SSID, WIFI_PASS)   # สองอาร์กิวเมนต์ตามลำดับเท่านั้น

if not ok:
    l_ssid.color(COL_BAD)
    l_ssid.text("ต่อไม่ติด")
    l_tick.text("ตรวจ SSID/รหัสผ่าน")
    ui.poll()
    raise SystemExit
...
led_up.value(1)                           # ต่อติดแล้วไฟดวงบนติด ดวงล่างหรี่
led_down.value(0)
เส้นเวลาของ wifi.connect() หนึ่งครั้ง 3-8 วิ รหัสถูก นานถึง 85 วินาที แล้วคืน False รหัสผิด · SSID พิมพ์ผิด · วงเป็นแบบเปิด ระหว่างนี้จอไม่อัปเดตเลย เพราะโค้ดของเราหยุดรอที่บรรทัดนี้ — จึงต้องเขียนข้อความบอกไว้ก่อนเรียก ยอมแพ้

wifi.connect() รับ สองอาร์กิวเมนต์แบบตำแหน่งเท่านั้น — ไม่มี timeout= ไม่มี security= · raise SystemExit เมื่อต่อไม่ติดคือการตัดสินใจที่ตั้งใจ เพราะท่าที่ 5 ไม่มีความหมายถ้าไม่มีลิงก์ — ล้มเร็วดีกว่าปล่อยให้ลูปวนแสดง timeout จนคนดูสับสน · สองบรรทัดสุดท้ายคือ สถานะลิงก์เป็นไฟ ไม่ใช่ตัวอักษรสี — แปลงภาพเป็นขาวดำแล้วตัวอักษรเขียว/แดงกลายเป็นเทาเหมือนกัน ส่วนไฟติดกับไฟหรี่ยังแยกออกด้วยความสว่างและตำแหน่ง

ในระบบฝังตัว ทุกบรรทัดที่บล็อกนานกว่าครึ่งวินาที ต้องมีการแจ้งผู้ใช้กำกับเสมอ

แกะโค้ดจริง — ท่าที่ 5 ลูปสถานะสด ยิงสองปลายทาง

while True:                                     # ท่าที่ 5 — ลูปเดินทุก 200 ms
    now = time.ticks_ms()
    events = ui.poll()                          # ลืมบรรทัดนี้ = widget หายใน 2 วิ
    for ev in events:                           # นิ้วมาถึงเมื่อไรก็รับได้ทันที
        if ev["type"] == "clicked" and ev["handle"] == btn_scan.id():
            l_tick.text("กำลังสแกน")            # บอกก่อน แล้วค่อยเรียกของที่บล็อก
            ui.poll()
            nets = rescan()
    if wifi.is_connected():
        if time.ticks_diff(now, t_ping) >= PING_EVERY_MS:   # ping เดินนาฬิกาตัวเอง
            t_ping = now
            try:
                ms_gw = wifi.ping(gw, PING_TIMEOUT_MS)
                ms_net = wifi.ping(NET_TEST_IP, PING_TIMEOUT_MS)
            except OSError:
                ms_gw, ms_net = -1, -1
            l_gw.text(ms_text("เกตเวย์", ms_gw))
    time.sleep_ms(200)

ลูปเดินทุก 200 ms แต่ ping เดินตามนาฬิกาของตัวเองทุก 3 วินาที — งานคนละจังหวะอยู่ในลูปเดียวกันได้ ถ้าแต่ละงานถามนาฬิกาเอง แทนที่จะใช้ sleep ยาว ๆ ขวางทาง · ถ้าเขียน sleep_ms(3000) ก้อนเดียว ปุ่มบนจอจะกดแล้วรอถึงสามวินาทีกว่าจะตอบ ซึ่งคนกดจะสรุปว่าปุ่มเสีย

แกะโค้ดจริง — ท่าที่ 5 (ต่อ) สองนาฬิกาในลูปเดียว

สถานะลิงก์ SSID: AIoT-Class IP: 192.168.1.42 เกตเวย์ 3 ms อินเทอร์เน็ต 24 ms หรือ timeout ทุก 3 วินาที แต่ ui.poll() ทุก 200 ms ห้าม sleep_ms(3000) ก้อนเดียว ไม่งั้น widget หายภายใน 2 วินาที

try/except ครอบไว้เพราะ ความผิดพลาดชั่วคราวของเครือข่ายเป็นเรื่องปกติ ไม่ใช่ข้อยกเว้น · ส่วนตัวเลขนับถอยหลัง "วัดใหม่ใน N วิ" เขียนใหม่เมื่อ วินาทีเปลี่ยน เท่านั้น ไม่ใช่ทุกรอบลูป — ตัวเลขที่กระพริบห้าครั้งต่อวินาที คนอ่านไม่ทันและไม่มีใครได้ประโยชน์

ยิงเกตเวย์ก่อนแล้วค่อยยิงอินเทอร์เน็ต — ลำดับนี้ทำให้อ่านผลสองบรรทัดแล้วรู้ทันทีว่าขาดตรงไหน

ข้อมูลไหลไปทางไหน — ตั้งแต่คลื่นถึงตัวเลขบนจอ

คลื่นในอากาศ 2.4 / 5 GHz CYW55513 ชิปวิทยุ + ไดรเวอร์ CM33 โค้ด Python ของเรา IPC กล่องจดหมาย CM55 → จอ Label + Bar ที่คนอ่าน RSSI หนึ่งค่า เดินทางผ่านห้าจุดกว่าจะเป็นแท่งสีบนจอ WiFi ทั้งหมดอยู่ฝั่ง CM33 เท่านั้น — CM55 แตะวงจรเครือข่ายไม่ได้เลย จอไม่ขึ้นเลข ไม่ได้แปลว่าเน็ตพัง ให้ไล่ดูว่าขาดตอนที่จุดไหน

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

วิธีรันบนบอร์ด

1 · เปิด Playground ค้างหน้านี้ไว้ตลอดคาบ 2 · แก้ SSID + รหัส สองบรรทัดบนสุดของไฟล์ 3 · เติม 6 ช่องว่าง ทีละจุด แล้วรันทุกครั้ง 4 · Program to Device แล้วมองจอบอร์ด สิ่งที่จะเห็นตามลำดับ — และห้ามตกใจ ตารางว่าง → นิ่ง 3-10 วิ (สแกน) → สามแถวขึ้นพร้อมกัน → นิ่งอีกครั้ง (ต่อ) → ไฟเขียวติด แล้ว ping เดิน

โครงหน้าจอถูกวางให้ต่อยอดจากแดชบอร์ดคาบ 8 ได้ทันที — ถ้าทีมอยากเอาการ์ด IMU กลับมาวางคู่กัน ให้ลด TOP_N จาก 3 เหลือ 2 แล้วตั้ง h ของตารางเป็น 216 (หัวตารางบวกสองแถว แถวละ 72) พื้นที่ที่ว่างขึ้นมาพอวางการ์ดเดิมได้

ถ้าจอค้างอยู่ที่ connecting นานเกินหนึ่งนาทีครึ่ง ให้กด RESTART แล้วตรวจ SSID กับรหัสผ่านทีละตัวอักษร — อย่ารอต่อ

ทีมละหนึ่งบอร์ด สลับกันพิมพ์ทุกช่วง คนที่ไม่ได้พิมพ์มีหน้าที่จับเวลาและจดตัวเลขที่เห็น

MVP checkpoint — ผ่านคาบนี้เมื่อ

หน้าจอ network status ของทีม (SSID, IP, ping ms) อัปเดตสดบน HMI ที่ต่อยอดจากคาบ 8

แปลเป็นสิ่งที่ตรวจได้จริง:

  • [ ] ตารางซ้าย ขึ้นชื่อเครือข่ายอย่างน้อย 3 วง เรียงจากแรงไปอ่อน ครบสี่คอลัมน์ และ ไม่มีช่องไหนตัดบรรทัด (ช่องที่ตัดบรรทัดทำให้แถวสูงสองเท่า แถวสุดท้ายจะตกขอบจอ)
  • [ ] แผงขวาแสดง SSID ที่โปรแกรมส่งเข้า connect() เอง และ เลข IP ที่ได้จาก DHCP — เลข IP ต้องไม่ใช่ค่าที่พิมพ์ไว้ในโค้ด ส่วน SSID บอร์ดตอบเองไม่ได้ (wifi.status()["ssid"] เป็นสตริงว่างเสมอ) ทีมจึงต้องจำสตริงที่ตัวเองส่งไป
  • [ ] ไฟสถานะลิงก์ ติดถูกดวง และมาตรวัด dBm ขยับตามวงของทีมจริง พร้อมพิสัย -90 ถึง -40 กำกับ
  • [ ] กด สแกนใหม่ แล้วตารางถูกล้างและเทใหม่ ไม่ใช่เขียนทับซ้อนของเดิม
  • [ ] บรรทัดเกตเวย์และอินเทอร์เน็ตแสดงเวลาเป็น ms และ อัปเดตซ้ำทุก 3 วินาที ต่อเนื่องอย่างน้อย 2 นาที
  • [ ] ทีมอธิบายได้ว่าถ้า gateway ผ่านแต่ internet timeout แปลว่าปัญหาอยู่ที่ไหน
  • [ ] ทีมตอบได้ว่าทำไม wifi.scan() ต้องอ่านด้วย net[1] ไม่ใช่ net['rssi']
  • [ ] ถ่ายรูปหน้าจอตอนทำงานแนบใน worksheet

เกณฑ์ข้อที่หกกับเจ็ดตอบด้วยปากเปล่าได้ — คาบนี้วัดการวินิจฉัย ไม่ได้วัดความยาวโค้ด

ลงมือทำ — เติมช่องว่างในไฟล์ฝึก

practise_codes/s09_network_status.py pass ท่า 2 — nets = wifi.scan() (ใน rescan) pass ท่า 2 — nets.sort(key=..., reverse=True) pass ท่า 3 — แกะ tuple สี่ช่อง (ในลูปของ rescan) pass ท่า 4 — ok = wifi.connect(...) (นอกสุด) pass ท่า 5 — events = ui.poll() (ต้นลูป) pass ท่า 5 — ms_gw = wifi.ping(...) (ใน try) รวม 6 จุด สังเกตระดับ การเยื้อง

เปิด practise_codes/s09_network_status.py↗ มีช่องว่างให้เติม 6 จุด — จุดเดียวที่อยู่ระดับนอกสุดคือ wifi.connect() ที่เหลือเยื้องเข้าไปอยู่ในฟังก์ชัน rescan() ในลูป หรือใน try ให้ดูตำแหน่งซ้าย-ขวาของกล่องเป็นตัวช่วยจำระดับ · หน้าจอถูกวางไว้ให้ครบแล้ว ไม่ต้องแก้ งานของเราคือทำให้ข้อมูลจริงไหลเข้าไปในนั้น

def rescan():
    nets = []
    # เติม: nets = wifi.scan()
    pass
    ...
    for i in range(min(TOP_N, len(nets))):
        # เติม: ssid, rssi, security, channel = nets[i]
        pass

ลำดับที่แนะนำ: เติมท่า 2 แล้วรันดูว่า print("found", len(nets), "networks") ขึ้นกี่วง จากนั้นค่อยเติมท่า 3 ให้แถวขึ้นจอ แล้วจึงไปต่อท่า 4 และ 5

ตัวอย่างชุดคาบ 9 — สามไฟล์แรกคือชุดที่พาหน้าจอสถานะเครือข่ายขึ้นจนครบ

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 29 นาที

ลำดับ · เรื่อง · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · scan() คืน tuple ไม่ใช่ dict · 6 นาที examples/s09/01_scan_tuples.py↗ แกะสี่ช่องของแต่ละวงได้ถูกตั้งแต่บรรทัดแรก แทนที่จะเสียครึ่งคาบกับ TypeError · จะเข้าใจว่าผลของ scan() เป็น tuple ไม่ใช่ dict จึงต้องอ่านด้วยลำดับช่อง ไม่ใช่ด้วยชื่อคีย์
2 · ป้ายสถานะขึ้นก่อนบรรทัดที่บล็อก · 8 นาที examples/s09/03_connect_says_first.py↗ เขียนลำดับที่ทำให้จอไม่ดูเหมือนเครื่องค้าง ตอน connect() กินเวลาเป็นนาที · จะเข้าใจว่าป้ายสถานะต้องขึ้นก่อนบรรทัดที่บล็อกเสมอ ไม่ใช่หลังจากมันคืนค่า
3 · หน้าจอสถานะลิงก์ที่วัดมาจริง · 15 นาที examples/s09/06_link_panel_hmi.py↗ ประกอบ MVP ของคาบนี้ได้ครบ — SSID · IP · ping ms พร้อมสามสถานะที่แยกออกจากกัน · จะเห็นว่าหน้าจอสถานะลิงก์ที่เชื่อได้ ทุกตัวเลขบนนั้นต้องวัดมาจริง

ไฟล์ที่ 3 ไม่มีแท่งความแรงสัญญาณ และนั่นตั้งใจ แท่งบนจอนั้นคือเวลา ping — ยาว = ช้า ตรงข้ามกับมาตรวัดความแรงในเฉลยที่มาจาก scan() ซึ่งเป็นคนละคำถามกัน เหตุผลอยู่ในหัวไฟล์ พร้อม path ของซอร์สที่ hardcode ค่าไว้

ติดตรงไหน เปิดอันนี้

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
ต่อติดแล้ว ได้ IP แล้ว แต่เปิดอะไรไม่ได้เลย examples/s09/04_ping_two_targets.py↗ — วัดเกตเวย์กับ 8.8.8.8 พร้อมกัน แล้วบอกได้ว่าขาดตรงไหน
เรียก status() หลายครั้งในรอบเดียว แล้วได้ภาพที่ไม่เคยเกิดจริง examples/s09/05_status_dict.py↗ — อ่านครั้งเดียวต่อรอบ แล้วใช้ค่าชุดนั้นทั้งรอบ
ไม่รู้ว่าวงไหนคือ AP ของห้อง examples/s09/02_rank_by_rssi.py↗ — เรียงจากแรงไปอ่อนด้วย dBm จริงจาก scan()

อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของคาบ 9 เพราะ M9 วัดแค่ว่าหน้าจอสถานะอัปเดตสดได้ ยังไม่ได้วัดการฟื้นตัว: examples/s12/03_reconnect_backoff.py↗ ตอบคำถามถัดไปว่าลิงก์หลุดแล้วจะต่อใหม่อย่างไร ไม่ให้สามสิบบอร์ดถล่มเราเตอร์พร้อมกัน

กับดักที่เจอบ่อย

อาการ สาเหตุที่แท้จริง วิธีแก้
TypeError: tuple indices เขียน net['ssid'] หรือ net['rssi'] ตามตัวอย่างที่มากับเครื่อง ใช้ net[0] และ net[1] — scan() คืน tuple
ต่อไม่ติดทั้งที่ SSID ถูกและไม่มีรหัสผ่าน connect() ล็อกโหมดเป็น WPA3/WPA2 ตายตัว ต่อวงแบบเปิดไม่ได้ ขอผู้สอนเปิดวงที่ตั้งรหัสผ่านไว้
จอนิ่งไปเป็นนาทีหลังกด Program connect() บล็อกได้ถึง ~85 วินาทีเมื่อรหัสผิด รอจนขึ้นผล แล้วตรวจรหัสทีละตัวอักษร ไม่ต้องกดซ้ำ
ValueError ตอนเรียก ping ใส่ชื่อโฮสต์ เช่น "google.com" wifi.ping() รับเฉพาะเลข IP
แถวเครือข่ายขึ้นแล้วหายไปใน 2 วิ ลืม ui.poll() ในลูป หรือหน่วงยาวเป็นก้อนเดียว เรียก ui.poll() ทุกรอบ และให้ลูปเดินรอบละ 200 ms
ตารางสูงเกินจอ แถวสุดท้ายหาย ข้อความในช่องยาวเกิน col_width แล้วถูกตัดบรรทัด แถวนั้นสูงเป็นสองเท่า ขยาย col_width ของคอลัมน์นั้น หรือย่อข้อความก่อนใส่ (เฉลยตัด SSID ที่ 12 ตัวอักษร)
กดสแกนใหม่แล้วแถวเก่ายังค้าง ลืม tbl.clear_items() ก่อนเทแถวชุดใหม่ ล้างก่อนเสมอ แล้วใส่หัวตารางใหม่ทุกครั้ง เพราะการล้างลบหัวตารางไปด้วย
เปลี่ยนสีแท่งแล้วแท่งดูเต็มทั้งราง .color() ของ ui.Bar ไปลงที่ราง ไม่ใช่แถบที่เต็ม อย่าเปลี่ยนสีแท่งตอนรัน ให้เปลี่ยนสีตัวเลขหรือใช้ ui.Led บอกสถานะแทน
wifi.status() คืน ssid ว่าง rssi = 0 ทั้งสองช่องเป็นค่าตายตัวในเฟิร์มแวร์ อ่านความแรงจาก wifi.scan() และเก็บ SSID ที่เราสั่งต่อไว้เอง
gateway timeout ทั้งที่ IP ขึ้นปกติ เดาเลขเกตเวย์เป็น .1 แต่วงนี้ไม่ได้ใช้ .1 ถามเลขจริงจากผู้สอน แล้วแก้ค่าในโค้ด
IndexError ตอนวาดแถวท้าย ๆ สแกนเจอน้อยกว่า TOP_N ใช้ min(TOP_N, len(nets)) เป็นขอบเขตลูป
ping เน็ต timeout ทุกครั้ง แต่ gateway ปกติ ทางออกอินเทอร์เน็ตของห้องมีปัญหา หรือถูกกรองไว้ ไม่ใช่บั๊กของเรา — บันทึกผลลงใบงานแล้วทำงานต่อได้

เจ็ดในสิบสองข้อนี้คือ ข้อจำกัดที่รู้ได้ล่วงหน้า ไม่ใช่ความผิดพลาดของโค้ด — สามข้อกลางตารางเป็นเรื่องของ ui.Table โดยเฉพาะ และทั้งสามข้อ ไม่มี error ให้จับ มีแต่จอที่ดูผิด

เฉลย s09_network_status.py↗ — ส่วนที่หนึ่ง

อ่านให้เข้าใจ แล้วพิมพ์เอง อย่าคัดลอกวาง

WIFI_SSID = "AIoT-Class"     # ชื่อเครือข่ายที่ผู้สอนแจกให้ห้องนี้
WIFI_PASS = "changeme"
NET_TEST_IP = "8.8.8.8"      # ปลายทางฝั่งอินเทอร์เน็ต
PING_TIMEOUT_MS = 1500
TOP_N = 3                    # ตารางสูง 288 = หัวตารางบวกสามแถว แถวละ 72 พิกเซล
PING_EVERY_MS = 3000
RSSI_FLOOR, RSSI_CEIL = -90, -40   # พิสัยของมาตรวัด

def signal_of(rssi):
    pct = (rssi - RSSI_FLOOR) * 100 // (RSSI_CEIL - RSSI_FLOOR)
    pct = 0 if pct < 0 else (100 if pct > 100 else pct)
    col = COL_RUN if rssi >= -60 else (COL_WARN if rssi >= -75 else COL_BAD)
    return pct, col
สามชั้นนี้เรียงจากบนลงล่างในไฟล์เสมอ — ค่าที่แก้บ่อยอยู่บนสุด ค่าที่ทีมต้องแก้ SSID · รหัสผ่าน อยู่บนสุด หาเจอทันที ค่าที่ปรับได้ทีหลัง timeout · จำนวนแถว ไม่ต้องไล่หาในลูป ตรรกะที่ใช้ซ้ำ signal_of() · ms_text() แก้ที่เดียว เปลี่ยนทั้งจอ

เกณฑ์สี −60 / −75 หลวมกว่าเกณฑ์ห้าขีดของหน้าจอบอร์ด เพราะการ์ดของเรามีสามระดับ — เลือกเกณฑ์ให้เข้ากับสิ่งที่จะแสดง ไม่ใช่ลอกมาทั้งชุด · ระดับ "ปกติ" คืนสีฟ้า COL_RUN ไม่ใช่เขียว สถานะปกติต้องเงียบ สีจัดสงวนไว้ให้เรื่องผิดปกติ — จอที่ระบายเขียวทั้งจอฝึกให้ตามองข้ามสี แล้ววันที่มีเรื่องจริง สีแดงก็กลืนไปกับพื้นหลัง

ค่าที่ต้องแก้บ่อยอยู่บนสุด ตรรกะอยู่กลาง หน้าจออยู่ล่าง — โครงนี้ใช้ได้กับทุกสคริปต์ตั้งแต่คาบ 1

เฉลย — หน้าจอ: ตาราง ไฟสถานะ และมาตรวัด dBm

tbl = ui.Table(x=24, y=104, w=480, h=288, cols=4)   # หัวตาราง + 3 แถว แถวละ 72 px
tbl.col_width(0, 144)        # ช่องแคบกว่าข้อความ = ตัดบรรทัด = แถวสูงสองเท่า = แถวท้ายตกจอ
tbl.col_width(1, 96)
tbl.col_width(2, 80)
tbl.col_width(3, 128)
led_up = ui.Led(x=536, y=112, w=48, h=48, color=COL_OK, value=0)     # ต่ออยู่
led_down = ui.Led(x=536, y=168, w=48, h=48, color=COL_BAD, value=1)  # ยังไม่ต่อ
bar_rssi = ui.Bar(x=536, y=320, w=152, h=12, color=COL_RUN,
                  min=RSSI_FLOOR, max=RSSI_CEIL, value=RSSI_FLOOR)
sc_rssi = ui.Scale(x=536, y=340, w=152, h=44, color=COL_TEXT,
                   min=RSSI_FLOOR, max=RSSI_CEIL)
sc_rssi.ticks(11, 5)      # 11 ขีด ป้ายเลขทุกขีดที่ 5 = เหลือสามป้าย ไม่ทับกัน

สามอย่างนี้แทนของเดิมทีละอย่าง และแต่ละอย่างมีเหตุผลที่วัดได้

ของเดิม ของใหม่ เพราะ
ui.Label เรียงกันห้าบรรทัด ui.Table สี่คอลัมน์ ตารางจัดคอลัมน์ให้เอง เราไม่ต้องนับพิกเซลทุกครั้งที่ข้อความเปลี่ยนความยาว
ตัวอักษรสีเขียว/แดงบอกว่าต่ออยู่ไหม ui.Led สองดวง แปลงภาพหน้าจอเป็นขาวดำแล้วสีตัวอักษรหายหมด ส่วนไฟติดกับไฟหรี่ยังแยกออก
ตัวเลข dBm ลอย ๆ ui.Bar ทับ ui.Scale ค่ากับพิสัยอยู่ด้วยกัน คนอ่านตอบได้ทันทีว่า −67 นี่ดีหรือแย่

ui.Scale ไม่รับ .value() มันคือไม้บรรทัด ตัวที่ขยับคือ ui.Bar ที่วางทับ (แบบวงกลมมีเข็มจริง — คาบ 6) · ui.Led สั่ง .value(0) แล้ว หรี่ ไม่ใช่หาย — ไฟที่หายไปตอนดับ ทำให้คนดูแยกไม่ออกว่าดับหรือจอเสีย

ทั้งสามตัวเป็นของใหม่ที่เพิ่มเข้าไลบรารีเมื่อ 15 ส.ค. 2026 — examples/s04/09_scale_led_spinbox.py↗ สอนทั้งสามตัวแยกกันทีละตัว

เฉลย — ส่วนที่สอง: สแกน เรียง แล้วเทลงตาราง

def rescan():                                 # ปุ่ม "สแกนใหม่" เรียกซ้ำได้ทั้งชุด
    nets = wifi.scan()
    nets.sort(key=lambda net: net[1], reverse=True)
    print("found", len(nets), "networks")
    tbl.clear_items()                         # ล้างก่อน ไม่งั้นแถวเก่าค้างใต้แถวใหม่
    tbl.add_row("SSID", "dBm", "ช่อง", "รหัส")
    for i in range(min(TOP_N, len(nets))):
        ssid, rssi, security, channel = nets[i]
        tbl.add_row(ssid[:12], str(rssi), str(channel),
                    "เปิด" if security == 0 else "มีรหัส")
        ui.poll()
ทั้งสองกล่องต่างกันแค่คำเดียว แต่ผลบนจอกลับด้านกันทั้งแผง ui.poll() อยู่ในลูป (ถูก) for i in range(...): tbl.add_row(...) ui.poll() เรียงกลับด้าน (ผิดที่พบบ่อย) nets.sort(key=lambda n: n[1]) ไม่ใส่ reverse=True → -86 มาก่อน -48 สามแถวบนจอกลายเป็นวงที่อ่อนที่สุด

security == 0 คือเครือข่ายแบบเปิด ค่าอื่นคือมีการเข้ารหัส — เขียนแค่ เปิด กับ มีรหัส เพราะโมดูลยังไม่มีค่าคงที่ให้เทียบ WPA2/WPA3 และเขียนเป็น คำ ไม่ใช่ระบายสี ภาพขาวดำก็ยังอ่านออก · ssid[:12] ตัดชื่อที่ยาวเกินคอลัมน์โดยตั้งใจ ราคาคือสองวงที่ขึ้นต้นเหมือนกันจะดูเหมือนกัน — ชื่อเต็มยังอยู่ที่ Console ทุกการตัดข้อมูลบนจอต้องรู้ตัวว่าตัดอะไร และต้องมีที่ให้ดูของเต็ม

reverse=True คือหนึ่งคำที่เปลี่ยนความหมายของทั้งหน้าจอ — ตรวจผลด้วยตาทุกครั้งหลังเรียงข้อมูล

เฉลย — ส่วนที่สาม และทำไมต้องเรียงห้าท่าแบบนี้

nets = rescan()                       # ท่า 2 + 3 จบในบรรทัดเดียว และเรียกซ้ำได้
...
ok = wifi.connect(WIFI_SSID, WIFI_PASS)
...
ip = wifi.ip()
gw = gateway_of(ip)                   # เดาว่าเกตเวย์เป็น .1 ของวงเดียวกัน
led_up.value(1)
led_down.value(0)
...
while True:
    now = time.ticks_ms()
    events = ui.poll()                # ท่า 5 — รับนิ้วทุก 200 ms
    ...
    if wifi.is_connected():
        ...
                ms_gw = wifi.ping(gw, PING_TIMEOUT_MS)      # ยิงทุก 3 วิ ตามนาฬิกาของมันเอง
                ms_net = wifi.ping(NET_TEST_IP, PING_TIMEOUT_MS)
ถ้าท่าที่ 3 พัง เรารู้แน่ว่าไม่เกี่ยวกับเครือข่าย เพราะท่า 2 ผ่านไปแล้ว 1 · จอใช้ได้ 2 · วิทยุใช้ได้ 3 · แปลข้อมูลถูก 4 · มีที่อยู่แล้ว 5 · คุยกับคนอื่นได้ แต่ละท่าพิสูจน์ท่าก่อนหน้า — พังตรงไหนรู้ทันที

ท่า 2 สแกน มาก่อนต่อ เพราะสแกนไม่ต้องใช้รหัสผ่าน — เจอวงแปลว่าวิทยุทำงาน · ท่า 3 แปลข้อมูล มาก่อนต่อเน็ต เพราะบั๊ก tuple/dict โผล่ตรงนี้ · ท่า 2 กับ 3 ถูกมัดเป็นฟังก์ชัน rescan() ตัวเดียว เพราะปุ่มบนจอต้องเรียกทั้งชุดซ้ำได้ — นั่นคือความต่างระหว่าง "สคริปต์ที่รันครั้งเดียว" กับ "หน้าจอที่คนใช้งานได้" · gateway_of() เดาเลขเกตเวย์เหมือนหน้าจอ C แต่เราไม่หยุดแค่เดา — ยิง ping เพื่อพิสูจน์

ไล่จากสิ่งที่พึ่งพาคนอื่นน้อยที่สุดไปหามากที่สุด — เป็นลำดับการดีบักมาตรฐานของงานฝังตัว

เชื่อมโยงรากฐาน · สรุปคาบ · คาบหน้า

ฝั่งสมองกลฝังตัว คลื่นวิทยุ · ชั้นโพรโทคอล บรรทัดที่บล็อกและวิธีรับมือ ฝั่ง Python tuple unpacking · sort(key=) try/except · lambda ฝั่งออกแบบระบบ วัดสองจุดเพื่อระบุตำแหน่งปัญหา แยก "เดา" ออกจาก "พิสูจน์แล้ว" คาบ 1-2 เล่นของจริง คาบ 3-8 สั่งฮาร์ดแวร์ + จอ คาบ 9 ออกสู่เครือข่าย คาบ 10-12 ขึ้นแพลตฟอร์ม วันนี้เราอยู่ตรงนี้ — บอร์ดมีที่อยู่แล้ว แต่ยังไม่ได้ส่งอะไรให้ใคร

วันนี้เราได้: อ่าน RSSI เป็น dBm และเทียบกำลังได้ · แกะ tuple จาก wifi.scan() · ต่อเครือข่ายด้วย wifi.connect() โดยรู้ว่ามันบล็อก · วินิจฉัยลิงก์ด้วย ping สองปลายทาง · ประกอบทั้งหมดเป็นการ์ดสถานะบนหน้าจอเดิม

การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ต่อยอด บันทึกลง worksheet ข้อ 8

คาบหน้า: บอร์ดจะเริ่ม ส่งข้อมูลออกไปจริง ๆ ด้วย MQTT — เปิดคอมพิวเตอร์อีกเครื่องแล้วเห็นค่าจากบอร์ดวิ่งขึ้นบนหน้าจอนั้น และสั่ง LED กลับมาที่บอร์ดได้

เก็บโค้ดวันนี้ไว้ให้ดี คาบ 10 เริ่มจากไฟล์นี้ต่อโดยตรง — เพิ่ม mqtt ทับลงบนลิงก์ที่เราเพิ่งทำให้ทำงาน

ใช้จริงที่ไหน — สี่มุมที่งานนี้ไปโผล่

โรงงาน · สำรวจสัญญาณก่อนติดตั้ง ก่อนติดเซนเซอร์ 200 ตัวในโรงงาน ต้องเดินวัด RSSI ทุกจุดก่อน จุดไหนต่ำกว่า −75 ต้องเพิ่ม AP เครื่องมือที่ใช้ = สิ่งที่เราเพิ่งเขียนวันนี้ อาคาร · เฝ้าสุขภาพลิงก์ของอุปกรณ์ อุปกรณ์ในตึกรายงาน RSSI และ ping ของตัวเอง ทีมช่างเห็นตัวไหนกำลังจะหลุด ก่อนที่มันจะหลุด เฝ้าลิงก์ ไม่ใช่เฝ้าแค่ข้อมูล เกษตร · งบสัญญาณกลางแจ้ง โรงเรือนห่างจากบ้าน 300 เมตร ใช้ 2.4 GHz เพราะ 5 GHz ไปไม่ถึง — คำนวณจาก FSPL ได้ก่อนซื้อ เลือกย่านความถี่คือการตัดสินใจทางวิศวกรรม ไอที · แยกปัญหาให้ถูกฝ่าย ping เกตเวย์ผ่าน แต่ออกเน็ตไม่ได้ = ไม่ใช่ปัญหาของอุปกรณ์ ส่งเรื่องให้ผู้ให้บริการ ประหยัดเวลาทั้งทีมได้เป็นวัน

ทั้งสี่มุมนี้ใช้ตัวเลขชุดเดียวกับที่ขึ้นบนจอบอร์ดวันนี้ ต่างกันแค่ว่าใครเป็นคนอ่านและตัดสินใจอะไรต่อ

ดูเพิ่มเติมนอกเวลา — คลิปที่ตรวจแล้วว่าเปิดได้

สองคลิปนี้เป็นการบ้านแบบสมัครใจ เนื้อหาในคาบเข้าใจได้ครบโดยไม่ต้องดู

DHCP DORA Process Explained
Sikandar Shaik CCIEx3 · 6:04 · อังกฤษ
ไล่ DORA ทีละขั้น เห็นภาพว่าเลข IP มาจากไหน
Wi-Fi 4-Way Handshake In Depth
Tall Paul Tech · 6:13 · อังกฤษ
สิ่งที่เกิดขึ้นระหว่างที่บอร์ด "กำลังต่อ" — การแลกกุญแจ

อ่านต่อสำหรับคนอยากรู้ลึก — ทำไมเลข dBm ถึงติดลบ https://dongknows.com/wi-fi-signal-strength-dbm-explained/ · RSSI ต่างจาก dBm อย่างไร https://www.oscium.com/training/resources/understanding-rssi/ · 2.4 กับ 5 GHz ภาษาไทย https://www.asus.com/th/support/faq/1044838/

คลิปทั้งสองไม่อยู่ในเกณฑ์ผ่าน แต่คนที่ดูจะเข้าใจว่าทำไม connect() ถึงใช้เวลาหลายวินาที

ต่อยอด — คิดต่อเอง (เลือกทำ 1 ข้อ)

ทั้งสี่ข้อใช้โค้ดวันนี้เป็นฐาน เพิ่มไม่เกินสิบบรรทัดต่อข้อ 1 · แผนที่สัญญาณ เดินวัด 5 จุดในห้อง แล้วเทียบกับ FSPL 2 · คุณภาพลิงก์ ping 100 ครั้ง หา min/avg/max + %หาย 3 · ล่าเกตเวย์ตัวจริง พิสูจน์ว่า .1 ถูกไหม แล้วเสนอวิธีเลิกเดา 4 · เฝ้าลิงก์ 10 นาที นับครั้งที่ timeout แล้วสรุปว่าเสถียรไหม

ข้อ 1 · แผนที่สัญญาณของห้องเรียน เดินถือบอร์ดไปห้าจุดที่ห่างจากเราเตอร์ต่างกัน จด RSSI ของ SSID เดียวกันพร้อมระยะโดยประมาณ วาดกราฟระยะเทียบ dBm แล้วอธิบายว่าทำไมของจริงแย่กว่าสูตร FSPL

ข้อ 2 · เครื่องวัดคุณภาพลิงก์ ยิง ping ไปที่เกตเวย์ 100 ครั้ง เก็บใน list แล้วหาค่าต่ำสุด เฉลี่ย สูงสุด และเปอร์เซ็นต์ที่ตอบไม่กลับ แสดงสี่ตัวเลขบนจอ · ทำไม "ค่าเฉลี่ยอย่างเดียว" ถึงหลอกเราได้

ข้อ 3 · ล่าเกตเวย์ตัวจริง ทดลอง ping .1, .254, .100 ของวงเดียวกันแล้วสรุปว่าอันไหนตอบ · เขียนสั้น ๆ ว่าถ้าเฟิร์มแวร์เปิด wifi.ifconfig() ให้ (ตอนนี้ยังไม่มี) โค้ดเราจะเปลี่ยนไปอย่างไร

ข้อ 4 · เฝ้าลิงก์สิบนาที ปล่อยโปรแกรมรันสิบนาทีโดยไม่แตะ นับครั้งที่ ping timeout และจดว่า RSSI เปลี่ยนไปกี่ dB แล้วสรุปว่าเครือข่ายห้องนี้เชื่อถือได้แค่ไหน

เขียนคำตอบลง worksheet ข้อ 8 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า

ปิดวงจร — ลิงก์มีไว้พาของออกไป ไม่ได้มีไว้ดูเล่น

แปดไฟล์ที่ผ่านมาดูแต่ตัวลิงก์ — สแกน ต่อ หลุด ต่อใหม่ examples/s09/09_link_gates_a_real_reading.py↗ เอาเซนเซอร์จริงมาต่อท้าย ให้เห็นว่าอะไรคือของที่รอส่ง และอะไรคือคนตัดสินว่าส่งได้หรือยัง

ค่าที่รอส่งคือ อุณหภูมิ อ่านผ่าน read_temp() ในไฟล์ — sensors.snapshot() ไม่มีช่องอุณหภูมิบนบอร์ดไหนเลย ไฟล์จึงถามก่อนว่าบอร์ดมี sensors.sht40 ไหม: บน Dev Kit ได้อุณหภูมิห้องจริง ส่วน บน Eva ไม่มีเซนเซอร์อุณหภูมิ ลูกบิดจึงเล่นบทแทน (0–100 % = 15–45 °C) และ console บอกไว้ตั้งแต่รอบแรกว่าค่ามาจากไหน — บทเรียนเรื่องคิวกับลิงก์เหมือนกันทั้งสองบอร์ด

กติกาข้อเดียวที่ต้องจำ — วัดกับส่งต้องแยกขาดจากกัน

ทำอะไร เมื่อไร เพราะอะไร
วัด ทุกรอบ ไม่ว่าเน็ตจะเป็นยังไง เซนเซอร์ไม่ได้พังตอนเน็ตหลุด
เก็บใส่คิว ทุกครั้งที่วัดได้ ค่าที่วัดได้ตอนออฟไลน์ยังมีค่า
ส่ง เมื่อลิงก์กลับมา และปล่อยทีละค่า ปล่อยรวดเดียวคือการยิงรัวใส่ปลายทาง

ถ้าเขียนว่า "วัดเมื่อเน็ตมา" ข้อมูลช่วงที่เน็ตหลุดจะหายไปตลอดกาล ทั้งที่เซนเซอร์ยังทำงานปกติ — และไม่มี error ให้จับสักตัว

ภาพจากตัวจำลอง bento_sim — โค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด · ค่าอุณหภูมิและสถานะลิงก์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

แถบบนคือลิงก์ (ออนไลน์พร้อม IP หรือหลุด) · การ์ดซ้ายคืออุณหภูมิที่วัดได้รอบล่าสุด · การ์ดขวาคือ จำนวนค่าที่ค้างอยู่ในคิว — ตัวเลขที่เดินขึ้นตอนลิงก์หลุด และไหลออกทีละค่าเมื่อลิงก์กลับมา

ลองบนโต๊ะ: ถอด WiFi ที่เราเตอร์ แล้วดูเลขคิวเดินขึ้น เสียบกลับแล้วดูมันไหลออก

รหัสผ่านของห้องเรียน ไม่ควรอยู่ในโค้ด — ui.Keyboard

ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ examples/s09/10_keyboard_types_the_password.py↗ สร้าง ชื่อวงในช่องซ้ายบนเครื่องโฮสต์เป็นค่าแทน บนบอร์ดจริงมันมาจาก wifi.scan()

14 ส.ค. 2026 บอร์ดสแกนเจอ 18 วง และไม่มีวงที่ตัวอย่างใช้อยู่ในนั้นเลย (HARDWARE_CHECKLIST.md ข้อ 6) ทั้งห้องค้างที่ป้าย "กำลังต่อ" พร้อมกัน เพราะ WIFI_SSID กับ WIFI_PASS เขียนตายอยู่ในไฟล์ทุกไฟล์ของคาบ 9 ถึง 12 — ของจริงทุกตัวที่ต่อ WiFi ได้ ถามรหัสผ่านจากคน ไม่ได้ฝังมาจากโรงงาน

ชิ้นส่วน หน้าที่
ui.Textarea ช่องที่ตัวอักษรไปโผล่
ui.Keyboard แป้นพิมพ์เต็มบนจอ
kb.bind(ta) บอกแป้นพิมพ์ว่าจะพิมพ์ลงช่องไหน — แป้นหนึ่งอันผูกได้ทีละช่องเดียว
.prop(ui.PROP_PASSWORD, 1) โหมดดาว คนที่ยืนข้าง ๆ ไม่ควรอ่านรหัสออก

สร้าง Textarea ก่อน Keyboard เสมอ — .bind() ไปยังแฮนเดิลที่ยังไม่ใช่ Textarea ถูก CM55 ปฏิเสธเงียบ ๆ

ui.Keyboard — เงียบจนกว่าจะขอ และอ่านกลับได้แล้ว

kb = ui.Keyboard(x=24, y=200, w=440, h=168, color=COL_TEXT)
kb.bind(ta_pass)
kb.listen("ready", "cancel")
...
        pw = ta_pass.text()
กติกา สิ่งที่เกิดจริง
ไม่ขอ ไม่ส่ง แป้นพิมพ์ไม่ส่ง event เองจนกว่าจะ kb.listen("ready", "cancel") — ปุ่มตกลงกับปุ่มปิดจึงรายงานกลับมา ค่าตั้งต้นคือเงียบ เพราะคิวเหตุการณ์มี 16 ช่อง
ตัวอักษรทีละตัวไม่ส่ง และนั่นถูกแล้ว — สิ่งที่โปรแกรมต้องรู้คือข้อความทั้งช่อง ไม่ใช่ปุ่มทีละใบ
อ่านกลับด้วย ta.text() ไม่ใส่อาร์กิวเมนต์ คืนสตริงที่อยู่ในช่องเดี๋ยวนั้น (GET_TEXT, opcode 0x6B ใน ipc_ui_protocol.h) — ไฟล์นี้จึงต่อด้วยรหัสที่พิมพ์จริง ไม่ใช่ค่าคงที่

จนถึง 15 ส.ค. 2026 ตาราง IPC_CMD_UI_* มี SET_TEXT (0x52) แต่ไม่มีคำสั่งอ่านกลับ หลักสูตรจึงเคยสอนว่า "สิ่งที่พิมพ์ คนอ่านได้ โปรแกรมอ่านไม่ได้" — ตอนนี้ไม่จริงแล้ว · ถ้าต้องประกอบสตริงจากปุ่มทีละใบด้วยเหตุผลอื่น ui.ButtonMatrix ยังทำได้ โดยส่ง value_changed พร้อมลำดับปุ่มกลับมา · อีกทางคือ SoftAP แล้วกรอกจากมือถือ ตาม examples/s09/08_softap_fallback.py↗

เลขที่ทำให้ต้องคิดเรื่องนี้ตั้งแต่ออกแบบ — เป้าสัมผัสตามเกณฑ์คือ 88 พิกเซล แป้นพิมพ์มีสี่แถว 4 x 88 = 352 พิกเซล ซึ่งกินพื้นที่วาด 398 ไปเกือบหมด ปุ่มบนแป้นพิมพ์จึงเล็กกว่าเกณฑ์เสมอบนจอ 4.3 นิ้ว นี่เป็นข้อจำกัดของขนาดจอ ไม่ใช่ของ widget

หน้าจอของทุกไฟล์ในคาบนี้ (1/3)

01 ผลของ wifi.scan() หน้าตาเป็นอย่างไร · 02 เรียงวงจากแรงไปอ่อน แล้วแปลง dBm ให้คนอ่านออก · 03 บอกก่อนแล้วค่อยรอ เพราะ connect() บล็อก
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

หน้าจอของทุกไฟล์ในคาบนี้ (2/3)

04 เกตเวย์ตอบ แต่อินเทอร์เน็ตไม่ตอบ แปลว่าอะไร · 05 อ่านสถานะครั้งเดียว แล้วใช้ค่าชุดนั้นทั้งรอบ · 06 หน้าจอสถานะลิงก์ ที่ทุกตัวเลขบนจอวัดมาจริง
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

หน้าจอของทุกไฟล์ในคาบนี้ (3/3)

07 สร้างสถานะ "เน็ตหลุด" ขึ้นมาดูเองตามสั่ง · 08 ถ้าในห้องไม่มีวงให้ต่อ บอร์ดปล่อยวงของตัวเองได้
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

อ้างอิงและเครดิต

เอกสารและมาตรฐาน

วิดีโอ (ตรวจแล้วว่าเปิดได้)

  • Wireless Client Association Process — RUCKUS Education Services · youtube.com/watch?v=WoUKXm9iG7k
  • DHCP DORA Procedure — CMSystemsBe · youtube.com/watch?v=oD5nC2NhHWQ
  • DHCP DORA Process Explained — Sikandar Shaik CCIEx3 · youtube.com/watch?v=ywkJepIQNIU
  • Wi-Fi 4-Way Handshake In Depth — Tall Paul Tech · youtube.com/watch?v=vHIRmG_BzQI

ภาพจาก Wikimedia Commons (ตามลำดับที่ปรากฏ) TaBaZzz · Sss41 · Kirlf (สเปกตรัม 2.4 GHz และ 5 GHz ที่วัดจริง, CC BY-SA 4.0) · Michael Gauthier, Wireless Networking in the Developing World · Superspritz · Gelmo96 · Michel Bakni · Aaron Filbert · Moeenrahi — CC BY-SA 3.0/4.0 ทั้งหมด วางทั้งภาพพร้อมเครดิต ไม่ได้ครอปหรือแก้ไข

ไดอะแกรมที่เหลือวาดขึ้นใหม่สำหรับหลักสูตรนี้ · ข้อเท็จจริงของโมดูล wifi ตรวจจากซอร์ส modwifi.c ใน BENTO-TESAIoT-libraries ซึ่งเป็นโค้ดร่วมของทั้ง Eva Kit และ TESAIoT Dev Kit — แปดชื่อ เพดาน 20 วง และกับดักทุกข้อในตารางจึงเหมือนกันสองบอร์ด · หน้า Wi-Fi Setting บนหน้า Home ก็เป็นหน้าเดียวกันทั้งคู่

ทุกตัวเลขบนสไลด์นี้สืบกลับไปที่เอกสารต้นทางหรือซอร์สโค้ดได้ ถ้าเจอที่ไม่ตรง บอกผู้สอนได้เลย

สองช่อง แป้นพิมพ์เดียว

ภาพจากตัวจำลอง Eva Kit (KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw/sim) ซึ่งรัน ipc_ui.c กับ ui_widget_mgr.c ตัวจริงเดียวกับบอร์ด บนพื้นที่วาด 792×398 (ขนาดเดียวกันบน Dev Kit) · แสดงหน้าจอที่ตัวอย่างสร้าง — หน้าจอของ examples/s09/11_two_fields_one_keyboard.py↗ · ค่าที่เห็นในภาพมาจากเซนเซอร์จำลองบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

ฟอร์มจริงมีมากกว่าหนึ่งช่อง แต่จอนี้ใส่แป้นพิมพ์ได้ใบเดียว — เหตุการณ์สองชนิดพอสำหรับทั้งฟอร์ม

ชนิด ใช้ทำอะไร
focused รู้ว่าผู้ใช้แตะช่องไหน แล้วผูกแป้นพิมพ์ไปที่ช่องนั้น
ready ผู้ใช้กดปุ่มตกลง — พิมพ์เสร็จแล้ว

ta.text() ที่ไม่ใส่อาร์กิวเมนต์ อ่านสิ่งที่ผู้ใช้พิมพ์กลับมาได้ (GET_TEXT, opcode 0x6B) · ตัวอักษรแต่ละตัวที่แตะ ไม่ ส่งเหตุการณ์มาทีละตัว และนั่นถูกแล้ว — สิ่งที่โปรแกรมต้องรู้คือข้อความทั้งช่อง ไม่ใช่ปุ่มทีละใบ

แป้นพิมพ์ใบเดียวย้ายไปตามช่องที่ถูกแตะ — focused คือเหตุการณ์ที่บอกว่าต้อง bind() ใหม่

fit-css

VIDEO-SLOT: คลิป 60-90 วินาที ถ่ายจอบอร์ดจริง — เปิดเมนู Wi-Fi Setting กด Scan ให้เห็นรายชื่อวงพร้อมขีดสัญญาณทยอยขึ้น เลือกวงของห้องเรียน ใส่รหัส กด Connect แล้วซูมให้เห็นไอคอน WiFi บนแถบบนสุดเปลี่ยนเป็นติด พร้อมแท็บ TCP/IP ที่ขึ้นเลข IP

วันนี้ไม่ได้เพิ่มเซนเซอร์ตัวใหม่ แต่เพิ่ม "ความสามารถในการอธิบายว่าทำไมมันไม่ทำงาน" ซึ่งมีค่ากว่าในงานจริง

อ่านตารางนี้ให้ครบก่อนเริ่มเขียน แล้วน้อง ๆ จะประหยัดเวลาไปได้มาก — กับดักเกินครึ่งของคาบนี้อยู่ในคอลัมน์ขวา ไม่ได้อยู่ในโค้ดที่เราเขียนผิด

ไฟล์ตัวหลัง (08_softap_fallback) คือแผนสำรองของทั้งคอร์ส ถ้าวันนั้นในห้องหาวงไม่เจอ

เจอ TypeError: tuple indices เมื่อไร ให้นึกถึงบรรทัดนี้ทันที — เกือบทุกครั้งคือเรื่องเดียวกัน

VIDEO-SLOT: คลิป 20-30 วินาที ถ่ายจอบอร์ดตอนผ่าน MVP — เห็นตารางซ้ายมีหัวตารางบวกสามแถวเรียงจากแรงไปอ่อน และแผงขวาแสดงไฟสถานะกับ SSID และ IP จริง แล้วรอให้เห็นเลข ping สองบรรทัดอัปเดตอย่างน้อยสองรอบ ปิดท้ายด้วยการเดินถือบอร์ดออกห่างเราเตอร์แล้วให้เห็นเลข ms เปลี่ยน

อย่าเติมครบหกจุดแล้วค่อยรันทีเดียว — การสแกนกับการต่อเน็ตพังคนละแบบ แยกรันแล้วจะรู้ทันทีว่าใครพัง

จนถึง 15 ส.ค. 2026 หลักสูตรนี้สอนว่า "สิ่งที่พิมพ์ คนอ่านได้ โปรแกรมอ่านไม่ได้" ซึ่งจริงในตอนนั้น — ไฟล์ที่บันทึกข้อจำกัดนั้นไว้ถูกแก้พร้อมกับความสามารถนี้ทุกไฟล์

☰ สารบัญ