คาบ 2 — บอร์ดคุยกับโลก

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

คาถาประจำคาบ: คาบที่แล้วบอร์ดพูดกับเรา คาบนี้มันพูดกับคนที่ไม่ได้อยู่ในห้องนี้

ดูของจริงก่อน — วันนี้ค่าหนึ่งค่าจะเดินทางออกจากโต๊ะนี้

เรื่องทั้งคาบมีสองลูกศร และมันวิ่งสวนทางกัน บอร์ดบนโต๊ะเรา หมุนลูกบิดจริง หลอด LED จริง sensors · gpio ที่พักข้อความ broker.hivemq.com ใครฟังหัวข้อไหน ก็ได้ของ สาธารณะ ไม่ต้องตั้งเอง คนที่อยู่คนละที่ เห็นเลขของเราขยับ แล้วพิมพ์คำสั่งกลับมา หน้าเว็บ ไม่ต้องลงโปรแกรม เขียวคือไฟล์ 05 ค่าออกไป ม่วงคือไฟล์ 06 คำสั่งกลับมา

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

ตรงกลางภาพคือ สิ่งที่ทำให้สองฝั่งไม่ต้องรู้จักกัน เราไม่ต้องรู้ว่าใครจะมาอ่านค่าของเรา และคนที่สั่งกลับมาก็ไม่ต้องรู้ว่าบอร์ดเราอยู่ที่ไหนบนโลก ทั้งคู่รู้แค่ชื่อหัวข้อเดียวกัน

ระบบ IoT จริงทั้งโลกวางบนภาพนี้ ไม่ว่าจะเป็นมิเตอร์ไฟหน้าบ้าน หรือรถบรรทุกที่วิ่งอยู่อีกจังหวัด

ปลายทางของคาบนี้ — และภาพจริงภาพเดียวที่เรามีตอนนี้

หน้าจอจริงจากการรัน examples/s02/05_value_leaves_the_board.py↗ บน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ภาพนี้ถ่ายตอนที่ Emulator ยังใช้ broker จำลอง ค่าในภาพจึงไม่ได้ออกไปไหนจริง ภาพนี้แสดงหน้าตาจอ ไม่ใช่หลักฐานว่าส่งถึง · ภาพนี้เก่า ในภาพ broker ยังเป็นเลขแลน 192.168.1.50 และหัวข้อเป็น bento/eva-team03/telemetry ส่วนไฟล์ปัจจุบันใช้ broker.hivemq.com กับหัวข้อ bento-aiot/team03/telemetry ส่ง 60 ใบ และส่งค่าเอียง az ด้วย รอถ่ายใหม่
  • ภาพนี้คือไฟล์ที่ 4 ของคาบ และคือปลายทางจริง: บันไดสามขั้นไล่เป็นเขียวครบ — WiFi ได้ IP · broker ต่อแล้ว · publish กำลังส่ง — เลขใบที่ส่งเดินขึ้นพร้อมค่าลูกบิดจริงที่เพิ่งออกจากบอร์ดไป (ในภาพเก่า broker ยังเป็นเลขแลน วันนี้ขั้นที่ 2 จะขึ้นว่า broker.hivemq.com)
  • แถวล่างคือหัวข้อที่ส่งขึ้น broker กับคำเตือนที่ไฟล์เขียนไว้บนจอตลอดเวลาที่รัน — พอร์ต 1883 ไม่เข้ารหัส ห้ามส่งของลับ
  • ภาพนี้เกิดได้ต่อเมื่อเน็ตของห้องปล่อยพอร์ต 1883 ออกไปถึง broker.hivemq.com — ถ้าถูกกัน ขั้นที่ 2 จะเป็นแดงพร้อมบอกเหตุผล แล้วโปรแกรมจบตรงนั้นอย่างสุภาพ ส่วนฝั่งผู้รับ วันนี้คือหน้าเว็บ examples/web/my_first_reader.html ไม่ต้องลงโปรแกรมใดในเครื่อง

ยังไม่มีใครรันไฟล์ 05 กับ 06 บนบอร์ดกับ broker.hivemq.com สิ่งที่วัดแล้วคือเวลาไปกลับของ broker จาก Mac บนโต๊ะผู้สอน ไม่ใช่จากเน็ตมหาวิทยาลัย ถ้าไปไม่ถึง ไฟล์จะขึ้นบนจอตรง ๆ ว่าหยุดที่ขั้นไหนและเพราะอะไร — ผลแบบนั้นก็ยังเป็นผลที่ใช้ได้

เจ็ดไฟล์ · เรื่องเดียวเล่าเป็นเจ็ดตอน

คาบนี้ไม่ได้สอนโมดูล wifi มันพาบอร์ดไปให้ถึงคนอื่น ตอนที่หนึ่ง · ขึ้นวงให้ได้ 01 ต่อ · 04 ฟังทั้งห้อง wifi · lcd · ui · time ตอนที่สอง · เชื่อได้แค่ไหน 02 เฝ้าลิงก์ · 03 เขียนกฎเอง ของเดิมทั้งชุด ไม่มีคำสั่งใหม่ ตอนที่สาม · ออกไปถึงคนอื่น 05 ส่งออก · 06 รับกลับ · 07 จำเอง mqtt · json · sensors · gpio โมดูลที่คาบนี้แตะจริงทั้งหมด wifi · mqtt · tesaiot · json · sensors · gpio · lcd · ui · time สี่ตัวแรกเป็นของใหม่วันนี้ ห้าตัวหลังคือของเดิมจากคาบที่แล้วที่กลับมาใช้อีก

สี่โมดูลใหม่วันนี้ทำหน้าที่คนละอย่างกันชัดเจน wifi พาบอร์ดขึ้นวง · mqtt ส่งข้อความออกและรับกลับ · json แปลงข้อมูลของเราให้เป็นข้อความที่ทุกภาษาอ่านได้ · tesaiot เก็บค่าตั้งไว้บนแฟลชของบอร์ดเอง

ที่เหลือเป็นของเดิม sensors อ่านลูกบิด gpio สั่งหลอดไฟ ส่วน lcd ui time คือชุดเดิมจากคาบที่แล้วทุกประการ

ไม่ต้องจำรายชื่อนี้ตอนนี้ แต่ละไฟล์จะแนะนำของของมันเองตอนที่ถึงคิว

เป้าหมายของคาบนี้

  1. พาบอร์ดขึ้นเครือข่ายด้วย wifi.connect() แล้วอ่านหมายเลข IP ของบอร์ดตัวเองได้
  2. ให้บอร์ดสำรวจคลื่นทั้งห้องด้วย wifi.scan() แล้ววินิจฉัยเองได้ว่าต่อไม่ติดเพราะอะไร
  3. แยก "ต่อติดตอนนั้น" ออกจาก "ยังต่ออยู่ตอนนี้" แล้วเขียนกฎเองว่าลิงก์แบบไหนเรียกว่าใช้ได้
  4. ส่งค่าจริงจากบอร์ดออกไปให้เครื่องอื่นเห็น แล้วรับคำสั่งจากที่ไกลกลับมาสั่งของบนบอร์ด

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

เปิดบอร์ดรอไว้ได้เลย เล่นเกมเปิดคาบเสร็จ เรารันไฟล์แรกทันที — คาบนี้ทฤษฎีมาทีหลังของจริงทุกเรื่อง

ก่อนเริ่ม — ทุกทีมเปิด Hotspot มือถือให้บอร์ด

WiFi ของคณะต้อง login ผ่านหน้าเว็บก่อนใช้งาน บอร์ดไม่มีเบราว์เซอร์ให้กดยอมรับ บอร์ดจะได้เลข IP แต่ออกอินเทอร์เน็ตไม่ได้ ไปไม่ถึง broker · วันนี้บอร์ดของทุกทีมต่อ Hotspot จากมือถือของคนในทีมหนึ่งเครื่อง

ขั้น iPhone Android
1 · ตั้งชื่อ ชื่อ Hotspot คือชื่อเครื่อง: การตั้งค่า > ทั่วไป > เกี่ยวกับ > ชื่อ ตั้งเป็น bento-team05 การตั้งค่า Hotspot > ชื่อเครือข่าย ตั้งเป็น bento-team05
2 · ตั้งรหัส ฮอตสปอตส่วนบุคคล > รหัสผ่าน Wi-Fi (อย่างน้อย 8 ตัว) รหัสผ่าน อย่างน้อย 8 ตัว ความปลอดภัย WPA2
3 · ย่านความถี่ เปิด เพิ่มความเข้ากันได้สูงสุด (Maximize Compatibility) เลือกย่าน 2.4 GHz ถ้ามีให้เลือก
4 · เปิดค้าง เปิดหน้าฮอตสปอตค้างไว้ตอนบอร์ดต่อครั้งแรก เปิด Hotspot ค้างไว้

แล้วแก้สองบรรทัดบนหัวทุกไฟล์ที่ต่อเน็ตให้ตรงกับมือถือ:

WIFI_SSID = "bento-team05"          # ชื่อ Hotspot ตัวอักษรอังกฤษ ไม่มีช่องว่าง
WIFI_PASS = "<รหัส Hotspot ของทีม>"
  • ตั้งชื่อเป็นตัวอักษรอังกฤษสั้น ๆ เพราะชื่อวงขึ้นบนจอบอร์ด ชื่อเครื่องแบบ สมชาย’s iPhone มีอักขระที่จอวาดไม่ได้ และยาวจนข้อความบนจอถูกตัด
  • บอร์ดใช้เน็ตน้อยมาก ส่งข้อความสั้น ๆ ราวใบละร้อยไบต์ทุก 2 วินาที
  • โน้ตบุ๊กที่เปิดหน้าเว็บ ต่อ WiFi คณะ (login ตามปกติ) ได้ ถ้าหน้าเว็บต่อ broker ไม่ได้ ให้ย้ายมาต่อ Hotspot เดียวกับบอร์ด

ยังไม่ได้ทดสอบกับ Hotspot ครบทุกรุ่น ถ้าบอร์ดหา Hotspot ไม่เจอ ให้เช็กข้อ 3 และข้อ 4 ก่อน แล้วลองรันไฟล์ 04_scan_the_room.py ดูว่าบอร์ดได้ยินชื่อนั้นไหม

ก่อนเริ่ม — ส่งข้อความถึงบอร์ดหน้าห้อง

เครื่องของคุณ เครื่องของเพื่อน อีกราว 40 เครื่อง broker.hivemq.com bento-aiot/team00/cmd บอร์ดหน้าห้อง ไฟล์ 06 · team00 กล่องรับกี่ช่อง

เปิดหน้ารวม https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/mqtt_dashboard.html บนเครื่องตัวเอง ไม่ต้องติดตั้งอะไร หน้าจะต่อ broker เอง ช่อง ทีม ในหัวข้อ "ส่งคำสั่ง" พิมพ์ team00 แล้วกด บี๊บ หรือพิมพ์ข้อความสั้น ๆ แล้วกด ส่งข้อความ (say) บอร์ดบนโต๊ะผู้สอนรัน examples/s02/06_command_comes_back.py↗ เป็น team00 อยู่ มันจะร้อง หรือขึ้นข้อความของคุณ (ตัดที่ 24 ตัวอักษร)

ทายก่อน แล้วค่อยดู ผู้สอนนับหนึ่งถึงสาม ทุกคนกดส่งพร้อมกัน ทีมเขียนคำทายลงใบงานข้อ 4.5 ก่อน เขียนแล้วห้ามแก้

ทีมเราทายว่าตัวเลข "ได้รับแล้ว" บนจอบอร์ดหน้าห้องจะขึ้น ☐ น้อยกว่า 10 ☐ 10 ถึง 30 ☐ ครบทุกคน

พิมพ์ภาษาไทยหรืออังกฤษเท่านั้น จอบอร์ดไม่มีตัวอักษรอื่น · ส่งจากหน้าเว็บ · ยังไม่มีใครนับว่าจริง ๆ จะขึ้นกี่ใบ ผลที่เห็นในห้องคือคำตอบ แล้วเราจะกลับมาดูว่าทำไมที่สไลด์ไฟล์ 06

ลงมือ 15 นาทีแรก — รันไฟล์แรกก่อนฟังอะไรทั้งนั้น

1 · เปิด Playground ค้างไว้บนจอบอร์ด 2 · แก้สองบรรทัดบนสุด WIFI_SSID · WIFI_PASS 3 · Program to Device แล้วมองจอบอร์ด 4 · จับเวลาที่จอนิ่ง นับในใจก็ได้ จอจะนิ่งไปนาน นั่นไม่ใช่บอร์ดแฮงก์ อย่าเพิ่งกดรีเซ็ต อย่าเพิ่งถอดสาย รอให้มันตอบกลับมาเอง

เปิด examples/s02/01_wifi_first_connect.py↗ แก้สองบรรทัดบนสุดให้ตรงกับเครือข่ายที่ผู้สอนแจกหน้าห้อง แล้วส่งขึ้นบอร์ดเลย

# แก้สองบรรทัดนี้ให้ตรงกับเครือข่ายที่ผู้สอนแจกหน้าห้อง
WIFI_SSID = "bento-teamXX"            # ชื่อ Hotspot มือถือของทีม (WiFi คณะต้อง login บอร์ดใช้ไม่ได้)
WIFI_PASS = "<รหัส Hotspot ของทีม>"     # อย่างน้อย 8 ตัว

สิ่งที่ต้องจดระหว่างรอ: จอค้างนานแค่ไหน (นับในใจก็ได้) แล้วเทียบกับตัวเลข ms ที่บอร์ดรายงานตอนกลับมา ตรงกันไหม

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

ทีมที่ต่อติดตั้งแต่ยี่สิบนาทีแรก จะมีเวลาเหลือไปถึงไฟล์ 05 กับ 06 อย่างสบาย ๆ ซึ่งเป็นสองไฟล์ที่สนุกที่สุดของคาบ

โมดูล wifi มีแปดตัว — ห้าตัวนี้ใช้วันนี้

wifi.connect(ssid, pw) คืน True หรือ False บล็อก โปรแกรมหยุดรอ ตอบว่า ตอนนั้นสำเร็จไหม wifi.ip() คืนสตริงเสมอ ไม่เคยคืน None ยังไม่มีที่อยู่ = "0.0.0.0" ตอบว่า เลขที่อยู่ของเราคือ wifi.is_connected() คืน True หรือ False ไม่บล็อก ถามได้ทุกรอบ ตอบว่า ตอนนี้ยังต่ออยู่ไหม wifi.scan() ทุกวงที่ได้ยิน พร้อมความแรงจริง wifi.status() มี ssid กับ rssi แต่สองคีย์นั้นปลอม

สามตัวบนใช้ตอนต่อ สองตัวล่างใช้ตอนสำรวจและตอนหาสาเหตุว่าทำไมต่อไม่ติด

อีกสามตัวที่เหลือ — wifi.disconnect() ตัดการเชื่อมต่อเอง · wifi.ping(host) วัดว่าปลายทางตอบไหมและช้าแค่ไหน · wifi.softap() เปลี่ยนบอร์ดเป็นตัวปล่อยสัญญาณเองเมื่อไม่มีเราเตอร์ให้เกาะ ทั้งสามอยู่ในคาบ 9 (examples/s09/04, 07, 08)

ตรวจเองได้เสมอว่าโมดูลไหนมีอะไร — พิมพ์ print(dir(wifi)) บนบอร์ด แล้วนับเอง อย่าเชื่อสไลด์รวมทั้งแผ่นนี้

lcd ui และ time ยังเป็นชุดเดิมจากคาบที่แล้วทุกประการ — ui.poll() หลังแก้ widget · ui.Seg7 รับข้อความ · ui.Chart เกิดมาพร้อมเส้นที่ 0 อยู่แล้ว · lcd.print() ส่งได้ 127 ไบต์ต่อครั้ง ส่วนป้าย ui พาได้ 126 ไบต์ทั้งตอนสร้างและตอนเรียก .text()

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

แกะไฟล์ 01 — ป้ายต้องขึ้น "ก่อน" บรรทัดที่บล็อก แล้วจับเวลาคร่อมมัน

step_lbl = ui.Label("ป้ายนี้ขึ้นก่อน แล้วจอจะนิ่ง", x=24, y=208, color=COL_TEXT,
                    value=20)
...
ui.poll()                        # เคาะให้ป้ายขึ้นจอจริง ๆ ก่อนเข้าบรรทัดที่บล็อก
...
t0 = time.ticks_ms()
ok = wifi.connect(WIFI_SSID, WIFI_PASS)
elapsed = time.ticks_diff(time.ticks_ms(), t0)
...
seg.text(str(elapsed))           # Seg7 รับข้อความ จึงคุมรูปแบบได้ seg.value(4218) ได้แค่จำนวนเต็ม
ท่านี้บล็อกได้นานถึงราว 85 วินาที จึงต้องบอกคนดูก่อน ไม่ใช่หลัง ป้าย + ui.poll() wifi.connect(ssid, pw) โปรแกรมหยุดรอตรงนี้ True → IP False → แจ้ง ระหว่างแถบนี้เดิน จอไม่ขยับเลยแม้แต่พิกเซล 4218 ms รหัสถูก 85310 ms รหัสผิด ตัวเลขบนจอเป็นของบอร์ดเอง ไม่ใช่ความรู้สึกว่า "นานจัง"

ป้ายที่สร้างไว้แต่ยังไม่ได้เคาะ จะไปโผล่ หลัง connect() จบ ซึ่งคือตอนที่ไม่มีใครต้องการมันแล้ว กฎ ui.poll() จากคาบที่แล้วกลายเป็นเรื่องเป็นเรื่องตายขึ้นมาตรงนี้

time.ticks_ms() กับ ticks_diff() เป็นคู่เดิมจากไฟล์ 07 ของคาบที่แล้ว ที่ต่างคือคราวนี้เราเอามันมาคร่อมบรรทัดที่ควบคุมไม่ได้ เพราะ "นานจัง" ไม่ใช่ข้อมูล ส่วน "4218 ms" เอาไปเทียบกันได้ทั้งระหว่างทีมและระหว่างรหัสถูกกับรหัสผิด

ถ้า ok เป็น False ไฟล์นี้รายงานแล้ว raise SystemExit ทันที โปรแกรมที่พูดเฉพาะตอนสำเร็จ จะเงียบในจังหวะที่คนอยากรู้ที่สุด

กับดักที่ชื่อ "0.0.0.0" — และทำไมรหัสผิดถึงช้ากว่ารหัสถูก

ภาพ: Wikimedia Commons — ลำดับการเข้าร่วมเครือข่ายไร้สาย จากการค้นหา ไปแนะนำตัว แล้วจึงขอเข้าร่วม · คาบ 9 เราจะเจาะสามจังหวะนี้ทีละขั้น วันนี้แค่รู้ว่ามันมีหลายขั้นก็พอ
ip = wifi.ip()
# ip() คืนสตริงเสมอ ตอนยังไม่มีที่อยู่มันคืน "0.0.0.0"
# ซึ่งเป็นสตริงที่ if ถือว่าจริง — ต้องเทียบตรง ๆ เท่านั้น
if ip == "0.0.0.0":
    note.text("ลิงก์ขึ้นแล้วแต่ยังไม่ได้เลข IP กำลังรอ DHCP")

การเข้าร่วมเครือข่ายกับการได้เลขที่อยู่เป็นคนละขั้น ลิงก์อาจขึ้นแล้วแต่ยังไม่มีใครแจกเลขให้ ช่วงนั้น wifi.ip() คืน "0.0.0.0" ซึ่ง Python ถือว่าจริง เพราะมันไม่ใช่สตริงว่าง เขียน if wifi.ip(): จึงผ่านทั้งที่ยังส่งอะไรออกไม่ได้

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

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

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

ไฟล์ 04 · examples/s02/04_scan_the_room.py↗ — บอร์ดได้ยินทั้งห้อง

nets = wifi.scan()                       # คืน list ของ (ssid, rssi, security, channel)
...
nets.sort(key=lambda net: net[1], reverse=True)   # เรียงเอง scan() ไม่เรียงให้
...
st = wifi.status()
...
trap.text("status() rssi = " + str(st["rssi"]) + " แต่ของจริง " +
          str(top_rssi) + " dBm")
scan() ไม่ต้องรู้รหัสผ่านใครเลย ได้ชื่อวง ความแรงจริง ช่อง และมีรหัสไหม บล็อกราว 3 ถึง 10 วินาที ย่าน 5 GHz นานกว่า ป้ายกับ ui.poll() จึงต้องมาก่อนบรรทัดนี้อีกครั้ง status() มีสองคีย์ที่เชื่อไม่ได้ ssid = "" rssi = 0 เฟิร์มแวร์ยัดค่าตายตัวไว้ ไม่ใช่ค่าที่วัดมา เอาไปวาดกราฟ จะได้เส้นศูนย์ตลอดกาลโดยไม่มี error

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

ไฟล์นี้สร้างป้ายห้าแถวไว้ครบตั้งแต่ต้น ก่อน จะรู้ว่าเจอกี่วง เพราะถ้าไปสร้างข้างในลูป จำนวน widget จะขึ้นกับจำนวนวงที่บังเอิญเจอในห้องนั้น ห้องที่มีวงเกินงบของคอร์ส 32 ตัว (เพดานเฟิร์มแวร์ 64) จะทำให้โปรแกรมตายกลางคัน ทั้งที่โค้ดไม่ได้ผิด

ตาคุณ อยู่ท้ายไฟล์ เดินถือบอร์ดไปสุดห้องแล้วรันซ้ำ จดชื่อวงเดิมกับตัวเลข dBm ทั้งสองจุด แล้วดูว่าอันดับของมันเลื่อนไหม

งานของจอ · ทำทุกรอบ ตอบว่า "ตอนนี้เป็นยังไง" ป้าย IP · วินาทีที่ออนไลน์ · เส้นกราฟ ถาม is_connected() ใหม่ทุกรอบ ไม่เชื่อคำตอบเดิมจากรอบก่อน งานของลิ้นชัก · เฉพาะตอนเปลี่ยน ตอบว่า "ที่ผ่านมาเกิดอะไร" หลุดวินาทีที่เท่าไร กลับมาเมื่อไร ยิงทุกรอบเมื่อไร ได้บรรทัดเดิมสามสิบบรรทัด แล้วคำถาม "หลุดตอนไหน" หาคำตอบไม่เจอ
    up = wifi.is_connected()          # ถามใหม่ทุกรอบ นี่คือทั้งบทเรียนของไฟล์นี้
    now = 1 if up else 0
    ...
    if up:                            # --- งานของจอ ---
        ...
        link_lbl.text("ต่ออยู่")
        ...
        chart.set_next(s_link, 100)
    else:
        link_lbl.text("หลุด")
        ...
        chart.set_next(s_link, 0)
    ...
    if now != last:                   # --- งานของลิ้นชัก ---
        last = now
        ...
            lcd.print("<span class=error>" + str(elapsed // 1000) +
                      " s  ลิงก์หลุด</span>")

ไฟล์นี้ไม่มีคำสั่งใหม่เลยสักตัว โครงเป็นของ examples/s01/08_status_screen.py↗ เป๊ะ ๆ เปลี่ยนแค่ต้นทางของค่า ของใหม่คือการเอาสี่โมดูลมาต่อกันบนจอใบเดียว ซึ่งเป็นสิ่งที่นักพัฒนามืออาชีพ 80 คนบอกว่าตัวอย่างโค้ดทั่วโลกขาดไปมากที่สุด

last = -1 แปลว่า "ยังไม่เคยรู้สถานะมาก่อน" รอบแรกจึงนับเป็นการเปลี่ยนเสมอ ตั้งต้นเป็น 1 เมื่อไร ประวัติจะไม่มีบรรทัดแรก

ตาคุณ อยู่ท้ายไฟล์ ระหว่างที่โปรแกรมกำลังเฝ้าดู เดินถือบอร์ดออกไปจนสุดห้องแล้วเดินกลับมา แล้วเปิดลิ้นชักดูว่าได้กี่บรรทัด และหลุดตอนวินาทีที่เท่าไร

วันนี้ข้อความเดินสองทาง — บอร์ดใช้ 1883 เบราว์เซอร์ใช้ wss 8884

สองทางคนละพอร์ต แต่ไปเจอกันที่ broker ตัวเดียวกัน หัวข้อเดียวกัน บอร์ดของทีม ไฟล์ 05 · 06 mqtt ไม่เข้ารหัส broker.hivemq.com สาธารณะ ไม่มีรหัสผ่าน bento-aiot/team03/... เบราว์เซอร์ my_first_reader.html ไม่ต้องลงโปรแกรม TCP 1883 wss 8884 Emulator: wss 8884 ไม่มีสายออกนอกเบราว์เซอร์ ต่อ "ติด" เสมอแต่ไม่มีอะไรไปถึง hivemq คนที่ใช้ Emulator อย่างเดียว ดูบอร์ดจริงของทีมผ่านหน้าเว็บแทน

บอร์ดพูด MQTT ธรรมดาที่พอร์ต 1883 ส่วนหน้าเว็บเปิดสาย TCP ดิบไม่ได้ จึงพูด MQTT ผ่าน WebSocket ที่ wss://broker.hivemq.com:8884/mqtt ทั้งสองทางลงหัวข้อชุดเดียวกัน broker ส่งต่อให้เองโดยไม่สนว่าแต่ละฝั่งมาทางไหน

วัดแล้ว 24 ก.ย. 2026 จาก Mac บนโต๊ะผู้สอน ไม่ใช่จากเน็ตมหาวิทยาลัย: 1883 ไป wss และ wss ไป 1883 ส่งถึงทั้งคู่ ไปกลับ 188-191 ms · ยังไม่ได้วัด: ยังไม่มีใครรันไฟล์ 05 กับ 06 บนบอร์ดกับ broker นี้ และยังไม่รู้ว่าเน็ตมหาวิทยาลัยปล่อยพอร์ต 1883 กับ 8884 ออกไปหรือไม่

ตัวสำรอง ใช้เมื่อผู้สอนประกาศเท่านั้น: บอร์ด BROKER = "test.mosquitto.org" พอร์ต 1883 · หน้าเว็บ wss://test.mosquitto.org:8081/mqtt (วัดแบบเดียวกัน 185-201 ms) · อย่าใช้ broker.mqttdashboard.com ใบรับรองไม่ตรงชื่อ หน้าเว็บต่อไม่ได้

ผู้สอน ก่อนเข้าห้อง ให้บอร์ดของผู้สอนต่อ Hotspot มือถือ (WiFi คณะต้อง login บอร์ดใช้ไม่ได้) เปิดหน้ารวม https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/mqtt_dashboard.html แล้วให้บอร์ดหนึ่งตัวรัน examples/s02/05_value_leaves_the_board.py↗ สักใบ การ์ดของทีมขึ้น = ทั้งสองทางผ่าน · ไม่ขึ้น = ลองตัวสำรอง

broker สาธารณะ: ใครก็อ่านได้ ใครก็เขียนได้

broker นี้ไม่มีรหัสผ่าน ใครรู้ชื่อหัวข้อก็อ่านและเขียนได้ทันที หัวข้อไม่ซ้ำใคร bento-aiot/team03/... ห้าม subscribe # เท่ากับขอรับทั้งโลก ห้ามส่งความลับ ไม่เข้ารหัส ใครก็อ่านได้ client_id ไม่ชน ชนได้กับทุกคนในโลก
  • หัวข้อต้องไม่ซ้ำใคร ทั้งห้องใช้ชุดเดียว bento-aiot/<ทีม>/telemetry (บอร์ดส่งค่า) · bento-aiot/<ทีม>/event (เหตุการณ์ เริ่มใช้คาบ 3) · bento-aiot/<ทีม>/cmd (คำสั่งเข้าบอร์ด) และผู้สอนแจก TEAM เป็น team01 ถึง team19
  • ห้าม subscribe # เครื่องหมายนี้แปลว่าทุกหัวข้อบน broker ข้อความของคนแปลกหน้าทั้งโลกจะไหลเข้ามา และกล่องรับของบอร์ดมีช่องเดียว ใบใหม่ทับใบเก่า · หน้าเว็บของทีมฟัง bento-aiot/team03/# หน้ารวมของผู้สอนฟัง bento-aiot/+/telemetry
  • ห้ามส่งความลับ ไม่ว่ารหัส WiFi ชื่อจริง หรือเบอร์โทร ทุกใบวิ่งแบบไม่เข้ารหัส และใครที่ subscribe หัวข้อเดียวกันก็เห็น
  • client_id ชนกันได้กับทุกคนบนอินเทอร์เน็ต ไม่ใช่แค่เพื่อนในห้อง ชนเมื่อไร broker เตะตัวเก่าออก (วัดแล้วว่าเป็นแบบนี้ทุกตัว) ไฟล์จึงตั้ง DEVICE_ID = "bento-aiot-" + TEAM ยาว 17 ตัวอักษร (เฟิร์มแวร์ตัดที่ 31) ส่วนหน้าเว็บสุ่ม web- ตามด้วยเลขสุ่มทุกครั้ง จึงไม่เตะบอร์ด

ใครก็ส่งเข้า bento-aiot/team03/cmd ได้ ไม่ใช่แค่หน้าเว็บของเรา — นี่คือเหตุผลที่ไฟล์ 06 ไม่เชื่อคนส่งเลยสักบรรทัด และคือเหตุผลที่คาบ 10 กับ 11 ต้องมีเรื่องสิทธิ์และการเข้ารหัส

ไฟล์ 05 · examples/s02/05_value_leaves_the_board.py↗ — บันไดสามขั้นที่ห้ามสลับ

1 · WiFi ต้องให้ IP ก่อน ไม่มีเลขที่อยู่ = ไปต่อไม่ได้เลย 2 · แนะนำตัวกับ broker mqtt.connect(BROKER, port=1883) 3 · ส่งของจริงออกไป mqtt.publish(TOPIC, body) ขั้นที่ยังไม่ผ่าน ต้องเห็นบนจอว่ายังไม่ผ่าน ไม่ใช่ค้างเงียบ
    linked = mqtt.connect(BROKER, port=1883, client_id=DEVICE_ID, keepalive=60)
...
    payload = {"id": TEAM,          # คีย์สั้นตัวเล็ก มี id กับ n เสมอ
               "n": i,
               "knob": knob,
               "az": az,
               "uptime_s": time.ticks_ms() // 1000}
    body = json.dumps(payload)      # dict ของเรา -> ข้อความที่ทุกภาษาอ่านออก
...
    try:
        ok = mqtt.publish(TOPIC, body)
    except OSError:                 # สายหลุดแล้ว publish ไม่คืน False มันโยน OSError
        ...

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

json.dumps() แปลง dict ของเราเป็นข้อความ ทำให้เครื่องปลายทางจะเขียนด้วยภาษาอะไรก็อ่านออก — นี่คือเหตุผลที่ IoT ทั้งโลกส่ง JSON กันไปมา ไม่ใช่ส่งโครงสร้างข้อมูลของภาษาใดภาษาหนึ่ง

mqtt.connect() ใช้ชื่อ username= ไม่ใช่ user= ใส่ผิดได้ TypeError ทันที · และ client_id ที่ซ้ำกับใครก็ตามบน broker เดียวกัน ทำให้ผลัดกันเตะกันออกโดยไม่มีข้อความเตือน · บน broker สาธารณะ "ใครก็ตาม" คือทั้งอินเทอร์เน็ต

ไฟล์ 05 (ต่อ) — ค่าที่ส่งออกไปต้องเป็นค่าจริง ไม่ใช่ตัวเลขสุ่ม

knob = -1                           # -1 = รอบนี้อ่านลูกบิดไม่ได้
az = -99.0                          # -99 = อ่านค่าเอียงไม่ได้
try:
    s = sensors.snapshot()          # ของเดิมจาก examples/s01/12_every_sense_at_once.py
    if "pot" in s:                  # ถามด้วย in ก่อนหยิบ รอบที่ไม่มีคีย์จะไม่ตาย
        knob = int(s["pot"]["percent"])
    if "bmi270" in s:
        az = round(s["bmi270"]["az"], 2)   # m/s^2 วางราบราว 9.8
except OSError:
    pass                            # อ่านไม่ได้รอบนี้ ไม่ใช่เหตุให้หยุดส่ง ค่าข้างบนบอกให้รู้แล้ว
ใบที่ 7 ที่เพิ่งออกจากบอร์ดไปเมื่อกี้ {"id": "team03", "n": 7, "knob": 63, "az": 9.79, "uptime_s": 41} หมุนลูกบิดหรือเอียงบอร์ดที่โต๊ะนี้ แล้วดูเลขบนหน้าเว็บของทีมขยับตาม ค่าที่พิสูจน์ได้ด้วยมือตัวเอง ต่างจากตัวเลขสุ่มที่ใครก็เถียงได้

ไฟล์นี้ส่งค่าจริงสองค่า ลูกบิด (Eva Kit: ลูกบิดสีน้ำเงิน · Dev Kit: VR1 — sensors.pot อ่านตัวนี้ตัวเดียว) กับค่าเอียงแกน z จาก BMI270 ทั้งสองบอร์ดมีทั้งคู่ คนหนึ่งหมุน อีกคนเอียง คนที่นั่งดูอีกฝั่งจึงพิสูจน์ได้ด้วยมือตัวเองว่าขยับที่นี่แล้วเลขที่โน่นขยับตาม ไม่ใช่ตัวเลขสุ่มที่ใครก็เถียงได้

mqtt ของบอร์ดไม่เข้ารหัส และ broker วันนี้เป็นของสาธารณะ ห้ามส่งของที่เป็นความลับ (คาบ 11 ค่อยย้ายไปทางเข้ารหัส) · เฟิร์มแวร์ส่งแบบ retain ไม่ได้ หน้าเว็บที่เปิดช้าเห็นแค่ใบถัดไป ไฟล์จึงส่ง 60 ใบ ใบละ 2 วินาที

ตาคุณ อยู่ท้ายไฟล์ เปิดหน้าเว็บของทีม หมุนลูกบิดหรือเอียงบอร์ด จดว่าเห็นใบแรกที่ n เท่าไร แล้วลองตั้ง TEAM ชนกับทีมข้าง ๆ ชั่วคราว ดูว่าใครถูกเตะออก

เปิดหน้าเว็บอ่านค่าของทีม — examples/web/my_first_reader.html

1 · เปิดลิงก์ของหลักสูตร ในเบราว์เซอร์ใดก็ได้ 2 · ต่อท้าย ?team= เลขเดียวกับ TEAM ในไฟล์ 05 06 3 · กด Enter ไม่ต้องบันทึกไฟล์ 4 · สถานะ ต่อแล้ว รอใบถัดไปจากบอร์ด เปิดได้ทุกเครื่องที่มีเบราว์เซอร์ รวมถึงมือถือ ไม่ต้องติดตั้งอะไร

ลิงก์ของหน้าอ่านค่า เปลี่ยน team05 ท้ายลิงก์เป็นเลขทีมของคุณ

https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/my_first_reader.html?team=team05

// ถ้าดาวน์โหลดไฟล์ไปเปิดเอง แก้บรรทัดนี้แทนการต่อท้ายลิงก์
let TEAM = "teamXX";          // ทีมของคุณ ตามที่อาจารย์แจก เช่น "team05"
  • หน้านี้ฟัง bento-aiot/<TEAM>/# แล้ววาดหนึ่งกล่องต่อหนึ่งคีย์ของ JSON (knob az n ...) และมีปุ่มส่งเสียงกับเปิดปิดไฟ LED 0 ที่ส่งเข้า .../cmd ให้ไฟล์ 06
  • เปิดหน้านี้หลังบอร์ดส่งไปแล้ว จะว่างจนกว่าใบถัดไปมาถึง เพราะเฟิร์มแวร์ส่งแบบ retain ไม่ได้ broker จึงไม่เก็บใบล่าสุดไว้ให้ ไฟล์ 05 ส่งทุก 2 วินาทีนานสองนาทีด้วยเหตุนี้
  • ผู้สอนฉายหน้ารวม https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/mqtt_dashboard.html ซึ่งฟัง bento-aiot/+/telemetry กับ bento-aiot/+/event เห็นทุกทีมเป็นการ์ด และส่งคำสั่งเข้า bento-aiot/<ทีม>/cmd ได้
  • Emulator ต่อ broker จริงได้แล้ว (อัปเดต 25 ก.ย. 08:41) รันไฟล์ 05 06 บน Emulator ใน ide.tesaiot.dev แล้วข้อความขึ้นบนหน้าเว็บของทีมจริง และคำสั่งจากหน้าเว็บวิ่งกลับมาที่ Emulator ได้ (ทดสอบแล้วทั้งสองทาง) · Emulator เติม -emu ท้าย client_id เอง จึงไม่เตะบอร์ดจริงของทีม · ถ้าต่อไม่ได้ภายใน 5 วินาที มันถอยไปใช้ broker จำลองในตัวและบอกใน Console · กด Run ใหม่หรือรีโหลดหน้าก่อน ถ้าเปิด IDE ค้างไว้ตั้งแต่ก่อน 08:41

ไม่มีไฟล์หน้าเว็บของเรา ใช้หน้าทดลองของผู้ให้บริการแทนได้ https://www.hivemq.com/demos/websocket-client/ ตั้ง host broker.hivemq.com port 8884 เปิด SSL แล้ว subscribe bento-aiot/<ทีมของคุณ>/#

ไฟล์ 06 · examples/s02/06_command_comes_back.py↗ — คำสั่งเดินทางกลับมา

if not mqtt.subscribe(TOPIC_CMD):   # ต้องมาหลัง connect() เสมอ และต้องทำซ้ำถ้าสายหลุดแล้วต่อใหม่
    stop_here("subscribe ไม่ผ่าน", "broker ไม่ยอมให้ฟังหัวข้อ " + TOPIC_CMD)
...
    msg = mqtt.get_message()        # ไม่บล็อก คืน None ทันทีเมื่อยังไม่มีอะไรมา
...
    if msg is not None:
        ...
        topic, raw = msg            # topic เป็น str ส่วน payload เป็น bytes
        ...
            cmd = json.loads(raw.decode())  # ต้อง .decode() ก่อนเสมอ
        ...
        action = cmd.get("cmd", "")
        ...
        elif action == "led":
            ...
                led = gpio.led(n)   # ของบนโต๊ะเราขยับ เพราะคนที่อยู่คนละที่พิมพ์มา
                if on:
                    led.on()
กล่องรับมีช่องเดียว ใบที่สองที่มาถึงก่อนเราหยิบใบแรก ทับใบแรกทิ้งไปเลย ไม่ได้ต่อคิว ลูปจึงห้ามหลับยาว ต้องถามซ้ำถี่ ๆ คนส่งพิมพ์มั่วได้เสมอ ของที่ไม่ใช่ JSON ต้องไม่ทำให้โปรแกรมตาย เลขดวงไฟที่บอร์ดนี้ไม่มี ต้องกันเอง ข้อความยาวเกินป้าย ต้องตัดก่อนเสมอ

ไฟล์ 05 ส่งออกอย่างเดียว ไฟล์นี้เติมทางกลับให้ครบวง คำสั่งที่บอร์ดรู้จักมีสาม beep ให้ร้อง led ให้ไฟติดหรือดับ และ say ให้ขึ้นข้อความบนจอ ทุกคำสั่งมาในรูป JSON เช่น {"cmd":"led","n":0} — n คือดัชนีดวงตาม gpio.board_info()["led_names"] ของบอร์ดนั้น ถ้าสั่งดวง 0 แล้วมองไม่เห็นบนบอร์ดของทีม (บน Dev Kit ดวง 0 คือ LED1 บนโมดูล) ให้ลอง n ของดวงที่ชื่อขึ้นต้นด้วย RGB_ จากช่อง LED ของหน้ารวม mqtt_dashboard.html เพราะหน้าอ่านค่าของทีมมีปุ่มแค่ดวง 0 · วันนี้คนส่งคือปุ่มบนหน้าเว็บ examples/web/my_first_reader.html ซึ่งส่งเข้า bento-aiot/<ทีม>/cmd

ครึ่งหนึ่งของไฟล์นี้คือ การไม่เชื่อคนส่ง เพราะบน broker สาธารณะ ใครที่รู้ชื่อหัวข้อก็ส่งเข้ามาได้ ข้อความจึงมาจากคนที่เราไม่รู้จักได้จริง เขาอาจพิมพ์ผิด อาจส่งของที่ไม่ใช่ JSON หรืออาจสั่งดวงไฟที่บอร์ดนี้ไม่มี ทั้งสามกรณีต้องจบลงที่ข้อความบนจอ ไม่ใช่ที่โปรแกรมตาย · ก่อนจบ ไฟล์นี้ปิดไฟทุกดวงเสมอ ไม่ทิ้งบอร์ดไว้ในสถานะที่คำสั่งสุดท้ายบังเอิญตั้งไว้ — เป็นนิสัยที่ระบบซึ่งต้องแขวนอยู่หน้างานหกเดือนขาดไม่ได้

นี่คือคำตอบของเกมต้นคาบ ทั้งห้องส่งพร้อมกัน แต่บอร์ดถามกล่องทุก 100 ms และกล่องมีช่องเดียว ใบที่มาถึงระหว่างนั้นทับกันเอง · ตาคุณ อยู่ท้ายไฟล์ ตั้ง POLL_MS = 3000 แล้วกดปุ่มบนหน้าเว็บสามครั้งรวดในวินาทีเดียว แล้วนับว่าตัวเลขบนจอขึ้นกี่ใบ เทียบกับสามใบที่ส่งไปจริง

05 คู่กับ 06 คือจุดขายของทั้งไลบรารี

สองไฟล์นี้ต่อกันแล้วได้วงจรที่ปิดครบ โต๊ะของเรา หมุนลูกบิด sensors.snapshot() หลอด LED ติด gpio.led(n).on() broker team03/telemetry team03/cmd สองหัวข้อ สองทิศทาง หน้าเว็บของทีม เห็นเลข knob ขยับ กดปุ่มส่งคำสั่ง {"cmd":"led","n":0} หกโมดูลทำงานพร้อมกันในเรื่องเดียว sensors gpio json mqtt wifi ui

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

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

อีกสิบคาบข้างหน้าไม่มีอะไรใหญ่กว่าภาพนี้ มีแต่การทำให้มันปลอดภัยขึ้น เร็วขึ้น และฉลาดขึ้น — โครงยังเป็นภาพเดิม

ไฟล์ 07 · examples/s02/07_platform_in_one_call.py↗ — อ่านนอกเวลา วันนี้ยังต่อไม่ได้

tesaiot.connect() TLS พอร์ต 8883 / 8884 broker.hivemq.com บอร์ดเข้าที่ 1883 ไม่เข้ารหัส คนละพอร์ต เข้ารหัสกับไม่เข้ารหัส วันนี้ไฟล์นี้อ่านคลังค่าตั้งอย่างเดียว ต่อจริงในคาบ 10 กับ 11
cfg = tesaiot.config()               # คลังค่าตั้งอยู่บนแฟลชของบอร์ด อ่านได้โดยไม่ต้องมีเน็ต
...
if PLATFORM_BROKER == "":            # วันนี้ยังว่าง: แสดงคลังค่าตั้งแล้วจบ ไม่เขียนแฟลช
    ...
    raise SystemExit
tesaiot.config_set("broker", PLATFORM_BROKER)   # รับข้อความทั้งสองอาร์กิวเมนต์
...
tesaiot.connect()                    # TLS พอร์ต 8883/8884 เสมอ ไม่อ่านคีย์ "port"
                                     # สั่งให้เริ่มต่อ แล้วคืนค่าทันที ไม่ได้รอให้ต่อเสร็จ
...
    if tesaiot.is_connected():       # จึงต้องวนถามเอง
        ...
    if waited >= WAIT_MS:            # และต้องมีกำหนดเวลาเลิกรอเสมอ
        break
...
    if tesaiot.publish(body):        # ไม่ต้องบอกหัวข้อ เฟิร์มแวร์ประกอบให้จาก device_id

ไฟล์นี้ต่อ broker.hivemq.com ไม่ได้ และไม่ได้ตั้งใจให้ต่อ tesaiot.connect() ต่อแบบเข้ารหัส TLS เสมอ ที่พอร์ต 8883 หรือ 8884 ตามโหมด และไม่อ่านคีย์ port ในคลังค่าตั้งเลย (mqtt_client_config.c:233-242 ในไลบรารีร่วม) ส่วน broker ของวันนี้รับบอร์ดที่ 1883 แบบไม่เข้ารหัส ไฟล์นี้จึงเป็นการอ่านนอกเวลาที่ชี้ไปคาบ 10 กับ 11 ถ้ารันวันนี้ มันแสดงคลังค่าตั้งแล้วจบ ไม่เขียนอะไรลงแฟลช

สิ่งที่มันสอนยังใช้ได้: ไฟล์ 05 กับ 06 ต้องพิมพ์ชื่อ broker ลงในโค้ดตรง ๆ ส่วนคลังค่าตั้งนี้ ตั้งครั้งเดียว เก็บบนแฟลชของบอร์ด ถอดไฟแล้วยังอยู่ โปรแกรมตัวถัดไปถาม config() เอาได้เลย

tesaiot.connect() ต่างจาก wifi.connect() ตรงที่มัน ไม่บล็อก ค่าที่คืนมาแปลว่า "รับคำสั่งไปแล้ว" ไม่ได้แปลว่า "ต่อได้แล้ว" ใครเขียน if tesaiot.connect(): แล้วส่งต่อทันทีจะได้ OSError เพราะสายยังไม่ขึ้น WAIT_MS คือกำหนดเวลาเลิกรอ ซึ่งแยกโปรแกรมที่ล้มเหลวอย่างสุภาพ ออกจากโปรแกรมที่ค้างจนคนดูต้องถอดไฟ

ตาคุณ อยู่ท้ายไฟล์ วันนี้จดค่า port กับ tls_mode ที่จอแสดง แล้วตอบว่าทำไม port ที่เห็นอาจไม่ใช่พอร์ตที่ connect() ใช้จริง · วันที่ได้ชื่อ broker ของแพลตฟอร์มในคาบ 10 ค่อยทำข้อถอดสาย USB แล้วดูว่าค่ายังอยู่ไหม

เทปผลตรวจ 24 รอบ — แต่ละช่องคือหนึ่งรอบของลูป รันซ้ำกี่รอบก็ได้ผลเดิม สะดุดรอบเดียว หลุดจริง 4 รอบ ต่ออยู่ แต่ไม่มีเลข IP ช่วงสีส้มคือช่วงที่กฎตั้งต้นมองไม่เห็น รายงานได้ 1 จากที่ควรได้ 2
NEED = 3             # ต้องเสียติดกันกี่รอบถึงจะรายงาน
WANT_REPORTS = 2     # เทปชุดนี้มีปัญหาจริงอยู่สองช่วง
...
# ----- เติมส่วนนี้เอง (งานของคุณ) -----
def is_usable(online, ip):
    ...                # docstring ในไฟล์บอกว่า online กับ ip คืออะไร และใบ้วิธีเทียบสตริงไว้แล้ว
    return online      # ตอนนี้มันดูแค่ครึ่งเดียวของโจทย์
# ----- จบส่วนที่ต้องเติม -----

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

โจทย์คือลิงก์ที่ใช้ได้จริงต้องครบสองอย่างพร้อมกัน ต่ออยู่ และ มีเลข IP ที่ใช้ได้ — ซึ่งคือกับดัก "0.0.0.0" จากไฟล์ 01 ที่กลับมาอีกครั้ง คราวนี้มาในรูปของโจทย์ และหลังจากรันไฟล์ 05 มาแล้ว ทุกคนจะรู้ว่าทำไมมันสำคัญ เพราะลิงก์ที่ไม่มี IP คือลิงก์ที่ publish() ไปไม่ถึงไหน

จอบนบอร์ดคือเฉลย แก้แล้วรันใหม่จนเลขสองตัวตรงกัน ไม่ต้องรอผู้สอน · ตาคุณ อยู่ท้ายไฟล์ ลองเปลี่ยน NEED เป็น 5 แล้วดูว่าเราพลาดอะไรไป

เจ็ดไฟล์เรียงแบบนี้เพราะอะไร

ความช่วยเหลือลดลงทีละขั้น จนขั้นสุดท้ายไม่มีเหลือ 01 · 04 · 02 เราทำให้ดู อ่านแล้วรันแล้วแก้ตามท้ายไฟล์ 05 · 06 ของจริงเต็มวง ส่งออกไป แล้วรับกลับมา 03 · 07 คุณตัดสินใจเอง เขียนกฎ และตั้งค่าที่บอร์ดจำ รูปแบบเดียวกับคาบที่แล้ว แค่คาบนี้ปลายทางไม่ได้จบอยู่บนจอบอร์ดแล้ว

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 135 นาที โดยสไลด์แทรกอยู่ระหว่างการรันแต่ละไฟล์

ลำดับ · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · 25 นาที examples/s02/01_wifi_first_connect.py↗ ต่อเน็ตให้ได้ จับเวลาทั้งรหัสถูกและรหัสผิด · รู้ว่าจอที่นิ่งไม่ได้แปลว่าแฮงก์ และ "0.0.0.0" หลอกเราได้
2 · 15 นาที examples/s02/04_scan_the_room.py↗ ให้บอร์ดฟังคลื่นทั้งห้อง · แยก "ไม่ได้ยินวงนั้นเลย" ออกจาก "ได้ยินแต่รหัสผิด" ได้ด้วยตัวเอง
3 · 25 นาที examples/s02/02_link_uptime.py↗ ประกอบสี่โมดูลเป็นจอเฝ้าลิงก์หนึ่งใบ แล้วเดินถือบอร์ดออกไปให้มันหลุดจริง
4 · 25 นาที examples/s02/05_value_leaves_the_board.py↗ ส่งค่าลูกบิดจริงออกจากบอร์ด · เห็นบันไดสามขั้นและรู้ว่าตัวเองไปหยุดที่ขั้นไหน
5 · 25 นาที examples/s02/06_command_comes_back.py↗ รับคำสั่งจากที่ไกลมาสั่งไฟบนบอร์ด · เข้าใจว่ากล่องรับมีช่องเดียวแปลว่าอะไร
6 · 20 นาที examples/s02/03_your_link_rule.py↗ เขียนกฎ is_usable() เองจนเลขสองตัวบนจอตรงกัน · "ต่ออยู่" ไม่ได้แปลว่า "ส่งข้อมูลออกได้"
อ่านนอกเวลา examples/s02/07_platform_in_one_call.py↗ คลังค่าตั้งบนแฟลช · วันนี้อ่านอย่างเดียว tesaiot.connect() เป็น TLS เสมอ ต่อจริงคาบ 10-11

ทุกไฟล์จบด้วยบล็อก ตาคุณ — อ่านแล้วรันแล้วยังไม่จบ ต้องแก้แล้วรันซ้ำถึงจะจบ

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

30% ของคาบนี้ไม่ใช่การเขียนโค้ดคุมวิทยุ — มันไม่มี API แบบนั้นให้เรียก 70% · เฟิร์มแวร์ทำให้แล้ว 30% · งานของเรา คุมวิทยุ · จับมือกับเราเตอร์ · ขอเลข IP ต่อ TCP · พูดโปรโตคอล MQTT · ซิงก์นาฬิกา ส่งอะไรออกไป รับคำสั่งอะไร และนิยามว่า "ใช้ได้" แปลว่าอะไร

สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
คุมชิปวิทยุ · จับมือกับเราเตอร์ · ขอและต่ออายุเลข IP · ต่อ TCP ไปหา broker · พูดโปรโตคอล MQTT ทั้ง publish และ subscribe · ลองต่อใหม่เองเมื่อหลุด · ซิงก์นาฬิกาจากอินเทอร์เน็ต · วาดไอคอนบนแถบบนสุด

สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่าจะส่ง ค่าอะไร ออกไป และในรูปแบบไหน · ยอมรับคำสั่งอะไรบ้าง และจะทำอย่างไรกับคำสั่งที่พิมพ์มาผิด · นิยามว่า "ลิงก์ใช้ได้" แปลว่าอะไรสำหรับงานของเรา · แล้วรายงานออกมาให้คนที่เดินผ่านหน้าจออ่านรู้เรื่องภายในสายตาเดียว

ภาพถ่ายจอจริงของบอร์ด Eva Kit หน้า Home หลังต่อเน็ตสำเร็จ — บันทึกโดยผู้สอน · สองอย่างสุดท้ายในรายการ 70% อยู่ในภาพนี้ครบ คือนาฬิกา "Fri 14 Aug 15:39" กับไอคอน WiFi มุมขวาบน ทั้งคู่เฟิร์มแวร์วาดเอง ไม่มีโค้ดของเราสักบรรทัดไปสั่ง · คาบที่แล้วสองช่องนั้นว่างเปล่า และเราบอกไว้ว่า "คาบหน้าต่อแล้วจะตรงเอง" — นี่คือหน้าตาของคำนั้นตอนเป็นจริง

สี่เรื่องนั้นไม่มีคำตอบสำเร็จรูปในเอกสารของผู้ผลิต เพราะมันขึ้นกับว่าเราจะเอาระบบนี้ไปทำอะไร

mqtt.publish() ใครก็เรียกได้ แต่การตอบว่า "ตกลงเราควรส่งอะไรออกไปให้คนอื่นเห็น" คือส่วนที่ไม่มีใครทำแทนเราได้

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

ภาพถ่ายจอจริงของบอร์ด Eva Kit หน้า BENTO Playground หลังต่อเน็ตสำเร็จ — แถบบนสุดขวาสุดมีนาฬิกา "Fri 14 Aug 11:04" กับไอคอน WiFi สีเขียวแล้ว ต่างจากคาบที่แล้วที่สองช่องนั้นว่างเปล่า · มุมขวาล่างคือปุ่มเปิดลิ้นชัก Console
  1. บนจอบอร์ด แตะการ์ด BENTO Playground เปิดค้างไว้ก่อนส่งโค้ดเสมอ
  2. บนคอม เปิด BENTO IDE เชื่อมต่อบอร์ด
  3. เปิดไฟล์จาก examples/s02/ แก้ WIFI_SSID กับ WIFI_PASS ให้ตรงกับ Hotspot มือถือของทีม และในไฟล์ 05 กับ 06 แก้ TEAM จาก teamXX เป็นเลขทีมที่ผู้สอนแจก ถ้าลืม ไฟล์จะหยุดตั้งแต่ขั้นแรกพร้อมบอกบนจอ เพราะชื่อทีมซ้ำจะเตะบอร์ดของทีมอื่นหลุด (BROKER ตั้งไว้แล้ว ไม่ต้องแก้) แล้วกด Program to Device
  4. หันไปมองจอบอร์ด แล้วรอ อย่ากดรีเซ็ตระหว่างที่จอนิ่ง
  5. แตะปุ่มไอคอนสีเขียวมุมขวาล่างเพื่อเปิดลิ้นชัก Console — ประวัติของทุกไฟล์อยู่ในนั้น
  6. ต่อสำเร็จแล้ว มองแถบบนสุด: ไอคอน WiFi ติดคือ "ต่อเครือข่ายได้" ส่วนนาฬิกาที่โผล่ตามมาคือ "ออกอินเทอร์เน็ตได้จริง" สองอย่างนี้ตอบคนละคำถาม

ภาพถ่ายจอจริงของบอร์ด Eva Kit เฉพาะแถบบนสุด ตัดมาจากภาพเดียวกันกับข้างบนเพื่อให้อ่านออก — บันทึกโดยผู้สอน · นี่คือสิ่งที่ข้อ 6 ให้มอง ขวาสุดคือนาฬิกากับไอคอน WiFi สีเขียว ซ้ายสุดคือบรรทัดลิขสิทธิ์ที่อยู่ตรงนั้นตลอดเวลา ไม่เกี่ยวกับสถานะเครือข่าย

พื้นที่วาดของเรายังเป็น 792 x 398 พิกเซล เท่าเดิม งบ widget ยังเป็น งบของคอร์ส 32 ตัว (เพดานเฟิร์มแวร์ 64) และมุมขวาล่างราว 100x58 ยังเป็นของปุ่ม Console ที่เฟิร์มแวร์จองไว้

ข้อควรระวังเรื่องเครือข่ายของห้อง: เครือข่ายที่ต้อง login หรือกดยอมรับเงื่อนไขบนหน้าเว็บก่อน เช่น WiFi ของคณะ จะทำให้ connect() คืน True และได้เลข IP ตามปกติ แต่นาฬิกาไม่ขึ้น และ mqtt.connect() จะไปไม่ถึง broker — นั่นคือกรณี "ติดข้อแรก ไม่ผ่านข้อสอง" · เน็ตที่กันพอร์ต 1883 ขาออกก็ไปไม่ถึงเช่นกัน

ข้อมูลไหลไปทางไหน — วิทยุอยู่ฝั่งเดียวกับโค้ดเรา

ผลของ wifi.* และ mqtt.* เดินทางไปขึ้นจอด้วยเส้นทาง IPC เส้นเดิมจากคาบที่แล้ว CM33 โค้ด Python ของเรา และตัวคุมวิทยุ ชิปวิทยุ Eva: CYW55513IUBG WiFi + BT ใช้เสาร่วมกัน เราเตอร์ แจกเลข IP broker พอร์ต 1883 CM55 วาดจอ ไม่ยุ่งกับเน็ต เน็ตทั้งเส้นอยู่ฝั่ง CM33 คอร์ที่รันโค้ดของเรา จึงบล็อกได้ทั้งโปรแกรม และจึงต้องเคาะ ui.poll() ให้จอทันก่อนเข้าบรรทัดนั้น

คาบที่แล้วเราเห็นแล้วว่า CM33 รันโค้ดของเรา ส่วน CM55 วาดจอ วันนี้มีข้อเท็จจริงเพิ่มมาข้อเดียว: ทั้งวิทยุ WiFi และงาน MQTT อยู่ฝั่ง CM33 เหมือนกัน

นั่นอธิบายทุกอย่างที่เราเจอวันนี้ — wifi.connect() บล็อกได้ทั้งโปรแกรม เพราะมันบล็อกคอร์เดียวกับที่รัน Python ของเรา และระหว่างที่มันบล็อก ไม่มีใครเหลือไปเคาะกล่องจดหมายให้ CM55 วาดอะไรใหม่ ส่วน mqtt.get_message() ตรงกันข้าม มันไม่บล็อกเลย จึงต้องเป็นเราที่วนถามเอง

ถ้าเข้าใจสไลด์นี้ จะไม่มีวันเขียนป้ายบอกสถานะไว้หลังบรรทัดที่บล็อกอีกเลย

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

ทีมต่อบอร์ดเข้าเครือข่ายด้วย wifi.connect() แล้วรายงานหมายเลข IP ขึ้นจอสำเร็จ

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

  • [ ] แก้ WIFI_SSID และ WIFI_PASS ให้ตรงกับ Hotspot มือถือของทีมได้เอง
  • [ ] examples/s02/01_wifi_first_connect.py↗ ขึ้นเลข IP ที่ ไม่ใช่ 0.0.0.0 บนจอ
  • [ ] จดเวลาที่ connect() ใช้ทั้งกรณีรหัสถูกและรหัสผิด พร้อมอธิบายว่าทำไมต่างกัน
  • [ ] examples/s02/04_scan_the_room.py↗ ขึ้นรายชื่อวงในห้อง และชี้ได้ว่าเลข status() กับเลขจาก scan() ต่างกันตรงไหน
  • [ ] examples/s02/02_link_uptime.py↗ เดินครบ 30 วินาที ทำให้ลิงก์หลุดจริงหนึ่งครั้ง แล้วชี้ได้ว่าหลุดวินาทีที่เท่าไร
  • [ ] examples/s02/05_value_leaves_the_board.py↗ รันแล้วเห็นค่าของทีมตัวเองบนหน้าเว็บ examples/web/my_first_reader.html — หรือถ้าเน็ตของห้องกันไว้ อ่านจากจอได้ว่าสายขาดที่ขั้นไหนและเพราะอะไร
  • [ ] examples/s02/06_command_comes_back.py↗ รันแล้วบอกได้ว่าบอร์ดกำลังฟังหัวข้ออะไร และคำสั่งที่มันรู้จักมีอะไรบ้าง
  • [ ] examples/s02/03_your_link_rule.py↗ รายงานครบ 2 ครั้ง (เลขสองตัวบนจอตรงกัน)
  • [ ] ถ่ายรูปหน้าจอบอร์ดแนบใน worksheet

ข้อที่ทีมส่วนใหญ่ตกคือข้อที่ห้า เพราะต้องออกแรงทำให้มันพังจริง · ข้อ 05 กับ 06 ผ่านเมื่อเห็นค่าของทีมบนหน้าเว็บ หรือถ้าเน็ตของมหาวิทยาลัยกันพอร์ตไว้ทั้งห้อง ทีมชี้จากจอได้ว่าสายขาดที่ขั้นไหน

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

อาการ สาเหตุที่แท้จริง วิธีแก้
จอนิ่งค้างนาน คิดว่าบอร์ดแฮงก์ wifi.connect() กำลังทำงาน มันบล็อกได้ถึงราว 85 วินาที รอให้จบ อย่ากดรีเซ็ต และคราวหน้าใส่ป้าย + ui.poll() ไว้ก่อนบรรทัดนั้น
จอว่างเปล่าตลอดช่วงที่รอ ทั้งที่เขียนป้ายไว้แล้ว ลืมเคาะ ui.poll() ก่อนเข้าบรรทัดที่บล็อก ย้าย ui.poll() ขึ้นมาไว้ก่อน wifi.connect() และก่อน wifi.scan()
โค้ดบอกว่าได้ IP แล้ว แต่ส่งอะไรก็ไม่ออก เขียน if wifi.ip(): — "0.0.0.0" เป็นสตริงที่ไม่ว่าง จึงถือว่าจริง เทียบตรง ๆ if wifi.ip() == "0.0.0.0":
connect() คืน False ทุกครั้ง SSID หรือรหัสผ่านผิด หรือบอร์ดไม่ได้ยินวงนั้นเลย เปิด examples/s02/04_scan_the_room.py↗ ก่อน แล้วดูว่าชื่อวงโผล่ในรายการไหม
กราฟความแรงสัญญาณเป็นเส้นศูนย์ตลอด หยิบ wifi.status()["rssi"] มาใช้ ซึ่งเป็นค่าตายตัว ความแรงจริงมาจาก wifi.scan() เท่านั้น
mqtt.connect() ได้ TypeError ใส่ชื่ออาร์กิวเมนต์เป็น user= ชื่อจริงคือ username=
ส่งไปได้สองสามใบแล้วโปรแกรมตาย publish() ตอนสายหลุดไม่คืน False มันโยน OSError ครอบด้วย try / except OSError ทุกครั้ง ไม่ใช่เช็กแค่ค่าที่คืน
ยิงคำสั่งมาสามใบ แต่บอร์ดนับได้ใบเดียว กล่องรับของ get_message() มีช่องเดียว ใบใหม่ทับใบเก่า ลด POLL_MS ให้ถามถี่ขึ้น และห้ามหลับยาวในลูป
สองบอร์ดผลัดกันหลุดทั้งคาบ client_id ซ้ำกัน broker เตะตัวเก่าออกทุกครั้งที่ตัวใหม่เข้ามา บน broker สาธารณะชนได้กับทุกคนในโลก ใช้ TEAM ที่ผู้สอนแจก ไม่ซ้ำกับทีมอื่น และคง "bento-aiot-" ไว้หน้า DEVICE_ID
บอร์ดส่งครบแต่หน้าเว็บว่าง TEAM ในหน้าเว็บกับในไฟล์ไม่ตรงกัน หรือเปิดหน้าเว็บหลังบอร์ดส่งจบแล้ว (เฟิร์มแวร์ส่ง retain ไม่ได้) ตรวจ TEAM สองที่ให้ตรงกัน แล้วรันไฟล์ 05 ใหม่ขณะหน้าเว็บเปิดอยู่
หน้าเว็บขึ้นว่าผิดพลาดหรือต่อไม่ได้ เน็ตกันพอร์ต 8884 ของเบราว์เซอร์ ลองตัวสำรองที่ผู้สอนประกาศ wss://test.mosquitto.org:8081/mqtt หรือต่อฮอตสปอตมือถือ
ตั้ง tesaiot.config_set("port", "1883") แล้วไม่มีผล tesaiot.connect() เลือกพอร์ตจากโหมด TLS เอง (8883/8884) ไม่อ่านคีย์ port ใช้โมดูล mqtt กับ broker 1883 ส่วน tesaiot รอคาบ 10 กับ 11
seg.value(4218) ขึ้น 4218 แต่ไม่ได้ 4218.0 .value() ส่งได้แต่จำนวนเต็ม ใช้ seg.text("4218.0") เมื่อต้องการทศนิยม
ข้อความไทยยาว ๆ ขาดหายท้ายบรรทัด เกินเพดาน — lcd.print() 127 ไบต์ ป้าย ui 126 ไบต์ ไทยตัวละ 3 ไบต์ แบ่งเป็นสองบรรทัด หรือใช้ท่าของ examples/s01/06_safe_print.py↗

ครึ่งหนึ่งของตารางนี้ ไม่มี error ให้จับสักตัว โปรแกรมเดินผ่านไปเงียบ ๆ แล้วรายงานสิ่งที่ไม่จริง นั่นคือประเภทของบั๊กที่แพงที่สุดในงานเครือข่าย

ลงมือทำ — งานของทีมในคาบนี้

ขั้นที่ 1 · ขึ้นวงให้ได้ ไฟล์ 01 · 04 ขั้นที่ 2 · เฝ้าให้เห็นหลุด ไฟล์ 02 · เดินให้สุดห้อง ขั้นที่ 3 · ออกไปกลับมา ไฟล์ 05 · 06 ขั้นที่ 4 · นิยามเอง ไฟล์ 03 · จนสองเลขตรงกัน

คาบนี้ไม่มีไฟล์ฝึกแยกต่างหาก งานของทีมคือไฟล์ตัวอย่างของคาบนี้เอง ทำตามลำดับในตารางแล้วทำบล็อก "ตาคุณ" ท้ายไฟล์ให้ครบทุกไฟล์

ทำสี่ขั้น: หนึ่ง ขึ้นวงให้ได้และรู้ว่าบอร์ดได้ยินอะไรบ้าง สอง เฝ้าลิงก์แล้วออกแรงทำให้มันหลุดจริง สาม ส่งค่าออกไปแล้วรับคำสั่งกลับมา สี่ เขียนกฎ is_usable() ของทีมเองจนบอร์ดบอกว่าผ่าน

ทีมที่เสร็จก่อน ลองรัน 05 กับ 06 พร้อมกันสองบอร์ดในทีมข้าง ๆ โดยให้บอร์ดหนึ่งส่งค่าลูกบิด แล้วอีกบอร์ดหนึ่งเป็นคนสั่งไฟกลับมา สองทีมจะได้เห็นว่าระบบที่มีอุปกรณ์มากกว่าหนึ่งตัวหน้าตาเป็นอย่างไร

อย่าแก้หลายไฟล์พร้อมกันแล้วค่อยรันทีเดียว — ไฟล์ละครั้ง รันครั้ง จะรู้ทันทีว่าพังที่ไหน

ส่งต่อข้อความรอบห้อง — เวลานี้เป็นของทั้งห้อง

ผู้สอน team01 team02 ... team19 team00 หยุดนาฬิกา ทุกทอดต้องผ่านบอร์ดจริงของทีม หน้าเว็บรวมไม่เห็นหัวข้อ cmd จึงลัดทางไม่ได้

ทุกบอร์ดรัน examples/s02/06_command_comes_back.py↗ ด้วย TEAM ของตัวเอง ไฟล์ 06 ฟังนาน 15 นาที (LISTEN_MS = 900000) พอสำหรับสองรอบ ถ้ารันค้างไว้นานกว่านั้นก่อนเกม ให้กดรันใหม่ก่อนเริ่ม

  1. ผู้สอนส่งคำลับสั้น ๆ เป็น say ไปที่ team01 จากหน้ารวม https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/mqtt_dashboard.html
  2. ทีมที่เห็นคำบนจอบอร์ดตัวเอง ให้ศูนย์ควบคุมส่งคำเดิมไปทีมถัดไปจากหน้าเดียวกัน
  3. ทีมสุดท้ายส่งกลับไป team00 บนโต๊ะผู้สอน นาฬิกาหยุดเมื่อคำขึ้นจอหน้าห้อง · รอบสองแข่งกับเวลารอบแรกของห้องเราเอง
คนนั่งบอร์ด ศูนย์ควบคุม คนจด คนแก้
ดูจอ อ่านคำออกเสียง พิมพ์แล้วส่งต่อ เวลาของห้องทั้งสองรอบ คำไม่ขึ้น ดูลิ้นชัก Console ว่ามาถึงไหม

ถ้ามีคำที่ไม่มีใครในห้องส่งโผล่ขึ้นมา นั่นคือ broker สาธารณะทำงานตามที่มันเป็น ใครก็เขียนเข้ามาได้ · ห้องที่มีไม่ถึงสิบเก้าทีม ทีมสุดท้ายคือทีมที่เลขสูงสุด

เชื่อมโยงรากฐาน — วันนี้เราแตะอะไรไปบ้าง

ฝั่งเครือข่าย เข้าร่วมวง · ขอเลขที่อยู่ ส่งขึ้นหัวข้อ · ขอฟังหัวข้อ ฝั่ง Python dict · JSON · bytes กับ str try/except · เทียบสตริง ฝั่งออกแบบระบบ ล้มเหลวแล้วต้องบอกว่าที่ขั้นไหน ไม่เชื่อข้อมูลที่คนอื่นส่งมา

ฝั่งเครือข่าย
การเข้าร่วมเครือข่ายเป็นกระบวนการหลายจังหวะ ไม่ใช่คำสั่งเดียวจบ · การได้เลขที่อยู่เป็นคนละขั้นกับการเข้าร่วมได้ · การส่งข้อความผ่านคนกลางที่เรียกว่า broker ทำให้ผู้ส่งกับผู้รับไม่ต้องรู้จักกัน · ความล้มเหลวใช้เวลามากกว่าความสำเร็จเสมอ เพราะมีการลองใหม่ซ่อนอยู่

ฝั่ง Python และวิทยาการคอมพิวเตอร์
dict กับการแปลงเป็น JSON · ความต่างระหว่าง bytes กับ str และเหตุที่ต้อง .decode() · try/except กับความล้มเหลวที่คาดไว้แล้ว · การเทียบสตริงตรง ๆ แทนการพึ่ง truthiness · การจับเวลาคร่อมคำสั่งที่เราควบคุมไม่ได้

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

ข้อสุดท้ายคือของที่ทีมทดสอบซอฟต์แวร์ทั่วโลกใช้ทุกวัน และวันนี้น้อง ๆ ได้ใช้มันไปแล้วโดยไม่รู้ตัว

งานทำเอง 30% + สรุปคาบ

ขึ้นวง เฝ้าเป็น ส่งออก รับกลับ คาบหน้า: สั่งฮาร์ดแวร์เองเป็นครั้งแรก หลอด LED บนบอร์ด กับปุ่มผู้ใช้จริง

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

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

คาบถัดไปเราจะให้ปุ่มกับไฟขึ้น broker ด้วย ชื่อชุดเดิมทั้งหมด broker.hivemq.com · TEAM · bento-aiot/<ทีม>/... ไฟล์ examples/s03/07_button_to_broker.py↗ ส่งการกดปุ่มเป็น event และสถานะไฟใน telemetry แล้วรับ cmd จากหน้าเว็บเดิมมาเปิดปิดไฟ เก็บหน้าเว็บของทีมไว้ ไม่ต้องปิด

คาบหน้า: สองคาบแรกเราเล่นของที่ไลบรารีมีให้ครบแล้ว ตั้งแต่คาบ 3 เราจะ สร้างมันขึ้นมาเองทีละชิ้น เริ่มที่หลอด LED ทุกดวงบนบอร์ด (gpio.num_leds() บอกว่ากี่ดวง — Eva 3 · Dev Kit 5) กับปุ่มผู้ใช้จริงหนึ่งปุ่ม ผ่านโมดูล gpio ที่วันนี้เพิ่งได้เห็นผ่านตาในไฟล์ 06

โครงลูป "อ่าน → ตัดสิน → รายงาน" ที่ใช้มาสองคาบแล้ว จะเป็นโครงเดียวกับคาบหน้าเป๊ะ ๆ เปลี่ยนแค่ว่าปลายทางของการตัดสินคือหลอดไฟ ไม่ใช่ตัวหนังสือ

เชื่อมจุดให้เห็นภาพ — วันนี้อยู่ตรงไหนของเส้นทาง

วันนี้ · คาบ 1-2 เห็นของครบทั้งชุด แล้วพาบอร์ดถึงคนอื่น คาบ 3-5 สั่งฮาร์ดแวร์เอง ไฟ ปุ่ม จอสัมผัส คาบ 6-8 อ่านเซนเซอร์ วาดเป็นแดชบอร์ด คาบ 9-12 ส่งขึ้นแพลตฟอร์ม แล้วสร้างของจริง ทุกอย่างที่เห็นวันนี้ อีกสิบคาบข้างหน้าเราจะสร้างมันขึ้นมาเองทีละชิ้น

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

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

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

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

ข้อ 1 · จอหน้าประตู
ดัดแปลง examples/s02/02_link_uptime.py↗ ให้เหลือข้อมูลน้อยที่สุดเท่าที่จะยังตอบคำถาม "ตอนนี้บอร์ดออนไลน์อยู่ไหม" ได้จากระยะสามเมตร ตัดอะไรออกไปบ้าง แล้วทำไมถึงกล้าตัดสิ่งนั้น

ข้อ 2 · ส่งของทีมเอง
แก้ payload ใน examples/s02/05_value_leaves_the_board.py↗ ให้ส่งค่าที่ทีมเลือกเองเพิ่มอีกอย่างจาก sensors.snapshot() แล้วเปิดหน้าเว็บ examples/web/my_first_reader.html ดูว่าฝั่งรับเห็นคีย์ใหม่เป็นกล่องใหม่จริงไหม และคีย์นั้นควรตั้งชื่อว่าอะไรคนอื่นถึงจะเข้าใจ

ข้อ 3 · คำสั่งของทีมเอง
เพิ่มคำสั่งที่สี่ให้ examples/s02/06_command_comes_back.py↗ นอกจาก beep led say ที่มีอยู่ แล้วอธิบายว่าถ้าคนส่งพิมพ์คำสั่งนั้นมาผิดรูปแบบ โปรแกรมของทีมจะทำอย่างไร

ข้อ 4 · เทปของทีมเอง
จดผลของ wifi.is_connected() กับ wifi.ip() จากการเดินจริงของทีม ลงเป็น TAPE ชุดใหม่ใน examples/s02/03_your_link_rule.py↗ แล้วตั้ง WANT_REPORTS ให้ตรงกับที่ควรจะเป็น พร้อมอธิบายว่านับมาได้อย่างไร

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

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

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

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

04 ให้บอร์ดฟังคลื่นทั้งห้อง แล้วบอกว่าใครอยู่ตรงไหนบ้าง · 05 ค่าที่วัดได้บนโต๊ะนี้ ไปโผล่บนเครื่องคนอื่น (ภาพเก่า ในภาพ broker ยังเป็นเลขแลนและหัวข้อเป็น bento/eva-team03/... ไฟล์ปัจจุบันใช้ broker.hivemq.com กับ bento-aiot/team03/telemetry รอถ่ายใหม่) · 06 คนอื่นพิมพ์คำสั่งจากที่ไกล แล้วไฟบนโต๊ะเราติด (ภาพเก่า ไฟล์ปัจจุบันฟัง bento-aiot/team03/cmd รอถ่ายใหม่)
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

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

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

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

เอกสารของผู้ผลิต

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

  • IEEE 802.11 — ลำดับ Scanning / Authentication / Association ที่อธิบายว่าทำไม connect() ถึงกินเวลาหลายวินาที และทำไมความล้มเหลวถึงกินเวลามากกว่า
  • RFC 2131 (DHCP) — เหตุผลที่ลิงก์ขึ้นแล้วยังอาจไม่มีเลขที่อยู่ และที่มาของค่า "0.0.0.0"
  • MQTT Version 3.1.1 — OASIS Standard: หัวข้อ (topic) การ publish และ subscribe ที่ไฟล์ 05 กับ 06 ใช้ · คาบ 10 เราจะกลับมาที่เอกสารนี้อย่างละเอียด
  • RFC 8259 (JSON) — รูปแบบข้อความที่ json.dumps() กับ json.loads() ผลิตและอ่าน
  • HiveMQ public broker broker.hivemq.com (1883 TCP · 8884 wss) ไม่มีรหัสผ่าน https://www.hivemq.com/demos/websocket-client/ · หน้าเว็บ examples/web/ ใช้ MQTT.js 5.16.0
  • MicroPython documentation — https://docs.micropython.org/

งานวิจัยที่กำหนดรูปร่างของคาบนี้

  • Carroll, J. M. et al. (1987–1990) — งานทดลองแบบมีกลุ่มควบคุม: คู่มือที่พาผู้เรียนลงมือทำงานจริงตั้งแต่ต้น ใช้เวลาเรียนน้อยกว่าและทำงานสำเร็จได้มากกว่าคู่มือที่ให้อ่านทฤษฎีก่อน · นี่คือเหตุผลที่คาบนี้ให้รันไฟล์แรกตั้งแต่สไลด์ที่เจ็ด
  • Mayer, R. E. — หลักการ pre-training: บอกชื่อและหน้าที่ของชิ้นส่วนสั้น ๆ ก่อนลงมือ · สไลด์ "โมดูล wifi มีอะไรให้ใช้บ้าง" จึงมีแค่ห้าช่อง ไม่ใช่สารบัญของทั้งไลบรารี
  • ผลสำรวจนักพัฒนามืออาชีพ 80 คนเรื่องตัวอย่างโค้ด — ข้อร้องเรียนอันดับหนึ่งคือตัวอย่างไม่ช่วยให้คิดออกว่าจะประกอบชิ้นส่วนเข้าด้วยกันอย่างไร · นี่คือเหตุผลที่ไฟล์ 02 กับคู่ 05-06 ได้พื้นที่มากที่สุดในเด็คนี้

หมายเหตุเรื่องภาพ

ไดอะแกรม SVG ทุกภาพในเด็คนี้วาดขึ้นใหม่สำหรับหลักสูตรนี้ · ภาพลำดับการเข้าร่วมเครือข่ายมาจาก Wikimedia Commons ระบุที่มาไว้ใต้ภาพ · ภาพหน้าจอของ 04_scan_the_room.py เป็นภาพจริงจากการรันที่ความละเอียดเท่าจอของทั้งสองบอร์ด · ภาพหน้า Playground เป็นภาพถ่ายจากบอร์ด Eva Kit จริง บันทึกโดยผู้สอน

ข้อเท็จจริงเกี่ยวกับพฤติกรรมของโมดูล wifi mqtt tesaiot sensors gpio lcd และ ui ตรวจสอบจากซอร์สโค้ดของโปรเจกต์ KIT_PSE84_EVAL_EPC2-MicroPython-BentoClaw (Eva Kit) TESAIoT_KIT_PSE84_AI-Micropython-BentoClaw (Dev Kit) และ BENTO-TESAIoT-libraries โดยตรง

ตัวเลข 85 วินาทีคือเพดานที่วัดได้จริงบนบอร์ดตอนใส่รหัสผิด ไม่ใช่ค่าจากเอกสาร ทีมที่วัดได้ต่างจากนี้ บอกผู้สอนได้เลย

fit-css

VIDEO-SLOT: คลิป 60-90 วินาที ถ่ายสองจอพร้อมกันในเฟรมเดียว — ซ้ายคือจอบอร์ด (Eva Kit หรือ Dev Kit) ที่รัน examples/s02/05_value_leaves_the_board.py ขวาคือเบราว์เซอร์ที่เปิด examples/web/my_first_reader.html ของทีม team03 (ฟัง bento-aiot/team03/# ผ่าน broker.hivemq.com) → มือหมุนลูกบิดหรือเอียงบอร์ด แล้วให้เห็นเลข knob หรือ az บนหน้าเว็บขยับตามมือ → ตัดไปที่ examples/s02/06_command_comes_back.py กดปุ่มเปิดไฟบนหน้าเว็บ (ส่ง {"cmd":"led","n":<ดัชนีดวงที่มองเห็นบนบอร์ดนั้น>,"on":1}) แล้วถ่ายให้เห็นหลอด LED บนบอร์ดติดขึ้นมา

ผู้สอน: ครึ่งหนึ่งของโมดูลนี้เป็นเรื่องชิปนิรภัย OPTIGA — ทั้งสองบอร์ดคอมไพล์โมดูล `optiga` มาเหมือนกัน ที่ต่างคือสวิตช์บิลด์ `ENABLE_OPTIGA_CLM` ซึ่ง Dev Kit เปิดเป็นค่าปริยาย ส่วน Eva Kit ปิดไว้ และ Makefile ของ Eva เขียนกำกับว่ายังไม่ได้พิสูจน์บนฮาร์ดแวร์ ไฟล์นี้จึงไม่แตะฝั่งนั้นเลยสักตัวไม่ว่าบอร์ดไหน เรื่องความปลอดภัยของจริงเป็นงานของคาบ 11

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

VIDEO-SLOT: คลิป 20-30 วินาที ถ่ายจอบอร์ดตอนผ่าน MVP — เริ่มที่ examples/s02/01_wifi_first_connect.py ขึ้นเลข IP จริงตัวใหญ่ แล้วตัดไปที่ examples/s02/05_value_leaves_the_board.py ที่ป้ายสามขั้นไล่เป็นเขียวครบ และเลขใบที่ส่งเดินขึ้น ใช้เป็น "หน้าตาของคำว่าผ่าน" ให้ทุกทีมเทียบ

☰ สารบัญ