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

ui.Table และเพิ่มไฟสถานะกับมาตรวัดในแผงขวาแล้ว ภาพชุดใหม่ยังไม่ได้ถ่ายโครงของหน้าจอรุ่นปัจจุบัน อ่านจากซ้ายไปขวา
ต่อเน็ตติดกับต่อเน็ตใช้ได้เป็นคนละเรื่อง หน้านี้แยกสองเรื่องนั้นให้เห็นในจอเดียว

หน้าจอเครื่องนี้ไม่ได้เขียนว่า "สัญญาณ 66%" แต่เขียนว่า −74 dBm ซึ่งเป็นหน่วยที่วัดกำลังจริง ๆ ของคลื่นที่เสาอากาศรับได้
อ่านเป็นภาษาคน: เอากำลังที่รับได้มาเทียบกับ 1 มิลลิวัตต์ แล้วบีบด้วยลอการิทึม เพราะช่วงค่ามันกว้างมากจนเขียนเป็นเลขธรรมดาไม่ไหว
ตัวเลขจริง: ที่ −67 dBm กำลังที่เสาอากาศรับได้คือ mW ≈ 0.2 นาโนวัตต์ — เล็กกว่ามิลลิวัตต์มหาศาล ลอการิทึมจึงออกมาติดลบเสมอ
0 dBm = 1 mW พอดี ซึ่งแรงกว่าที่ WiFi รับได้จริงหลายล้านเท่า เราจึงไม่มีวันเห็นเลขบวกบนบอร์ด
เลขติดลบไม่ได้แปลว่าผิดปกติ — มันคือธรรมชาติของหน่วยที่วัดของเล็ก ๆ เทียบกับของใหญ่
เกณฑ์ห้าขีดข้างบนไม่ได้คิดขึ้นเอง — เป็น เกณฑ์ชุดเดียวกับที่หน้าจอ Wi-Fi Setting ของบอร์ดใช้จริง (−50 / −60 / −70 / −80 / −90)
ตัวเลขที่ช่างเครือข่ายใช้กันหน้างาน: ดีกว่า −60 คือสบายทุกงาน · −67 คือขีดจำกัดของงานที่ต้องต่อเนื่องอย่างวิดีโอ/เสียง · ต่ำกว่า −80 อย่าไว้ใจ ต่อติดวันนี้พรุ่งนี้อาจหลุด
ทุก 3 dB คือประมาณ 2 เท่า และทุก 10 dB คือ 10 เท่า — จำสองข้อนี้แล้วอ่านเลข dBm ได้ทันทีโดยไม่ต้องกดเครื่องคิดเลข
เวลาทีมรายงานว่า "สัญญาณอ่อนไปนิดเดียว" ให้ถามกลับว่ากี่ dBm — ต่างกัน 20 dB คือต่างกันร้อยเท่า
เมื่อ เป็นกิโลเมตร และ เป็นเมกะเฮิรตซ์
อ่านเป็นภาษาคน: คลื่นที่แผ่ออกไปในที่โล่งจะจางลงตามระยะและตามความถี่ — ระยะเป็นสองเท่า หายไป 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

5 GHz ไม่ได้ "ดีกว่า" — มันแลกระยะทางกับความเร็ว เลือกให้ตรงกับงาน ไม่ใช่เลือกเลขที่มากกว่า
ย่าน 2.4 GHz มี 14 ช่อง และซ้อนทับกันอย่างที่เห็น — ในทางปฏิบัติใช้ได้จริงแค่ 3 ช่องที่ไม่ทับกันคือ 1, 6, 11
ย่าน 5 GHz มีช่องมากกว่านั้นหลายเท่า และบางช่องต้อง ฟังเงียบ ๆ ก่อนว่ามีเรดาร์ใช้อยู่ไหม จึงจะส่งได้
การสแกนหนึ่งครั้งคือการ ไล่ฟังทีละช่อง ช่องละไม่กี่สิบมิลลิวินาที ยิ่งมีช่องเยอะ ยิ่งใช้เวลานาน นี่คือเหตุผลที่บอร์ดหยุดนิ่งระหว่างสแกน ไม่ใช่เพราะมันค้าง
เชื่อมกับวันนี้: wifi.scan() ของเรา บล็อกได้ถึง 10 วินาที — โค้ดบรรทัดถัดไปจะไม่ทำงานเลยจนกว่าสแกนจบ ดังนั้นในท่าที่ 2 เราจะขึ้นข้อความ กำลังสแกน บนจอ ก่อน เรียกมัน ไม่ใช่หลัง ไม่งั้นผู้ใช้จะเห็นจอว่างเปล่าแล้วนึกว่าโปรแกรมพัง
1 · Scanning บอร์ดฟังหรือถามหาว่ามี AP ไหนอยู่แถวนี้ — wifi.scan() ทำงานอยู่ตรงนี้
2 · Authentication ขั้นตอนแนะนำตัว
3 · Association AP ตอบรับให้เข้าร่วม แล้วจึงเริ่มส่งข้อมูลได้
ตรงจุดที่ภาพเขียนว่า Data Transfer บอร์ดยัง ยังไม่มีเลข IP — ยังคุยกับใครนอกห้องไม่ได้
wifi.connect()คืนTrueเมื่อผ่านครบทั้งห้าขั้น ถ้าติดขั้นไหนก็ตาม เราได้Falseเหมือนกันหมด — จึงต้องมี ping ไว้แยกแยะ
สี่คำที่ต้องจำ DORA
Discover — มีใครแจกเลขบ้าง
Offer — เอาเลขนี้ไปไหม
Request — ขอเลขนี้
Acknowledge — เอาไปเลย
เลขที่ได้มาเป็นการ ยืมมาชั่วคราว (lease) ไม่ใช่ของเราถาวร — ปิดบอร์ดแล้วเปิดใหม่ อาจได้เลขคนละตัว นี่คือเหตุผลที่โค้ดของเราต้องอ่าน wifi.ip() ทุกครั้งหลังต่อ ห้ามจำเลขเก่าไว้ใช้
ถ้า DHCP ในห้องเต็มหรือพัง เราจะ "ต่อ WiFi ติด" แต่ "ไม่มีเลข IP" — อาการนี้ผู้เรียนจะเจอจริง และจะดูเหมือนต่อไม่ติดทั้งที่ไม่ใช่
| ค่า | ตัวอย่าง | มันบอกอะไร |
|---|---|---|
| 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 จะสอนวิธีตรวจว่าเราเดาถูกหรือเปล่า
wifi.ping(ip, timeout_ms) คืนเวลาไป-กลับเป็นมิลลิวินาที หรือ −1 ถ้าไม่มีคำตอบภายในเวลาที่ตั้งไว้
การยิงสองปลายทางแล้วอ่านผลเป็น คู่ ทำให้ตอบได้ทันทีว่าต้องไปแก้ที่ไหน — นี่คือท่าพื้นฐานที่วิศวกรเครือข่ายใช้ทุกวัน และเป็นเหตุผลที่การ์ดของเรามีสองบรรทัด ไม่ใช่บรรทัดเดียว
ระบบที่บอกได้ว่า "พังตรงไหน" มีค่ากว่าระบบที่บอกแค่ว่า "พัง"
wifi.ping() ถึงรับแต่ตัวเลขเครื่องคอมพิมพ์ 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 ให้

ping หนึ่งครั้งถูกห่อทีละชั้นก่อนกลายเป็นคลื่นวิทยุ: ข้อมูลของเรา → หัว IP (เลขต้นทาง-ปลายทาง) → หัวชั้นลิงก์ (MAC) → บิตที่ส่งออกอากาศ ปลายทางแกะย้อนกลับตามลำดับเดิม
ที่ต้องรู้วันนี้สองข้อ: แต่ละชั้นเพิ่มขนาดข้อมูล (มีค่าใช้จ่ายคงที่ต่อแพ็กเก็ต) และ แต่ละชั้นพังได้เอกเทศ — คลื่นแรงดีแต่ไม่มี IP เกิดขึ้นได้จริง · คาบ 10 เราจะเพิ่มชั้น MQTT ทับลงบนกองนี้
wifi.connect()จัดการชั้นล่างให้ ·wifi.ping()วัดชั้นกลาง · คาบหน้าเราจะเขียนชั้นบนสุดเอง
ไดรเวอร์ WiFi หนักกว่าโค้ดทั้งคาบนี้หลายพันบรรทัด — เราไม่ได้เขียนมัน เรา ใช้มันให้เป็น และนั่นคือทักษะที่ตรงกับงานจริงมากกว่า
โมดูล
wifiมี แปดชื่อ ไม่ใช่หก — อีกสองตัวคือwifi.disconnect()กับwifi.softap()ซึ่งโครงหลักไม่ได้เรียก แต่เราจะลงมือทั้งคู่ในสไลด์ถัดไป เพราะตัวหนึ่งคือวิธีพิสูจน์ว่าลิงก์หลุดจริง และอีกตัวคือทางออกเมื่อในห้องไม่มีวงให้ต่อ
ข้อ 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
disconnect() ทำให้เรา สร้างสถานะ "หลุด" ขึ้นมาดูได้ตามสั่ง ไม่ต้องรอให้เน็ตห้องล่มเอง — คาบ 12 ต้องเขียนโค้ดที่ทนการหลุด ถ้าไม่เคยเห็นว่าหลุดแล้วค่าไหนเปลี่ยน จะเขียนเงื่อนไขถูกได้ยาก
softap() เป็นทางออกจริงเมื่อวงของห้องไม่มี — เครื่องมือช่างและกล้องติดรถจำนวนมากตั้งค่าครั้งแรกด้วยวิธีนี้ ข้อแลกเปลี่ยนคือ วิทยุมีชุดเดียว เป็น AP แล้วจะเป็นลูกข่ายพร้อมกันไม่ได้ ping() ออกเน็ตจึงใช้ไม่ได้ในโหมดนี้ · ลองไฟล์ examples/s09/07_disconnect_rejoin.py↗ กับ examples/s09/08_softap_fallback.py↗
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) # "มีรหัส" คือข้อความที่ยาวที่สุดในคอลัมน์นี้
ทุก widget ถูกสร้าง ก่อน เข้าลูป ในลูปแค่เปลี่ยนข้อความกับค่า · ตารางเดียวแทน ui.Label สิบตัว — col_width คือสิ่งเดียวที่ต้องตั้งให้ถูก ช่องแคบกว่าข้อความ = ตัดบรรทัด = แถวสูงสองเท่า แถวสุดท้ายตกขอบเงียบ ๆ · ปุ่ม สแกนใหม่ อยู่บนแถบหัวเรื่อง เพราะ มุมขวาล่างเฟิร์มแวร์ถือไว้ให้ปุ่ม Console widget ที่ไปทับมันจะถูกบังจนกดไม่โดน
16 widgets จาก 64 — เหลือที่ให้ทีมเติมของตัวเองได้อีกมาก
# --- ท่าที่ 2 + 3 อยู่ในฟังก์ชันเดียว เพราะปุ่ม "สแกนใหม่" ต้องเรียกซ้ำได้ทั้งชุด ---
def rescan():
nets = wifi.scan() # บล็อกราว 3-10 วินาที
nets.sort(key=lambda net: net[1], reverse=True) # net[1] คือ rssi
sort() แก้ลิสต์เดิมในที่ · key=lambda net: net[1] สั่งให้เรียงตามช่องที่สอง · reverse=True ทำให้ค่ามากมาก่อน — RSSI ค่ามากคือแรงกว่า เพราะ −48 มากกว่า −79
ทำไมสองบรรทัดนี้ต้องอยู่ในฟังก์ชัน: ปุ่ม สแกนใหม่ ต้องทำงานทั้งชุด — สแกน เรียง ล้างตาราง เทใหม่ ถ้าปล่อยสองบรรทัดนี้ไว้กลางไฟล์ ปุ่มจะเรียกได้แค่ครึ่งเดียวของงาน
ตัวอย่างที่มากับเฟิร์มแวร์เขียนผิดจริง — ไฟล์ network/01_wifi_scan_connect.py บรรทัด 33 ยังเขียน net['ssid'] อยู่จนวันนี้ บทเรียนคือ โค้ดตัวอย่างไม่ใช่เอกสารอ้างอิง อย่าลอกไปใช้
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) + "%)")
— ยืดช่วง −90 ถึง −40 dBm ให้เป็น 0 ถึง 100 เพื่อบอก คุณภาพเป็นเปอร์เซ็นต์ ข้างตัวเลข dBm · ตัวเลขจริง: −67 dBm →
ui.Bar ไม่ต้องแปลงอะไรเลย เพราะตั้ง min=-90, max=-40 ได้ตรง ๆ — ที่ต้องแปลงคือเลขที่ คน จะอ่าน ไม่ใช่เลขที่ widget จะรับ · min(TOP_N, len(nets)) กันพังตอนสแกนเจอน้อยกว่าสามวง
signal_of()คืนทั้งเปอร์เซ็นต์และสีพร้อมกัน เพราะมาจากตัวเลขเดียว — แยกกันคำนวณเมื่อไร วันหนึ่งมันจะไม่ตรงกัน
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() รับ สองอาร์กิวเมนต์แบบตำแหน่งเท่านั้น — ไม่มี timeout= ไม่มี security= · raise SystemExit เมื่อต่อไม่ติดคือการตัดสินใจที่ตั้งใจ เพราะท่าที่ 5 ไม่มีความหมายถ้าไม่มีลิงก์ — ล้มเร็วดีกว่าปล่อยให้ลูปวนแสดง timeout จนคนดูสับสน · สองบรรทัดสุดท้ายคือ สถานะลิงก์เป็นไฟ ไม่ใช่ตัวอักษรสี — แปลงภาพเป็นขาวดำแล้วตัวอักษรเขียว/แดงกลายเป็นเทาเหมือนกัน ส่วนไฟติดกับไฟหรี่ยังแยกออกด้วยความสว่างและตำแหน่ง
ในระบบฝังตัว ทุกบรรทัดที่บล็อกนานกว่าครึ่งวินาที ต้องมีการแจ้งผู้ใช้กำกับเสมอ
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) ก้อนเดียว ปุ่มบนจอจะกดแล้วรอถึงสามวินาทีกว่าจะตอบ ซึ่งคนกดจะสรุปว่าปุ่มเสีย
try/except ครอบไว้เพราะ ความผิดพลาดชั่วคราวของเครือข่ายเป็นเรื่องปกติ ไม่ใช่ข้อยกเว้น · ส่วนตัวเลขนับถอยหลัง "วัดใหม่ใน N วิ" เขียนใหม่เมื่อ วินาทีเปลี่ยน เท่านั้น ไม่ใช่ทุกรอบลูป — ตัวเลขที่กระพริบห้าครั้งต่อวินาที คนอ่านไม่ทันและไม่มีใครได้ประโยชน์
ยิงเกตเวย์ก่อนแล้วค่อยยิงอินเทอร์เน็ต — ลำดับนี้ทำให้อ่านผลสองบรรทัดแล้วรู้ทันทีว่าขาดตรงไหน
เวลาดีบัก ให้ถามว่า "ขาดที่ช่วงไหนของห้าช่วงนี้" แล้วปัญหาจะเหลือแค่หนึ่งในห้า ไม่ใช่ทั้งระบบ
โครงหน้าจอถูกวางให้ต่อยอดจากแดชบอร์ดคาบ 8 ได้ทันที — ถ้าทีมอยากเอาการ์ด IMU กลับมาวางคู่กัน ให้ลด TOP_N จาก 3 เหลือ 2 แล้วตั้ง h ของตารางเป็น 216 (หัวตารางบวกสองแถว แถวละ 72) พื้นที่ที่ว่างขึ้นมาพอวางการ์ดเดิมได้
ถ้าจอค้างอยู่ที่ connecting นานเกินหนึ่งนาทีครึ่ง ให้กด RESTART แล้วตรวจ SSID กับรหัสผ่านทีละตัวอักษร — อย่ารอต่อ
ทีมละหนึ่งบอร์ด สลับกันพิมพ์ทุกช่วง คนที่ไม่ได้พิมพ์มีหน้าที่จับเวลาและจดตัวเลขที่เห็น
หน้าจอ network status ของทีม (SSID, IP, ping ms) อัปเดตสดบน HMI ที่ต่อยอดจากคาบ 8
แปลเป็นสิ่งที่ตรวจได้จริง:
connect() เอง และ เลข IP ที่ได้จาก DHCP — เลข IP ต้องไม่ใช่ค่าที่พิมพ์ไว้ในโค้ด ส่วน SSID บอร์ดตอบเองไม่ได้ (wifi.status()["ssid"] เป็นสตริงว่างเสมอ) ทีมจึงต้องจำสตริงที่ตัวเองส่งไปwifi.scan() ต้องอ่านด้วย net[1] ไม่ใช่ net['rssi']เกณฑ์ข้อที่หกกับเจ็ดตอบด้วยปากเปล่าได้ — คาบนี้วัดการวินิจฉัย ไม่ได้วัดความยาวโค้ด
เปิด 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
ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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
เกณฑ์สี −60 / −75 หลวมกว่าเกณฑ์ห้าขีดของหน้าจอบอร์ด เพราะการ์ดของเรามีสามระดับ — เลือกเกณฑ์ให้เข้ากับสิ่งที่จะแสดง ไม่ใช่ลอกมาทั้งชุด · ระดับ "ปกติ" คืนสีฟ้า COL_RUN ไม่ใช่เขียว สถานะปกติต้องเงียบ สีจัดสงวนไว้ให้เรื่องผิดปกติ — จอที่ระบายเขียวทั้งจอฝึกให้ตามองข้ามสี แล้ววันที่มีเรื่องจริง สีแดงก็กลืนไปกับพื้นหลัง
ค่าที่ต้องแก้บ่อยอยู่บนสุด ตรรกะอยู่กลาง หน้าจออยู่ล่าง — โครงนี้ใช้ได้กับทุกสคริปต์ตั้งแต่คาบ 1
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()
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)
ท่า 2 สแกน มาก่อนต่อ เพราะสแกนไม่ต้องใช้รหัสผ่าน — เจอวงแปลว่าวิทยุทำงาน · ท่า 3 แปลข้อมูล มาก่อนต่อเน็ต เพราะบั๊ก tuple/dict โผล่ตรงนี้ · ท่า 2 กับ 3 ถูกมัดเป็นฟังก์ชัน rescan() ตัวเดียว เพราะปุ่มบนจอต้องเรียกทั้งชุดซ้ำได้ — นั่นคือความต่างระหว่าง "สคริปต์ที่รันครั้งเดียว" กับ "หน้าจอที่คนใช้งานได้" · gateway_of() เดาเลขเกตเวย์เหมือนหน้าจอ C แต่เราไม่หยุดแค่เดา — ยิง ping เพื่อพิสูจน์
ไล่จากสิ่งที่พึ่งพาคนอื่นน้อยที่สุดไปหามากที่สุด — เป็นลำดับการดีบักมาตรฐานของงานฝังตัว
วันนี้เราได้: อ่าน RSSI เป็น dBm และเทียบกำลังได้ · แกะ tuple จาก wifi.scan() · ต่อเครือข่ายด้วย wifi.connect() โดยรู้ว่ามันบล็อก · วินิจฉัยลิงก์ด้วย ping สองปลายทาง · ประกอบทั้งหมดเป็นการ์ดสถานะบนหน้าจอเดิม
การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ต่อยอด บันทึกลง worksheet ข้อ 8
คาบหน้า: บอร์ดจะเริ่ม ส่งข้อมูลออกไปจริง ๆ ด้วย MQTT — เปิดคอมพิวเตอร์อีกเครื่องแล้วเห็นค่าจากบอร์ดวิ่งขึ้นบนหน้าจอนั้น และสั่ง LED กลับมาที่บอร์ดได้
เก็บโค้ดวันนี้ไว้ให้ดี คาบ 10 เริ่มจากไฟล์นี้ต่อโดยตรง — เพิ่ม
mqttทับลงบนลิงก์ที่เราเพิ่งทำให้ทำงาน
ทั้งสี่มุมนี้ใช้ตัวเลขชุดเดียวกับที่ขึ้นบนจอบอร์ดวันนี้ ต่างกันแค่ว่าใครเป็นคนอ่านและตัดสินใจอะไรต่อ
สองคลิปนี้เป็นการบ้านแบบสมัครใจ เนื้อหาในคาบเข้าใจได้ครบโดยไม่ต้องดู
อ่านต่อสำหรับคนอยากรู้ลึก — ทำไมเลข 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 · แผนที่สัญญาณของห้องเรียน เดินถือบอร์ดไปห้าจุดที่ห่างจากเราเตอร์ต่างกัน จด 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 ให้จับสักตัว
09_link_gates_a_real_reading.py
แถบบนคือลิงก์ (ออนไลน์พร้อม IP หรือหลุด) · การ์ดซ้ายคืออุณหภูมิที่วัดได้รอบล่าสุด · การ์ดขวาคือ จำนวนค่าที่ค้างอยู่ในคิว — ตัวเลขที่เดินขึ้นตอนลิงก์หลุด และไหลออกทีละค่าเมื่อลิงก์กลับมา
ลองบนโต๊ะ: ถอด WiFi ที่เราเตอร์ แล้วดูเลขคิวเดินขึ้น เสียบกลับแล้วดูมันไหลออก
ui.Keyboard
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



เอกสารและมาตรฐาน
scan() connect() เหมือน Eva ทุกประการ (โค้ด modwifi.c เป็นชุดเดียวกัน)วิดีโอ (ตรวจแล้วว่าเปิดได้)
youtube.com/watch?v=WoUKXm9iG7kyoutube.com/watch?v=oD5nC2NhHWQyoutube.com/watch?v=ywkJepIQNIUyoutube.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 ก็เป็นหน้าเดียวกันทั้งคู่
ทุกตัวเลขบนสไลด์นี้สืบกลับไปที่เอกสารต้นทางหรือซอร์สโค้ดได้ ถ้าเจอที่ไม่ตรง บอกผู้สอนได้เลย

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 หลักสูตรนี้สอนว่า "สิ่งที่พิมพ์ คนอ่านได้ โปรแกรมอ่านไม่ได้" ซึ่งจริงในตอนนั้น — ไฟล์ที่บันทึกข้อจำกัดนั้นไว้ถูกแก้พร้อมกับความสามารถนี้ทุกไฟล์