คาบ 11 — MQTTs (serverTLS)

จากพอร์ต 1883 ที่ใครก็อ่านได้ ไปพอร์ต 8884 ที่รู้ว่ากำลังคุยกับใคร

คาถาประจำคาบ: เราไม่ได้เข้ารหัสเพราะมันดูเป็นมืออาชีพ — เราเข้ารหัสเพราะถ้าไม่เข้ารหัส ทุกคนในเครือข่ายเดียวกันคืออุปกรณ์ของเรา

ดูของจริงก่อน — รหัสผ่านของทีมเราบนหน้าจอคนอื่น

คาบ 10 · พอร์ต 1883 — สิ่งที่คนดักฟังเห็น MQTT CONNECT username: team03 password: Kx7pQm2wLz9vRt4B {"accel_x":0.12,"pot":48.2} อ่านได้ทุกไบต์ ไม่ต้องถอดรหัสอะไรเลย วันนี้ · พอร์ต 8884 — สิ่งที่คนดักฟังเห็น TLS Application Data 17 03 03 00 4a 9c e1 b0 ... 3f a7 20 dd 61 8e 4c 05 ... ยาวเท่าเดิม เวลาเดิม แต่เนื้อในหายไป เห็นชื่อโฮสต์ปลายทางได้อย่างเดียว ความลับรั่วออกไปเรื่อย ๆ รั่วได้แค่ "มีคนคุยกัน" ไม่ใช่ "คุยว่าอะไร"

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

คาบที่แล้วเราทำให้ ส่งได้ วันนี้เราจะทำให้ คนอื่นแอบฟังไม่ได้ และรู้ด้วยว่ากำลังส่งให้ใคร

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

คำถาม คำตอบของคาบนี้ อยู่ช่วงไหน
Why คาบ 10 ส่งขึ้น broker ได้แล้ว ทำไมต้องย้ายพอร์ตอีก เพราะพอร์ต 1883 ไม่ได้ "ปลอดภัยน้อยกว่า" — มันไม่มีความปลอดภัยเลย รหัสผ่านของทีมเดินเป็นข้อความเปล่าให้ทุกคนในเครือข่ายเดียวกันอ่านได้ · และคาบนี้เป็นคาบแรกที่คำตอบที่ถูกที่สุดคือ "ได้ แต่แค่ระดับนี้" ครึ่งแรก · ใบรับรอง · ห่วงโซ่ความเชื่อถือ · SNI
What มีอะไรให้ใช้บ้าง import tesaiot ได้มา 28 ชื่อ — ใช้ได้จริง เก้าตัว เหมือนกันทั้ง Eva Kit และ Dev Kit · สิบหกตัวข้ามคอร์ไปหาชิปที่เฟิร์มแวร์ของคอร์จอไม่ได้เปิดไว้ จึงได้ OSError (เวลาที่เสียไปยังไม่ได้วัดจริง) · อีกสามตัวเป็นคนละเรื่องกัน และหนึ่งในนั้น (protected_update) ห้ามเรียก เพราะบน Dev Kit มันเขียนลงชิปจริง สไลด์ 28 ชื่อ + ตารางเต็มของเก้าตัวที่ใช้ได้
How ประกอบยังไงให้ใช้งานได้จริง ตั้งตัวตนด้วย config_set() → สั่ง connect() แล้ว วนรอจน is_connected() เป็นจริง เพราะฟังก์ชันคืนค่าไม่ได้แปลว่างานเสร็จ → publish() → เอาหลักฐานขึ้นจอ หกไฟล์ตัวอย่าง + ใบฝึก

ปลายทางที่จับต้องได้ — ข้อความชุดเดิมของคาบ 10 แต่เดินในท่อที่เข้ารหัส กราฟของ device_id ทีมเราขยับอยู่บน dashboard ของแพลตฟอร์ม และ Console ยืนยันว่าโหมดคือ server_tls -> 8884

คาบ 10 เราทำให้ ส่งได้ · คาบนี้เราทำให้คนอื่นแอบฟังไม่ได้ และรู้ด้วยว่ากำลังส่งให้ใคร

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

  1. อธิบายได้ว่า serverTLS ปกป้องอะไร (ช่องทาง + ตัวตนของเซิร์ฟเวอร์) และ ไม่ปกป้องอะไร (ตัวตนของอุปกรณ์)
  2. อ่านลำดับการจับมือ TLS ได้ระดับวิศวกรทำงาน — ใบรับรอง, CA, ห่วงโซ่ความเชื่อถือ, SNI
  3. ใช้ tesaiot.config_set() / connect() / is_connected() / publish() ได้ถูกต้องตามข้อจำกัดจริงของเฟิร์มแวร์
  4. เทียบสิ่งที่คนดักจับสัญญาณเห็นระหว่างพอร์ต 1883 กับ 8884 แล้วสรุปเป็นตารางของทีมเอง

ปลายทางของวันนี้: กราฟของ device_id ทีมเราขยับอยู่บน dashboard ของแพลตฟอร์ม โดยข้อมูลเดินผ่านช่องที่เข้ารหัสตลอดทาง

คาบนี้โค้ดสั้นที่สุดในตอนที่ 4 แต่เป็นคาบที่ ความเข้าใจผิดแพงที่สุด

ปลายทางของคาบนี้ — ข้อความเดิม แต่เดินในท่อที่เข้ารหัสแล้ว

หน้าจอจากการรัน examples/s11/06_secure_publish_loop.py↗ บน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ไม่ใช่ภาพวาด ไม่ใช่ mock-up และไม่ใช่ภาพถ่ายจากบอร์ด · หน้าจอของไฟล์เฉลย (ตารางตัวตน · ไฟสามดวงของการจับมือ · ปุ่มต่อใหม่/ตัดสาย · มาตรวัดเวลาจับมือ) ยังไม่ได้ถ่าย
  • การ์ดบนบอกโหมดกับพอร์ตที่ใช้จริง serverTLS -> 8884 · เลขใหญ่นับใบที่ส่งสำเร็จ · "สาย: ต่ออยู่" จะเปลี่ยนเป็นแดงทันทีที่หลุด
  • กราฟล่างคือจังหวะการส่ง (ms ระหว่างสองครั้ง) — เส้นราบแปลว่าลูปเดินสม่ำเสมอ ไม่มีรอบไหนค้าง
  • บรรทัดล่างสุดคือหลักฐานของคาบ: TLS สำเร็จใน 2738 ms — การจับมือนับเป็นวินาที และต้องเช็ก is_connected() ก่อนส่งทุกครั้ง

หน้าตาแทบไม่ต่างจากคาบ 10 — สิ่งที่เปลี่ยนคือเลขพอร์ต กับบรรทัด "TLS สำเร็จ" ที่ต้องรอเป็นวินาทีกว่าจะขึ้น

ทบทวนคาบ 10 — เราหยุดไว้ตรงไหน

mqtt.connect() ต่อ broker ได้ publish() ส่ง JSON ทุก 5 วิ subscribe() รับคำสั่งกลับมา TESAIoT CE ของทีมเราเอง วันนี้ tesaiot.* พอร์ต 8884 คาบ 10 ข้อมูลไหลครบสองทางแล้ว — แต่ไหลแบบเปลือย ต้องหยิบมาใช้ต่อวันนี้: JSON ส่งแบน ไม่ต้องห่อ · device_id ไม่เกิน 31 ตัวอักษร · ค่าต้องเป็นตัวเลขถึงขึ้นกราฟ สิ่งที่ยังไม่ได้ทำ: พิสูจน์ว่าปลายทางเป็นตัวจริง และปิดไม่ให้คนกลางอ่าน

โมดูลก็เปลี่ยนด้วย — คาบ 10 ใช้ mqtt.* ที่เราเลือกโฮสต์และพอร์ตเองได้ ส่วนวันนี้ใช้ tesaiot.* ซึ่งเป็นโมดูลคนละตัว ตั้งค่าคนละแบบ และ เลือกพอร์ตเองไม่ได้

ทุกข้อจำกัดของ mqtt ในคาบที่แล้วยังอยู่ครบ วันนี้แค่เพิ่มชั้นความปลอดภัยทับลงไป ไม่ได้ลบข้อจำกัดเดิม

ก่อนเริ่มคาบ — ทุกทีมต้องมีตัวตนของตัวเอง

ซ้าย — ภาพหน้าจอ: TESAIoT Community Edition v1.1.8 — เอกสารของ repo (Apache-2.0) · ขวา — ภาพ: Alexander Klink / Wikimedia Commons — CC BY 3.0 — การ์ด HSM (nCipher nShield) ตัวจริงที่เสียบอยู่ในเครื่องของ CA: ตัวตนที่ทีมกำลังจะได้รับ ถูกเซ็นด้วยกุญแจที่อยู่ในของแบบนี้ ไม่ใช่ไฟล์บนโน้ตบุ๊กของใคร

บอร์ดทุกตัวออกจากโรงงานมาพร้อม device_id ค่าเริ่มต้นตัวเดียวกันหมด และ MQTT บังคับว่า client id ต้องไม่ซ้ำกันบน broker เดียวกัน

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

ผู้สอน provision ตัวตนรายทีมให้ก่อนคาบ ทีมต้องได้ครบสี่ค่า: device_id · api_key · mqtt_pass · ชื่อโฮสต์ของ broker — กรอกลงกล่องข้อ 4.1 ในใบงานก่อนแตะโค้ด

id ซ้ำ = ลูปเตะกันเอง ทีม A ทีม B broker

ทีมที่ยังไม่ได้ค่าครบสี่ตัว ห้ามเริ่มท่าที่ 2 — ไม่ใช่กฎห้องเรียน แต่เป็นเพราะมันจะทำให้ทั้งห้องต่อไม่ติดพร้อมกัน

กุญแจ ลายเซ็น และแฮช — สามชิ้นที่ TLS ประกอบขึ้นมา

ซ้าย/กลาง — ภาพ: Davidgothberg / Wikimedia Commons — สาธารณสมบัติ · ขวา — ภาพ: Jorge Stolfi (ต่อยอดจากงานของ Helix84) / Wikimedia Commons — สาธารณสมบัติ

ซ้าย — กุญแจคู่ ล็อกด้วยกุญแจสาธารณะของ Alice แล้ว มีแต่กุญแจส่วนตัวของ Alice ที่เปิดได้ จึงส่งความลับให้คนที่ไม่เคยเจอกันได้ โดยไม่ต้องนัดรหัสกันก่อน

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

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

จำสามคำนี้ให้แม่น: เข้ารหัส = ปิดไม่ให้อ่าน · เซ็น = พิสูจน์ว่าใครเขียน · แฮช = จับได้ว่าถูกแก้ TLS ใช้ทั้งสามพร้อมกันเสมอ

ใบรับรองคืออะไรกันแน่ — เปิดดูข้างในทีละช่อง

ใบรับรอง X.509 ของเซิร์ฟเวอร์ Subject — ชื่อที่ใบนี้พูดถึง CN = broker.tesaiot.com Public Key — กุญแจสาธารณะของเซิร์ฟเวอร์ RSA 2048 หรือ EC P-256 Validity — ช่วงเวลาที่ใช้ได้ notBefore / notAfter Issuer + Signature — ใครรับรอง และลายเซ็น CN = TESAIoT Intermediate CA ลายเซ็นนี้คือทั้งหมดที่ทำให้เชื่อได้ ใบรับรองรับรองอะไร "กุญแจสาธารณะใบนี้ เป็นของชื่อโฮสต์นี้จริง" และมี CA ที่เรารู้จักเซ็นรับรองข้อความนั้นไว้ ใบรับรองเป็นข้อมูลสาธารณะ ไม่ใช่ความลับ ก๊อปได้ ใบรับรอง ไม่ ได้รับรองอะไร ไม่ได้บอกว่าเจ้าของเป็นคนดีหรือปลอดภัย ไม่ได้บอกว่าข้อมูลที่ส่งไปจะถูกเก็บอย่างดี พิสูจน์แค่ "ชื่อคู่กับกุญแจ" — เท่านั้นจริง ๆ

ภาพ: Winstonlee / Wikimedia Commons — CC BY-SA 4.0 — ใบรับรองจริงที่เปิดดูจากเบราว์เซอร์บนเครื่องผู้ใช้: ช่อง Issuer, Validity และ Subject ที่กล่องซ้ายมือกำลังไล่อธิบาย อยู่ครบในหน้าต่างนี้ และกรอบบนสุดคือห่วงโซ่ root → intermediate → leaf ของสไลด์ถัดไป

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

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

ห่วงโซ่ความเชื่อถือ — root → intermediate → leaf

ภาพ: Yuhkih / Wikimedia Commons — CC BY-SA 4.0

ภาพ: Rudolf.Achter / Wikimedia Commons — CC BY-SA 4.0

ซ้าย — ใบของเซิร์ฟเวอร์ (leaf) ถูกเซ็นโดย intermediate, intermediate ถูกเซ็นโดย root และ root เซ็นตัวเอง ห่วงโซ่จบตรงนั้นเสมอ เพราะ root คือสิ่งที่เรา "ตัดสินใจเชื่อ" ไว้ล่วงหน้า ไม่ใช่สิ่งที่พิสูจน์ได้ · ขวา — ถ้ามีกล่องกลางทางที่เรา (หรือผู้ดูแลเครือข่าย) ใส่ CA ของมันไว้ในเครื่อง มันจะออกใบรับรองชื่อเดียวกันได้ และเราจะเชื่อโดยไม่รู้ตัว — นั่นคือเหตุผลว่าทำไม รายชื่อ CA ที่เชื่อ ถึงสำคัญพอ ๆ กับตัวการเข้ารหัส

PKI Bootcamp — Basics of Certificate Chain Validation — Paul Turner · 3 นาที 42 วินาที · อังกฤษ — ตอบคำถาม "ทำไมบอร์ดต้องมี root CA ติดตัว" ได้ครบใน 4 นาที

ความเชื่อไม่ได้เกิดจากการพิสูจน์ทั้งเส้น มันเกิดจาก จุดเริ่มต้นที่เราเลือกเชื่อไว้ก่อน แล้วพิสูจน์ต่อจากจุดนั้นลงมา

การจับมือ TLS ทีละขั้น — ในภาษาที่เราใช้กันจริง

พื้นฐาน SSL TLS HTTPS CSR Certificate — SaKKo sama · 11 นาที 56 วินาที · ไทย — คลิปภาษาไทยที่ครอบคลุมทั้ง TLS, HTTPS, CSR และใบรับรอง

สี่จังหวะที่ต้องจำ: หนึ่ง TCP ต่อวงจรก่อน (ยังไม่มีอะไรเข้ารหัส) · สอง ClientHello บอกว่าเรารองรับอะไรบ้าง และ แนบชื่อโฮสต์ปลายทางไปด้วย · สาม เซิร์ฟเวอร์ยื่นใบรับรอง เราตรวจลายเซ็นย้อนขึ้นไปถึง root ที่เรามี แล้วแลกกุญแจลับของรอบนี้ · สี่ ตั้งแต่ Finished เป็นต้นไปทุกไบต์ถูกเข้ารหัส แล้ว MQTT CONNECT ค่อยเดินเข้าไปข้างใน

ตัวเลขในภาพสมมติเวลาเดินทางเที่ยวเดียว 34 ms ตามภาพต้นฉบับ — บนเครือข่ายจริงตัวเลขเปลี่ยน แต่ จำนวนรอบไป-กลับไม่เปลี่ยน และนั่นคือเหตุผลที่ TLS ใช้เวลาเป็น "วินาที" ไม่ใช่ "มิลลิวินาที" บนอุปกรณ์เล็ก

MQTT ไม่ได้รู้เรื่อง TLS เลย — มันแค่ถูกวางไว้ ข้างใน ท่อที่ TLS สร้างเสร็จแล้ว นี่คือความหมายของตัว s ใน MQTTs

เกร็ด: TLS 1.3 ตัดรอบไป-กลับออกไปหนึ่งรอบ

ภาพทั้งสอง: Fleshgrinder และ The Tango! Desktop Project / Wikimedia Commons — สาธารณสมบัติ · ซ้าย TLS 1.2 · ขวา TLS 1.3

TLS 1.2 (RFC 5246, ปี 2008) ใช้ สองรอบไป-กลับ กว่าจะเริ่มส่งข้อมูลจริง ส่วน TLS 1.3 (RFC 8446, ปี 2018) ย้ายการเสนอกุญแจไปไว้ใน ClientHello เลย จึงเหลือ รอบเดียว — ในภาพคือ 136 ms เทียบกับ 68 ms และตัดชุดวิธีเข้ารหัสรุ่นเก่าที่มีปัญหาออกไปทั้งหมด

เชื่อมกับวันนี้: เฟิร์มแวร์ของเราเจรจา TLS 1.2 ไม่ใช่ 1.3 นั่นแปลว่าเวลารอเชื่อมต่อของเราอยู่ในกลุ่มบนของภาพซ้าย — ทั้ง TCP, การจับมือ, การตรวจใบรับรอง แล้วค่อยถึง MQTT CONNECT บวกกันแล้วกินเวลาหลายวินาทีบนบอร์ดที่ CPU ช้า จึงเป็นเหตุผลตรง ๆ ที่โค้ดวันนี้ ต้องมีลูปรอ ไม่ใช่เขียน publish ต่อท้าย connect ทันที

SNI — ชื่อที่เดินไปก่อนการเข้ารหัส

บอร์ดของเรา ClientHello ยังไม่เข้ารหัส เครื่องเดียว หนึ่ง IP หลายชื่อโฮสต์ ใบรับรอง A ใบรับรอง B server_name = sni_hostname ตั้งตรงกับ broker ได้ใบที่ถูก ตรวจผ่าน ต่อติด ตั้งไม่ตรง ได้ใบผิด ตรวจไม่ผ่าน ล้มเงียบ SNI คือชื่อโฮสต์ที่เดินไปแบบเปิดเผย ก่อนการเข้ารหัสจะเริ่ม นี่คือสิ่งเดียวที่คนดักฟังยังเห็นได้บนพอร์ต 8884 — เห็นว่าเราคุยกับใคร แต่ไม่เห็นว่าคุยว่าอะไร

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

ตั้ง sni_hostname ผิด อาการที่ได้คือ "ต่อไม่ติด โดยไม่มีข้อความอะไรเลย" — ไม่ใช่ error ที่บอกว่าชื่อผิด จำอาการนี้ไว้ตั้งแต่ตอนนี้

เข้าใจฮาร์ดแวร์ · TLS วิ่งอยู่ตรงไหนบนบอร์ด

CM33_NS — คอร์ที่รัน MicroPython โค้ด Python ของเรา tesaiot.publish() งาน TLS + MQTT เข้ารหัสทุกไบต์ที่ออก ไดรเวอร์ WiFi — วิทยุตัวเดียวของทั้งบอร์ด root CA ถูกฝังไว้ในเฟิร์มแวร์ตั้งแต่ตอน build CM55 — คอร์ที่วาดจอและอ่านเซนเซอร์ อ่าน CapSense / pot ให้เรา · IMU ด้วยบน Eva วาดผลบนจอผ่าน LVGL ไม่แตะเครือข่ายและไม่แตะการเข้ารหัสเลย ค่าเซนเซอร์เดินข้ามมาทาง IPC ก่อนถูกเข้ารหัส จอค้างไม่ได้แปลว่า TLS หลุด และในทางกลับกันด้วย TLS ต้องการหน่วยความจำก้อนใหญ่ที่สุดตอนจับมือ — ไม่ใช่ตอนส่งข้อมูล นี่คือเหตุผลที่เฟิร์มแวร์จัดสรรหน่วยความจำให้เส้นทาง tesaiot ไว้ล่วงหน้า และเป็นเหตุผลที่ MPY heap มีแค่ 64 KB

การเข้ารหัสทั้งหมดเกิดบน CM33_NS คอร์เดียวกับที่รันโค้ด Python ทุกไบต์ที่ tesaiot.publish() ส่งออกไปจะถูกเข้ารหัสก่อนลงสายอากาศ ส่วน CM55 ที่วาดจอไม่รู้เรื่องด้วยเลย — ภาพนี้จริงทั้งสองบอร์ด เพราะโค้ด Wi-Fi/MQTT/TLS เป็นชุดเดียวกัน ต่างกันแค่ว่าใครอ่าน IMU: บน Eva คอร์จออ่านให้ บน Dev Kit CM33 อ่านเองจาก I2C (CapSense กับลูกบิดยังมาจากคอร์จอทั้งคู่)

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

เรียก tesaiot.connect() ตอนต้นสคริปต์ ตอนหน่วยความจำยังโล่ง แล้วค่อยไปทำอย่างอื่น อย่าเรียกกลางลูปที่ของเต็มมือ

กลไกหลักของคาบ — พอร์ตมาจาก tls_mode ไม่ใช่คีย์ port

กับดักข้อแรกของคาบ: มีคีย์ชื่อ port อยู่จริง แต่มันไม่ได้เลือกพอร์ต config_set("tls_mode", "server_tls") ค่าเริ่มต้นของบอร์ดอยู่แล้ว config_set("port", 1883) ตั้งได้ ไม่ error แต่เปลี่ยนแค่ป้ายที่แสดง เฟิร์มแวร์ อ่าน tls_mode แล้วเลือกพอร์ตเอง ตัน server_tls → 8884 เส้นทางของคาบนี้ mutual_tls → 8883 เส้นทางที่ต้องมีใบรับรองของอุปกรณ์ จอ Eva แสดงเลขที่เราตั้ง ค่าที่แสดง ≠ ค่าที่ระบบใช้จริง พอร์ตเป็นผลลัพธ์ ไม่ใช่ค่าที่รับเข้ามา

นี่คือรูปแบบที่จะเจอไปทั้งชีวิตการทำงาน: ค่าที่ตั้งได้ ไม่ได้แปลว่าค่านั้นมีผล และ ค่าที่จอแสดง ไม่ได้แปลว่าระบบใช้ค่านั้น ในใบงานข้อ 5.3 น้อง ๆ จะได้ลองตั้ง port ให้ผิดแล้วดูเองว่าพอร์ตจริงไม่ขยับ · "จอ" ในกล่องล่างขวาคือการ์ด TESAIoT Connectivity ซึ่งมีบนหน้า Home ของ Eva Kit เท่านั้น — บน Dev Kit ไม่มีการ์ดนี้ ให้ดูจาก print(tesaiot.config()) แทน ผลเหมือนกันทุกประการ: คีย์ port เปลี่ยน แต่พอร์ตที่ต่อจริงไม่เปลี่ยน

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

ทำไม MQTTs ยิงเข้า CE ที่ทีมติดตั้งเองไม่ได้

บอร์ดของเรา root CA ของแพลตฟอร์ม คอมไพล์ติดมากับเฟิร์มแวร์ ไม่มี API ให้เปลี่ยนตอนรัน เปลี่ยนได้ทางเดียวคือ build ใหม่ แพลตฟอร์ม TESAIoT ใบรับรองห้อยจาก root ใบนั้น ตรวจผ่าน · ต่อได้จริง นี่คือปลายทางของคาบ 11 TESAIoT CE ของทีมเอง สุ่ม root CA ใหม่ทุกครั้งที่ติดตั้ง บอร์ดไม่รู้จัก จึงตรวจไม่ผ่าน ไม่ใช่บั๊ก — เป็นผลของการออกแบบ 8884 · ผ่าน 8884 · ไม่ผ่าน คาบ 10 ใช้ 1883 กับ CE จึงทำได้

คาบ 10 ใช้ MQTT ธรรมดา (1883) ยิงเข้า CE ที่ทีมติดตั้งเอง — ได้จริง · คาบ 11 ใช้ MQTTs (8884) ยิงเข้าแพลตฟอร์ม TESAIoT อย่างเป็นทางการ ที่เฟิร์มแวร์ฝัง CA ไว้ตรงกัน — ได้จริง แต่ การเอา MQTTs ไปยิง CE ที่ self-host ทำไม่ได้ในวันนี้ เพราะสคริปต์ติดตั้งของ CE สร้าง root CA ใหม่แบบสุ่มทุกครั้ง ใบรับรองของ broker จึงห้อยจาก root ที่บอร์ดไม่มีทางรู้จัก

จะทำให้ได้ต้องเอา CA ของ CE ชุดนั้น ใส่กลับเข้าไปในซอร์สแล้ว build เฟิร์มแวร์ใหม่ทุกบอร์ด ต่อการติดตั้งหนึ่งชุด — เป็นงานที่ทำได้ แต่ไม่ใช่งานของคาบนี้

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

serverTLS พิสูจน์ตัวตนได้ข้างเดียว

ภาพ: Essich / Wikimedia Commons — CC BY 3.0 — ภาพนี้คือ mTLS ที่ยื่นใบรับรอง ทั้งสองฝั่ง วันนี้เราทำแค่ครึ่งเดียวของมัน

สิ่งที่วันนี้ทำได้จริง

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

สิ่งที่วันนี้ยัง ไม่ ได้ทำ

  • เซิร์ฟเวอร์ ไม่รู้ว่าอุปกรณ์ตัวไหนพูด — มันรู้แค่ว่ามีคนที่รู้ device_id กับ mqtt_pass ที่ถูกต้อง
  • ความลับชนิดนั้น คัดลอกได้ ใครได้รหัสไปก็ปลอมเป็นบอร์ดเราได้ทันที และเซิร์ฟเวอร์แยกไม่ออก
  • ในภาพซ้าย ขั้น "client certificate" คือส่วนที่หายไป — นั่นคือ mTLS ที่ต้องมีกุญแจส่วนตัวอยู่ในอุปกรณ์จริง ๆ

ประโยคที่ต้องตอบได้โดยไม่เปิดสไลด์: serverTLS พิสูจน์ช่องทางและพิสูจน์เซิร์ฟเวอร์ — ไม่ได้พิสูจน์อุปกรณ์ ตัวตนอุปกรณ์วันนี้มาจากรหัสผ่าน ซึ่งเป็นความลับที่ถูกก๊อปได้

1883 กับ 8884 — ตารางที่ทีมต้องกรอกให้ได้เอง

ภาพ: Ademant / Wikimedia Commons — CC BY-SA 4.0 — broker ตัวเดียวเปิดได้ทั้ง listener ธรรมดาและ listener ที่มีใบรับรอง
คาบ 10 · 1883 วันนี้ · 8884
การเข้ารหัส ไม่มีเลย TLS 1.2
ตัวตนของ broker ไม่มีการพิสูจน์ ใบรับรองที่ CA เซ็น + ตรวจ SNI
ตัวตนของอุปกรณ์ client_id ที่ใครก็อ้างได้ credentials ที่ provision รายทีม
ปลายทาง CE ที่ทีมติดตั้งเอง แพลตฟอร์ม TESAIoT
โมดูล mqtt tesaiot

พอร์ต 1883 ไม่ได้ "ปลอดภัยน้อยกว่า" — มันไม่มีความปลอดภัยเลย ทั้งเรื่องเนื้อหาและเรื่องตัวตน สิ่งที่ 8884 เพิ่มเข้ามาคือสองอย่างพร้อมกัน: การเข้ารหัส และการรู้ว่ากำลังคุยกับใคร

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

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

70% — เฟิร์มแวร์ + แพลตฟอร์มทำให้แล้ว TLS · ใบรับรอง · root CA · broker · ฐานข้อมูล · กราฟ 30% — งานของเรา ตัวตน · การรอ · หลักฐาน โค้ดวันนี้สั้นกว่าคาบ 10 แต่ตอบผิดหนึ่งข้อแล้วต่อไม่ติดทั้งคาบ งานที่เหลือให้เราไม่ใช่การพิมพ์ แต่คือการรู้ว่าอะไรพิสูจน์อะไร

สิ่งที่ทำให้แล้ว (70%)
การจับมือ TLS ทั้งหมด · การตรวจใบรับรองย้อนขึ้นไปถึง root · root CA ที่ฝังมากับเฟิร์มแวร์ · การเลือกพอร์ตจาก tls_mode · การประกอบ topic จาก device_id · ฝั่งแพลตฟอร์ม: broker EMQX, บริดจ์ที่รอ subscribe อยู่แล้ว, ฐานข้อมูลอนุกรมเวลา และกราฟที่สร้างจากชื่อคีย์ JSON อัตโนมัติ

สิ่งที่เป็นงานของเรา (30%)
ตั้งค่าตัวตนของอุปกรณ์ให้ถูกทั้งสี่ค่า · รอให้การเชื่อมต่อเสร็จจริงก่อนส่ง · เลือกว่าจะส่งฟิลด์อะไรเป็นตัวเลข · และแสดงหลักฐานบนจอให้คนอื่นตรวจได้โดยไม่ต้องเปิดโค้ด

ยิ่งไลบรารีทำให้เยอะ ความผิดพลาดที่เหลือยิ่ง เงียบ — เพราะสิ่งที่เหลือให้เราพลาดคือเรื่องที่ไลบรารีไม่มีทางรู้ว่าเราตั้งใจอะไร

import tesaiot ได้มา 28 ชื่อ — ใช้ได้จริงเก้าตัว ทั้ง Eva Kit และ Dev Kit

เก้าตัวที่ทำงาน ทั้งหมดอยู่บน CM33 ไม่ข้ามคอร์ config() config_set() config_reset() config_reload() connect() disconnect() is_connected() publish() slots() ทั้งคาบนี้ใช้แค่กล่องนี้ สิบหกตัวที่ข้ามคอร์ไปหาชิป ส่ง IPC ไป CM55 ที่ประกอบมาโดยไม่มี OPTIGA init() device_id() health() license_verify() sign() cred_read/write/erase() random() hash() hmac() aes_keygen() encrypt() decrypt() counter_read() counter_inc() ยังไม่ได้วัดเวลาจริง — อย่าเรียกในลูป อีกสามตัว คนละเรื่อง protected_update() ห้ามเรียกในคาบนี้ Eva: ปิดไว้ (OSError) Dev Kit: เขียนลงชิปจริง http_post() HTTPS จาก CM33 ตาม tls_mode device_identity() ตัวตนบอร์ดให้หน้า Add Device

พูดให้ตรง: ทั้งสองบอร์ดประกอบเฟิร์มแวร์ของคอร์จอด้วย ENABLE_OPTIGA ?= 0 สิบหกคำสั่งที่ข้ามคอร์จึงไม่มีชิปให้คุย · ซอร์สบอกว่าคอร์จอตอบ "ไม่มีให้" กลับมาโดยไม่รอชิป แล้วฝั่ง Python โยน OSError พร้อมข้อความ (เพดานรอ 10 วินาทีมีไว้กรณีคอร์จอไม่ตอบเลย) — ยังไม่ได้วัดเวลาจริงบนบอร์ดไหน ลอง t=time.ticks_ms(); tesaiot.health(); print(time.ticks_diff(time.ticks_ms(), t)) บนโต๊ะก่อนสอน แล้วบอกน้องด้วยตัวเลขที่วัดได้

เรื่องเดียวที่สองบอร์ดต่างกันจริง คือ tesaiot.protected_update() — บน Eva ไม่ได้ถูกคอมไพล์เข้ามา (OSError) แต่บน Dev Kit (ENABLE_OPTIGA_CLM=1) มันทำงานจริง: ขอชุด Protected Update จากแพลตฟอร์มแล้วเขียนใบรับรองลงช่อง E0E1 ของชิป OPTIGA และ csr=True สร้างคู่กุญแจใหม่ทับของเดิม — ห้ามเรียกในคาบนี้ ทั้งจากไฟล์ตัวอย่างและ REPL เพราะสิ่งที่เขียนลงชิปย้อนกลับจากในห้องเรียนไม่ได้

ชิป OPTIGA มีอยู่จริงและใช้ได้ ผ่านโมดูล optiga (สไลด์โบนัสท้ายคาบ) — Eva: I2C จาก CM33 ตรง ๆ · Dev Kit: ชิปอยู่บนบัสจอของ CM55 จอหยุดรับสัมผัสชั่วครู่ทุกครั้งที่เรียก และยังไม่ได้ตรวจว่าทุกบอร์ดมีชิปครบ — ลอง optiga.uid() ก่อน

เก้าตัวที่ใช้ได้ — ตารางเต็มพร้อมกับดักของแต่ละตัว

เรียกอย่างไร คืนอะไร สิ่งที่ต้องรู้
tesaiot.config() dict 19 คีย์ คีย์ครบชุด: tls_mode device_id factory_uid api_key broker port sni_hostname qos keepalive timeout_ms max_retries retry_interval_ms api_host api_port api_endpoint wifi_ssid sntp_server sntp_timezone debug_level
tesaiot.config_set(key, value) True / False รับ สตริงทั้งสองช่อง ตัวเลขก็ต้องส่งเป็นสตริง · คีย์ผิดคืน False เงียบ ๆ ต้องรับค่ากลับมาดู · ตั้ง "tls_mode","server_tls" แล้วอ่านกลับได้ "serverTLS" เพราะมันแปลงชื่อให้
tesaiot.config_reset() None ล้างกลับเป็นค่าโรงงาน ทั้ง 19 คีย์ ตัวตนของทีมหายหมด ต้องตั้งใหม่ทุกค่า
tesaiot.config_reload() True / False อ่านไฟล์ตั้งค่าจากแฟลชขึ้นมาใหม่ ทับค่าที่แก้ไว้ในหน่วยความจำ · ใช้ทิ้งการแก้ที่ยังไม่พอใจ
tesaiot.connect() True / False True แปลว่า งานเริ่มแล้ว ไม่ใช่ต่อเสร็จแล้ว ต้องวนรอ is_connected() เอง
tesaiot.disconnect() True / False ตัวนี้คืน bool ไม่เหมือน mqtt.disconnect() ที่คืน None — สองโมดูลไม่เหมือนกัน
tesaiot.is_connected() True / False ตัวจริงที่ตอบว่าต่อเสร็จหรือยัง
tesaiot.publish(payload, topic=None) True / False payload มาก่อน topic และ topic ใส่หรือไม่ใส่ก็ได้ ไม่ใส่แล้วเฟิร์มแวร์ประกอบให้จาก device_id
tesaiot.slots() dict 13 คู่ ตอบได้ทันทีโดยไม่แตะชิป เพราะเป็นตารางชื่อในซอร์ส · ช่อง 4 ถูกกันไว้ จึงไม่อยู่ในรายการ

tesaiot.publish() กับ mqtt.publish() สลับลำดับกัน — คาบที่แล้วเขียน mqtt.publish(topic, payload) วันนี้เขียน tesaiot.publish(payload) สลับเมื่อไรได้ผลประหลาดทันทีโดยไม่มี error เพราะทั้งสองช่องรับสตริงเหมือนกัน

config_set() เก็บค่าไว้เฉย ๆ ยังไม่ได้ต่ออะไรทั้งนั้น พิมพ์คีย์ผิดจะไม่มีใครเตือนจนกว่าจะต่อไม่ติด — จึงต้อง print(tesaiot.config()) หนึ่งครั้งหลังตั้งค่าเสมอ

แกะโค้ดจริง — ท่าที่ 1 ตั้งค่าตัวตนของอุปกรณ์

# --- ท่าที่ 1: ตั้งค่าตัวตนของอุปกรณ์ ---
tesaiot.config_set("device_id", DEVICE_ID)
tesaiot.config_set("api_key", API_KEY)
tesaiot.config_set("mqtt_pass", MQTT_PASS)
tesaiot.config_set("broker", BROKER)
tesaiot.config_set("sni_hostname", BROKER)     # ต้องเป็นชื่อเดียวกับ broker
print("config ปัจจุบัน:", tesaiot.config())

config_set() รับ ทีละคู่ (key, value) และแค่ เก็บค่าไว้ในเฟิร์มแวร์ ยังไม่ได้ต่ออะไรทั้งสิ้น — พิมพ์ชื่อคีย์ผิดก็ไม่มีใครเตือน ค่าที่ผิดจะไปโผล่ตอนต่อไม่ติดเท่านั้น

จึงต้อง print(tesaiot.config()) ทุกครั้งหลังตั้งค่า ก่อน จะไปท่าถัดไป — เป็นวิธีเดียวที่เห็นว่าค่าเข้าครบและสะกดถูก

device_id ต้อง สั้นกว่า 31 ตัวอักษร เกินแล้วถูกตัดเงียบ แล้วแพลตฟอร์มจะหาอุปกรณ์ไม่เจอ

ตัวตน = สามอย่างรวมกัน device_id — ชื่อที่แพลตฟอร์มรู้จัก api_key — กุญแจของ API mqtt_pass — รหัสของ broker ขาดข้อใดข้อหนึ่ง = ถูกปฏิเสธ

sni_hostname ไม่ใช่ค่าเสริม — มันคือชื่อที่ทำให้เซิร์ฟเวอร์ หยิบใบรับรองใบที่ถูกมายื่นให้เรา ตั้งไม่ตรงเมื่อไร การจับมือล้มโดยไม่มีข้อความบอก

แกะโค้ดจริง — ท่าที่ 2 สั่งต่อ แล้ววนรอจนต่อเสร็จจริง

# --- connect() เป็น API แบบ asynchronous ---
tesaiot.connect()                       # คืนค่าทันที ยังไม่ได้แปลว่าต่อแล้ว
t0 = time.ticks_ms()
...
while not tesaiot.is_connected():       # ตัวจริงที่ตอบได้คือ is_connected()
    waited = time.ticks_diff(time.ticks_ms(), t0)
    if waited > WAIT_CEILING_S * 1000:  # WAIT_CEILING_S = 30 ตัวเลขเดียวกับบนจอ
        print("ต่อแพลตฟอร์มไม่สำเร็จใน 30 วินาที")
        break
    ...
    time.sleep_ms(500)
connect() คืนค่าตรงนี้ TCP จับมือ TLS จับมือ + ตรวจใบรับรอง MQTT CONNECT is_connected() เป็น True ตรงนี้ หลายวินาทีหลังจากนั้น publish() ที่เขียนต่อท้าย connect() ทันที จะยิงลงช่วงกลางเส้นนี้ แล้วหายไปเงียบ ๆ ลูปรอทุกลูปต้องมี timeout — ไม่งั้นวันที่เน็ตล่ม โปรแกรมค้างตรงนี้ตลอดกาลโดยไม่มีใครรู้

tesaiot.connect() เป็น API แบบ asynchronous คืนค่าทันทีเพื่อไม่บล็อกโปรแกรม — ค่าที่คืนมา ไม่ใช่สถานะสุดท้าย สิ่งเดียวที่ตอบได้ว่าต่อเสร็จหรือยังคือ tesaiot.is_connected() · ใบงานข้อ 5.2 ให้ลบลูปนี้ออกแล้วรันหนึ่งรอบ — จะเห็นด้วยตาว่า publish ที่ยิงเร็วเกินไป ไม่ error และไม่ถึงแพลตฟอร์ม

"ฟังก์ชันคืนค่าแล้ว" กับ "งานเสร็จแล้ว" เป็นคนละเรื่องเสมอสำหรับ API แบบ async — ความต่างนั้นวัดได้เป็นวินาที

แกะโค้ดจริง — ท่าที่ 3 และ 4 ส่งค่าจริง แล้วโชว์หลักฐานบนจอ

m = sensors.bmi270.motion()                       # (ax, ay, az, gx, gy, gz)
payload = {"accel_x": round(m[0], 2),
           "heading": round(sensors.bmm350.heading(), 1),
           "pot": sensors.pot.percent()}
tesaiot.publish(json.dumps(payload))              # ไม่ต้องใส่ topic เฟิร์มแวร์ประกอบให้
cfg = tesaiot.config()
lcd.print("ส่งครั้งที่", sent, "| โหมด", cfg["tls_mode"], "-> 8884")
ค่าจากเซนเซอร์ ax · heading · pot round() ก่อนเสมอ ตัวเลขจริง ไม่ใช่สตริง ส่งแบน ไม่ต้องห่อ {"accel_x":0.12, "heading":183.4} แพลตฟอร์มห่อให้เอง ชื่อคีย์ตรงได้หน่วย เข้ารหัสแล้วส่ง พอร์ต 8884 topic ประกอบจาก device_id เราไม่ต้องพิมพ์ topic เอง จอบอร์ด ตัวนับเดินขึ้น tls_mode ที่ใช้จริง ท่าที่ 4 ไม่ใช่ของประดับ — มันคือหลักฐานที่กรรมการอ่านได้โดยไม่ต้องเปิดโค้ด

tesaiot.publish(payload) รับ ตัวข้อมูลอย่างเดียว — ต่างจาก mqtt.publish(topic, payload) ของคาบที่แล้ว เพราะเฟิร์มแวร์ประกอบ topic ให้เองจาก device_id ที่เราตั้งไว้ในท่าที่ 1 (จะระบุ topic เองก็ได้ แต่คาบนี้ไม่ต้อง) · ส่ง ตัวเลขจริง ไม่ใช่สตริง ไม่งั้น dashboard จะขึ้นค่าแต่วาดกราฟไม่ได้ — กับดักเดียวกับคาบ 10

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

ข้อมูลไหลไปทางไหน — ตั้งแต่เซนเซอร์ถึงกราฟ

เซนเซอร์ BMI270 · pot โค้ดของเรา json.dumps ชั้น TLS เข้ารหัสทุกไบต์ EMQX listener 8884 บริดจ์ + ฐานข้อมูล อนุกรมเวลา กราฟ อัปเดตสด กล่องสีม่วงคือทั้งหมดที่เพิ่มมาจากคาบ 10 — ที่เหลือเหมือนเดิมทุกกล่อง และมันอยู่ในเฟิร์มแวร์ ไม่ได้อยู่ในโค้ดที่เราเขียน ดีบักตามลำดับนี้เสมอ: is_connected() → print(config()) → กราฟบนแพลตฟอร์ม ถ้า is_connected() ยัง False ปัญหายังไม่เดินทางไปถึงเรื่อง topic หรือรูปร่าง JSON เลย อย่าเริ่มเดาจากปลายทาง — กราฟว่างเปล่าบอกได้แค่ว่า "ไม่ถึง" ไม่ได้บอกว่าตกตรงไหน

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

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

# --- ล้างแล้วตั้งใหม่: ทางออกเมื่อตั้งค่ามั่วจนไม่รู้ว่าเหลืออะไรอยู่ ---
tesaiot.config_reset()            # คืน None และล้างครบทั้ง 19 คีย์
tesaiot.config_set("device_id", DEVICE_ID)   # ต้องตั้งใหม่ทุกค่า

# --- ทิ้งการแก้ที่ยังไม่พอใจ แล้วดึงของเดิมจากแฟลชกลับมา ---
tesaiot.config_reload()           # True ถ้าอ่านไฟล์ตั้งค่าสำเร็จ

# --- ชื่อช่องเก็บความลับ ตอบได้โดยไม่ต้องแตะชิป ---
print(tesaiot.slots())            # {'device_id': 0, 'license': 1, ...}

# --- ปิดงานให้เรียบร้อย ตัวนี้คืน bool ไม่เหมือน mqtt.disconnect() ---
tesaiot.disconnect()
config_reset() ล้างครบทั้ง 19 คีย์ ตัวตนของทีมหายด้วย ต้องตั้งใหม่ทุกค่าก่อนต่อ ใช้ตอนหลงทางเท่านั้น config_reload() อ่านไฟล์จากแฟลชกลับมา ทับค่าที่แก้ค้างไว้ ของที่ยังไม่ได้เซฟหายไป ใช้ตอนอยากย้อนกลับ slots() คืนชื่อช่องเก็บความลับ 13 ช่อง ตอบทันที ไม่แตะชิป ไม่ค้าง ช่อง 4 ถูกกันไว้ ไม่อยู่ในนี้ แผนที่ของงานคาบต่อ ๆ ไป

config_reset() ล้างของจริง ส่วน config_reload() แค่ย้อนกลับไปหาของที่เซฟไว้ — ตั้งค่ามั่วจนงงว่าเหลืออะไรอยู่ ให้ config_reset() แล้วเริ่มใหม่จากศูนย์ ดีกว่าไล่แก้ทีละคีย์ · slots() ตอบได้โดยไม่ต้องข้ามคอร์ไปถามชิป เพราะอ่านตารางชื่อในเฟิร์มแวร์ — แผนที่ว่าถ้าวันหนึ่งเปิด OPTIGA ได้ ความลับแต่ละอย่างจะไปนอนช่องไหน · tesaiot.disconnect() คืน True/False ไม่เหมือน mqtt.disconnect() ที่คืน None อย่าจำรวมกัน

examples/s11/01_config_store.py↗ ไล่คีย์ทั้ง 19 · examples/s11/02_config_reset_reload.py↗ สองตัวที่สลับกันง่าย · examples/s11/03_slots_and_the_dead_half.py↗ จับเวลาครึ่งที่ต้องมีชิป · examples/s11/04_disconnect_and_republish.py↗ ปิดแล้วเปิดใหม่

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

1 · ก่อนแตะโค้ด รับค่าประจำตัวสี่ตัวจากผู้สอน กรอกกล่องข้อ 4.1 ในใบงาน ต่อ WiFi ให้ได้ก่อนเสมอ นับตัวอักษร device_id ให้ไม่เกิน 31 2 · แก้ห้าบรรทัดบนหัวไฟล์ TEAM_NAME · DEVICE_ID API_KEY · MQTT_PASS · BROKER ที่เหลือเหมือนกันทุกทีม เติมช่องว่างทีละจุด แล้วรันทุกครั้ง 3 · รันแล้วดูสองจอ จอบอร์ด: ตัวนับกับ tls_mode จอคอม: dashboard ของแพลตฟอร์ม ขยับบอร์ดแล้วกราฟต้องขยับตาม
  1. เปิดหน้า BENTO Playground บนบอร์ดค้างไว้ แล้วต่อ WiFi ให้เรียบร้อยก่อน (wifi.connect() ของคาบ 9 บล็อกได้นานถึงราว 85 วินาทีถ้ารหัสผิด)
  2. เปิด practise_codes/s11_secure_telemetry.py↗ แก้ห้าบรรทัดบนหัวไฟล์ให้เป็นค่าของทีม
  3. เติมช่องว่างท่าที่ 1 ให้ครบ กด Program to Device แล้วดูว่า print(tesaiot.config()) ขึ้นค่าครบและสะกดถูก ก่อน ไปท่าที่ 2
  4. เติมท่าที่ 2 แล้วรัน — จับเวลาว่ากี่วินาที is_connected() จึงเป็น True แล้วจดลงใบงานข้อ 4.2
  5. เติมท่าที่ 3 และ 4 แล้วเปิด dashboard ของแพลตฟอร์ม เลือกอุปกรณ์ของทีม ดูกราฟขยับพร้อมกับตัวนับบนจอบอร์ด
  6. ถ่ายภาพทั้งสองจอเก็บไว้เป็นหลักฐาน แล้วค่อยไปทำตารางเทียบข้อ 4.3

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

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

ลำดับ · เรื่อง · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · อ่านค่าที่บอร์ดเก็บไว้ก่อนต่ออะไร · 6 นาที examples/s11/01_config_store.py↗ กรอกกล่องข้อ 4.1 จากค่าที่บอร์ดตอบจริง ไม่ใช่จากใบที่ผู้สอนแจก · จะเข้าใจว่าต้องอ่านค่าที่บอร์ดเก็บไว้ให้ครบก่อน แล้วค่อยสั่งต่ออะไร
2 · รอให้ต่อเสร็จเป็น · 8 นาที examples/s11/05_wait_for_connected.py↗ เขียนลูปรอด้วย is_connected() และจับเวลาจริงลงใบงานข้อ 4.2 · จะเข้าใจว่า connect() คืนค่าก่อนต่อเสร็จ จึงต้องรอเป็น ไม่ใช่เชื่อว่าคืนค่าแล้วคือพร้อม
3 · ลูปส่ง telemetry บนช่องที่เข้ารหัส · 15 นาที examples/s11/06_secure_publish_loop.py↗ ประกอบ MVP ของคาบนี้ — ตัวนับเดินขึ้นบนจอ พร้อมกราฟที่ขยับบน dashboard · จะเห็นว่าลูปส่ง telemetry ตัวเดิมย้ายขึ้นช่องที่เข้ารหัสได้ โดยรูปร่างของลูปไม่เปลี่ยน

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

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

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
ต่อไม่ติด และแยกไม่ออกว่าติดที่ WiFi หรือที่แพลตฟอร์ม examples/s09/04_ping_two_targets.py↗ — พิสูจน์ให้จบก่อนว่าออกอินเทอร์เน็ตได้จริง แล้วค่อยโทษ TLS
ตอบไม่ได้ว่าวันนี้ต่างจากคาบ 10 ตรงไหน examples/s10/03_connect_and_publish.py↗ — เปิดคู่กับไฟล์ที่ 3 ข้างบน แล้วเทียบทีละบรรทัดว่าอะไรเปลี่ยน

อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของคาบ 11 แต่เป็นนิสัยที่ใช้ได้ทั้งคาบ 10 และคาบนี้: examples/s10/06_sent_is_not_delivered.py↗ สอนให้นับใบที่ ถึงปลายทาง ไม่ใช่นับใบที่เราสั่งส่ง · ส่วนตัวอย่างที่เทียบ 1883 กับ 8884 ให้เห็นด้วยตาว่า TLS ปกป้องอะไร ยังไม่มีในคลัง วันนี้จึงใช้ตารางข้อ 4.3 กับผลดักฟังในสไลด์แทน

ก่อนหน้านี้คาบนี้มีตัวอย่างอีกสามไฟล์ที่บันทึกพฤติกรรมแปลกของ tesaiot.config() ไว้ (ตั้งค่าแล้วอ่านกลับได้อีกคำ · พิมพ์ผิดแล้วยังคืน True · คีย์ port ที่ไม่มีใครอ่าน) ทั้งสามถูกถอดออก เพราะเป็นรายงานบั๊กของเฟิร์มแวร์ ไม่ใช่นิสัยที่เอาไปใช้กับงานตัวเองได้ ความจริงเรื่องพอร์ตยังอยู่ในสไลด์ "พอร์ตมาจาก tls_mode ไม่ใช่คีย์ port" และในตารางกับดัก

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

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

ภาพหน้าจอ: TESAIoT Community Edition v1.1.8 — เอกสารของ repo (Apache-2.0) — หน้าตาของ dashboard ที่จะไปดูกราฟของทีม

telemetry ของทีมขึ้น dashboard แพลตฟอร์มผ่าน TLS + ตอบได้ว่าต่างจากคาบ 10 ตรงไหน

  • [ ] กราฟของ device_id ทีมนั้น ขยับ บน dashboard ของแพลตฟอร์มจริง และขยับตามเมื่อเอียงบอร์ด
  • [ ] จอบอร์ดแสดง device_id + โหมด tls_mode + ตัวนับที่เดินขึ้นต่อเนื่อง
  • [ ] กล่องค่าประจำตัวข้อ 4.1 ครบสี่ค่า และ device_id ไม่ซ้ำทีมอื่น
  • [ ] ตารางเทียบ 1883 กับ 8884 ข้อ 4.3 กรอกครบทั้งเจ็ดแถว จากสิ่งที่เห็นเอง ไม่ใช่ลอกสไลด์
  • [ ] ตอบได้โดยไม่เปิดสไลด์ว่า serverTLS ปกป้องอะไร และไม่ปกป้องอะไร
  • [ ] ตอบได้ว่าพอร์ต 8884 ถูกเลือกมาจากอะไร (และทำไมไม่ใช่จากคีย์ port)

ข้อที่ห้าคือหัวใจ — ถ้าตอบไม่ได้ เราติดตั้งความปลอดภัยเป็น แต่ยังไม่รู้ว่าเราซื้ออะไรมาด้วยราคาเท่าไร

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

อาการ สาเหตุที่แท้จริง วิธีแก้
ต่อไม่ติด ไม่มีข้อความอะไรเลย sni_hostname ไม่ตรงกับ broker เซิร์ฟเวอร์ยื่นใบผิดใบ ตั้งสองค่านี้เป็นสตริงเดียวกันเสมอ
ต่อไม่ติดกับ CE ที่ทีมติดตั้งเอง CE สุ่ม root CA ใหม่ทุกการติดตั้ง บอร์ดฝัง CA ของแพลตฟอร์มไว้ ใช้โฮสต์ที่ผู้สอนให้ — MQTTs เข้า CE ต้อง build เฟิร์มแวร์ใหม่
ตั้ง port แล้วพอร์ตไม่เปลี่ยน พอร์ตมาจาก tls_mode คีย์ port เปลี่ยนแค่ป้ายที่แสดง ดู tls_mode เป็นคำตอบ อย่าดูคีย์ port
publish() ไม่ error แต่ไม่มีข้อมูลบนแพลตฟอร์ม ยิงก่อน is_connected() เป็น True เพราะ connect() เป็น async วนรอด้วย while not tesaiot.is_connected() พร้อม timeout
โปรแกรมค้างตรงลูปรอ ไม่ไปไหนเลย ลูปรอไม่มี timeout วันที่เน็ตล่มจึงวนตลอดกาล time.ticks_diff() เกิน 30000 ms แล้ว break
ทั้งห้องหลุดสลับกันเป็นจังหวะ หลายทีมใช้ device_id ค่าเริ่มต้นตัวเดียวกัน broker เตะตัวเก่าออก หนึ่งทีมหนึ่ง device_id ที่ provision มา
ข้อมูลขึ้นแต่ไม่มีเส้นกราฟ ส่งค่าเป็นสตริง ส่งตัวเลขจริง ไม่ใช่ "25.5"
ชื่อวัดขึ้นต้นด้วย data_ และไม่มีหน่วย ห่อ payload เองด้วย {"data": ...} ส่งแบน bridge ห่อให้เองอยู่แล้ว

แปดแถวนี้เป็นเรื่องสาย ใบรับรอง และรูปร่างข้อมูล — หน้าถัดไปคือกับดักที่อยู่ในตัวโมดูล tesaiot เอง

กับดักที่เจอบ่อย (ต่อ) — ขอบเขตของโมดูล tesaiot

อาการ สาเหตุที่แท้จริง วิธีแก้
เรียก device_id() health() random() แล้วได้ OSError "... failed" ตัวในกลุ่มสิบหกส่ง IPC ไปคอร์จอ ซึ่งทั้ง Eva และ Dev Kit ประกอบมาโดยไม่มี OPTIGA — คอร์จอตอบ "ไม่มีให้" กลับมา (เวลาที่เสียไปยังไม่ได้วัดจริง) อย่าเรียกกลุ่มนั้นบนบอร์ดไหนเลย · อยากคุยกับชิปจริงให้ใช้โมดูล optiga แทน
บน Dev Kit เรียก tesaiot.protected_update() แล้วคืน True ไม่มี error ฟังก์ชันนี้ทำงานจริงบน Dev Kit (ENABLE_OPTIGA_CLM=1) — มันขอชุด Protected Update จากแพลตฟอร์มแล้วเขียนใบรับรองลงชิป OPTIGA ห้ามเรียกในคาบนี้ ทั้งสองบอร์ด — บน Eva ได้ OSError แต่บน Dev Kit สิ่งที่เขียนลงชิปย้อนกลับจากในห้องเรียนไม่ได้
publish() คืน True แต่ข้อมูลไปโผล่ผิด topic สลับลำดับเป็น tesaiot.publish(topic, payload) ตามความเคยชินจากคาบ 10 ตัวนี้ payload มาก่อน เขียน tesaiot.publish(json.dumps(payload))
ตั้ง tls_mode เป็น "server_tls" แล้วอ่านกลับได้คนละคำ เฟิร์มแวร์แปลงชื่อโหมดให้เป็น "serverTLS" ปกติ ไม่ใช่บั๊ก เทียบค่าด้วยคำที่อ่านกลับมา อย่าเทียบกับคำที่เราส่งไป
if tesaiot.disconnect(): ทำงาน แต่ if mqtt.disconnect(): ไม่เคยจริง สองโมดูลคืนคนละชนิด ตัวแรกคืน bool ตัวหลังคืน None อย่าจำรวมกัน เปิดตารางดูทุกครั้งที่สลับโมดูล

สิบในสิบสามแถวของสองหน้านี้ ไม่ใช่บั๊กในโค้ด แต่เป็นความเข้าใจผิดเรื่องขอบเขตของ API — โค้ดถูกทุกตัวอักษร แต่สมมติฐานผิดหนึ่งข้อ

อ่านตารางนี้ ก่อน เจอปัญหา — แทบไม่มีอาการไหนมีข้อความ error ที่ชี้ไปหาสาเหตุ และแถว protected_update() คือแถวเดียวที่ "ไม่มี error" แปลว่าเสียหายไปแล้ว

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

s11_secure_telemetry.py pass ท่า 1 — config_set สามบรรทัด (ตัวตน) pass ท่า 1 — broker + sni_hostname pass ท่า 2 — ลูปรอ is_connected() + timeout pass ท่า 3 — payload ตัวเลขจริง pass ท่า 4 — lcd.print หลักฐานบนจอ

เปิด practise_codes/s11_secure_telemetry.py↗ มีช่องว่างให้เติม 5 จุด

# เติม: tesaiot.config_set("device_id", DEVICE_ID) แล้วอีกสองบรรทัด api_key และ mqtt_pass
pass
# เติม: tesaiot.config_set("broker", BROKER) แล้วบรรทัดถัดไป sni_hostname เป็นชื่อเดียวกัน
pass
# เติม: while not tesaiot.is_connected(): เกิน 30000 ms ให้ break ไม่งั้น time.sleep_ms(500)
pass
    # เติม: payload = {"accel_x": round(m[0], 2), "heading": ..., "pot": ...}   # แบน ไม่ห่อ
    pass
    # เติม: lcd.print("ส่งครั้งที่", sent, "| โหมด", cfg["tls_mode"], "-> 8884")
    pass

หนึ่ง แก้ห้าบรรทัดบนหัวไฟล์ สอง เติมท่า 1 แล้วรันดู print(tesaiot.config()) สาม เติมท่า 2 แล้วจับเวลา สี่ เติมท่า 3-4 แล้วเปิด dashboard

tesaiot.connect() มีให้แล้วในไฟล์ ไม่ใช่ช่องว่าง — สิ่งที่เราต้องเขียนคือลูปที่รอมัน นั่นคือประเด็นของท่าที่ 2 ทั้งท่า

โบนัส · ตัวตนที่ฝังอยู่ในซิลิคอน (สาธิต ไม่คิดคะแนน)

ภาพ: Raimond Spekking / Wikimedia Commons — CC BY-SA 4.0 — ชิปความปลอดภัยของ Infineon (SLB9655 บนเมนบอร์ดโน้ตบุ๊ก) เป็นคนละรุ่นกับ OPTIGA Trust M บนบอร์ดเรา แต่หน้าตาและหน้าที่เป็นตระกูลเดียวกัน

บน Eva Kit มีชิป OPTIGA Trust M อยู่จริง และเรียกจาก REPL ได้แล้ววันนี้ — ยืนยันด้วยการทดสอบบนบอร์ดจริง · บน Dev Kit ชิปตัวเดียวกันต่ออยู่บนบัสจอของ CM55 ทุกครั้งที่เรียก จอจะหยุดรับสัมผัสชั่วครู่ และยังไม่ได้ตรวจว่าทุกบอร์ดติดตั้งชิปมาครบ — ก่อนสาธิตให้ลอง optiga.uid() บนบอร์ดตัวนั้นก่อน

optiga.uid()          # เลขประจำตัวที่โรงงานเขียนไว้ อ่านได้ ลบไม่ได้
optiga.random(16)     # เลขสุ่มจากวงจรจริง ไม่ใช่จากสูตรในซอฟต์แวร์
optiga.sha256(b"...") # แฮชด้วยฮาร์ดแวร์
optiga.sign(...)      # เซ็นด้วยกุญแจที่ออกจากชิปไม่ได้

ความต่างที่สำคัญ: mqtt_pass ของวันนี้เป็นความลับที่ คัดลอกได้ ส่วนกุญแจส่วนตัวในชิปนี้ ออกจากชิปไม่ได้เลย ชิปยอมเซ็นให้เท่านั้น ใครขโมยบอร์ดไปได้ต้องขนบอร์ดไปทั้งตัว จะก๊อปตัวตนไปเฉย ๆ ไม่ได้

และในชิปยังมี ใบรับรองจากโรงงาน อ่านออกมาดูได้ — นั่นคือชิ้นส่วนที่ mTLS ต้องการ

นี่คือคำตอบของคำถามที่ค้างไว้เมื่อสองสไลด์ก่อน: ตัวตนของอุปกรณ์ที่ก๊อปไม่ได้ ต้องมาจากฮาร์ดแวร์ ไม่ใช่จากสตริงในไฟล์ Python

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

ฝั่งสมองกลฝังตัว งบหน่วยความจำตอนจับมือ ค่าคงที่ที่คอมไพล์ติดมา ตัวตนที่มาจากซิลิคอน ฝั่งเครือข่ายและ Python กุญแจคู่ · ลายเซ็น · แฮช ใบรับรอง · ห่วงโซ่ · SNI API แบบ async ต้องรอเป็น ฝั่งออกแบบระบบ ขอบเขตของกลไกความปลอดภัย ตัวตนรายอุปกรณ์ ไม่ใช่รายรุ่น ค่าที่แสดง ≠ ค่าที่ใช้จริง

วันนี้เราได้: ส่ง telemetry ขึ้นแพลตฟอร์มผ่านช่องที่เข้ารหัส · อ่านการจับมือ TLS ได้ทีละขั้นและรู้ว่าใบรับรองพิสูจน์อะไร · รู้ว่าพอร์ตมาจาก tls_mode ไม่ใช่คีย์ port · และรอ API แบบ async เป็น แทนที่จะเชื่อค่าที่ฟังก์ชันคืนมา

สิ่งที่ติดตัวไปแม้เปลี่ยนภาษาและเปลี่ยนบอร์ด: นิสัยถามว่า "กลไกนี้พิสูจน์อะไร และไม่ได้พิสูจน์อะไร" ก่อนจะเชื่อมัน · การแยก "ฟังก์ชันคืนค่า" ออกจาก "งานเสร็จ" · และการไม่เชื่อค่าที่ตัวเองเพิ่งตั้งจนกว่าจะวัดที่ปลายทาง

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

คาบหน้า: capstone — ทีมเลือกโจทย์อุตสาหกรรมของตัวเอง แล้วประกอบ เซนเซอร์ → หน้าจอ → MQTT/MQTTs → แพลตฟอร์ม ให้ครบวงจรเป็นระบบเดียว

คาบนี้เป็นคาบแรกที่คำตอบที่ถูกที่สุดคือ "ได้ แต่แค่ระดับนี้" — และนั่นคือวิธีที่วิศวกรพูดถึงความปลอดภัยเสมอ

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

TEAM_NAME = "BentoBuilders"
DEVICE_ID = "team03"              # ต้องตรงกับที่ขึ้นทะเบียน และสั้นกว่า 31 ตัวอักษร
API_KEY   = "<api key ของทีม>"
MQTT_PASS = "<รหัสผ่าน MQTT 16 ตัว>"
BROKER    = "<โฮสต์แพลตฟอร์ม>"

tesaiot.config_set("device_id", DEVICE_ID)
tesaiot.config_set("api_key", API_KEY)
tesaiot.config_set("mqtt_pass", MQTT_PASS)
tesaiot.config_set("broker", BROKER)
tesaiot.config_set("sni_hostname", BROKER)      # ชื่อเดียวกับ broker เสมอ
print("config ปัจจุบัน:", tesaiot.config())
ค่าที่ต้องแก้อยู่ห้าบรรทัด รวมไว้บนหัวไฟล์ที่เดียว ข้างล่างไม่ต้องแตะเลย คนมาแก้ทีหลังหาเจอทันที BROKER โผล่สองที่ broker และ sni_hostname จึงใช้ตัวแปรตัวเดียว ทำให้ไม่มีทางไม่ตรงกัน print(config()) ก่อนต่อ เห็นค่าที่เฟิร์มแวร์เก็บจริง จับคีย์ที่สะกดผิดได้ตรงนี้ ถูกกว่าการไปเดาตอนต่อไม่ติด

เขียนค่าที่ต้องตรงกันไว้ที่เดียว แล้วบั๊กตระกูล "ลืมแก้ที่หนึ่ง" จะหายไปทั้งตระกูล — คาบนี้ค่านั้นคือ BROKER

เฉลย — ตัวตนที่ตั้งไว้ ต้องมองเห็นได้ ไม่ใช่เชื่อเอา

cfg0 = tesaiot.config()
tbl_id = ui.Table(x=30, y=62, w=412, h=286, cols=2)
tbl_id.col_width(0, 140)
tbl_id.col_width(1, 260)
tbl_id.add_row("คีย์", "ค่าที่ตั้งไว้")
tbl_id.add_row("device_id", DEVICE_ID)
tbl_id.add_row("broker", BROKER)
tbl_id.add_row("tls_mode", str(cfg0["tls_mode"]))
ui.Label("mqtt_pass ตั้งแล้ว แต่อ่านกลับไม่ได้", x=30, y=356, color=COL_DIM, value=14)

led_wait = ui.Led(x=488, y=64, w=28, h=28, color=COL_RUN, value=1)
led_ok = ui.Led(x=588, y=64, w=28, h=28, color=COL_OK, value=0)
led_fail = ui.Led(x=688, y=64, w=28, h=28, color=COL_BAD, value=0)

bar_hs = ui.Bar(x=484, y=236, w=200, h=12, color=COL_RUN,
                min=0, max=WAIT_CEILING_S, value=0)
sc_hs = ui.Scale(x=484, y=250, w=200, h=44, color=COL_TEXT,
                 min=0, max=WAIT_CEILING_S)

config_set() เงียบสนิท ค่าที่ตั้งไปจึงไม่มีใครเห็น จนกว่าจะเอาขึ้นจอ print() ตอบได้เฉพาะคนที่นั่งอยู่หน้าคอม ส่วนตารางนี้ตอบคนที่เดินมาดูบอร์ด

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

ไฟสามดวงติดทีละดวง ไล่ตามจังหวะจริงของการจับมือ และ bar_hs เดินขึ้นเทียบกับ WAIT_CEILING_S ซึ่งเป็น ตัวเลขเดียวกับที่ลูป while ใช้ตัดสิน ไม่ใช่เลขที่วาดไว้ให้ดูสวย

คนที่ยืนรออยู่หน้าจอ 30 วินาที ต้องเห็นว่าโปรแกรมยังทำงาน จอที่นิ่งสนิทกับบอร์ดที่แฮงก์ หน้าตาเหมือนกันทุกประการ

เฉลย — ส่วนที่สอง: ลูปหลัก

tesaiot.connect()
t0 = time.ticks_ms()
last_s = -1
while not tesaiot.is_connected():
    waited = time.ticks_diff(time.ticks_ms(), t0)
    if waited > WAIT_CEILING_S * 1000:
        print("ต่อแพลตฟอร์มไม่สำเร็จใน 30 วินาที")
        break
    if waited // 1000 != last_s:          # ตัวเลขเปลี่ยนวินาทีละครั้ง ไม่ถี่กว่านั้น
        last_s = waited // 1000
        bar_hs.value(last_s)
        lbl_hs.text(str(last_s) + " วิ")
    ui.poll()
    time.sleep_ms(500)
...
while tesaiot.is_connected():
    try:
        m = sensors.bmi270.motion()             # (ax, ay, az, gx, gy, gz)
        payload = {"accel_x": round(m[0], 2),
                   "heading": round(sensors.bmm350.heading(), 1),
                   "pot": sensors.pot.percent()}
    except OSError:
        time.sleep_ms(1000)
        continue
    tesaiot.publish(json.dumps(payload))
    sent += 1
    cfg = tesaiot.config()
    lcd.print("ส่งครั้งที่", sent, "| โหมด", cfg["tls_mode"], "-> 8884")
    lbl_sent.text("ส่งแล้ว " + str(sent) + " ใบ")
    for ev in ui.poll():
        ...        # ปุ่มต่อใหม่ / ตัดสาย พร้อมกล่องยืนยัน - อยู่ในไฟล์เฉลยเต็ม
    time.sleep_ms(5000)

เงื่อนไขของ while คือการเช็กสายก่อนทุกรอบส่ง ไม่ใช่เช็กครั้งเดียวตอนเริ่ม · สามค่าในลูปส่งอยู่ใน try เดียวกัน — อ่านพลาดหนึ่งรอบต้องไม่ทำให้หลุดการเชื่อมต่อ

เฉลย — ทำไมเรียงสี่ท่าแบบนี้

ท่า 1 — ตัวตนถูกต้อง ท่า 2 — ต่อเสร็จจริง ท่า 3 — ข้อมูลขึ้นกราฟ ท่า 4 — พิสูจน์ได้บนจอ

ท่า 1 มาก่อนเพราะตรวจได้โดยไม่ต้องใช้เน็ต — print(config()) บอกทันทีว่าค่าเข้าครบไหม · ท่า 2 แยกเป็นท่าของตัวเอง เพราะ "ต่อเสร็จ" เกิดทีหลังคำสั่ง ไม่ใช่ผลของคำสั่ง · ท่า 3 ส่งข้อมูลจริง เมื่อสองท่าแรกยืนยันแล้ว ถ้ากราฟยังว่าง รู้แน่ว่าปัญหาอยู่ที่รูปร่าง JSON · ท่า 4 คือหลักฐาน ที่ให้คนอื่นตรวจงานได้โดยไม่ต้องเปิดโค้ด

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

เรียงจาก "ตรวจง่ายที่สุด" ไป "ตรวจยากที่สุด" แล้วความล้มเหลวจะบอกที่อยู่ของตัวเองเสมอ

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

คาบ 1-8 อ่านเซนเซอร์ วาดจอ ในบอร์ดของเราเอง คาบ 9-10 ต่อเน็ต แล้วส่งข้อมูล ออกไปให้เครื่องอื่นใช้ วันนี้ · คาบ 11 ช่องทางเข้ารหัส และรู้ว่าปลายทางเป็นตัวจริง คาบ 12 ประกอบทั้งหมด เป็นสินค้าหนึ่งชิ้น วันนี้คือจุดที่ "ระบบที่ใช้งานได้" กลายเป็น "ระบบที่ปล่อยออกนอกห้องได้"

คำถามคิดต่อ: ถ้ารหัส mqtt_pass ของทีมหลุดออกไป จะรู้ตัวได้อย่างไร และควรทำอะไรเป็นอย่างแรก · ใครควรเป็นคนตัดสินใจว่าอุปกรณ์ตัวไหนถูกถอนสิทธิ์ ระหว่างคนดูแลแพลตฟอร์มกับคนเขียนเฟิร์มแวร์ · ถ้ามีอุปกรณ์ 10,000 ตัวและแต่ละตัวต้องมีตัวตนไม่ซ้ำกัน ขั้นตอนการ provision ที่โรงงานควรหน้าตาเป็นอย่างไร

ใช้จริงที่ไหน — สี่มุมที่ TLS ทำงานอยู่ตอนนี้

โรงพยาบาล · ข้อมูลที่กฎหมายคุ้มครอง เครื่องวัดสัญญาณชีพส่งค่าผ่านช่องที่เข้ารหัสเท่านั้น และต้องพิสูจน์ได้ว่าค่ามาจากเครื่องไหน เตียงไหน ระดับนี้ใช้ mTLS ไม่ใช่ serverTLS อย่างที่เราทำวันนี้ โรงงาน · เครื่องจักรที่สั่งงานได้จากไกล คำสั่งหยุดเครื่องต้องมาจากคนที่มีสิทธิ์เท่านั้น ปลอมคำสั่งได้ = อันตรายต่อความปลอดภัยของคน การเข้ารหัสอย่างเดียวไม่พอ ต้องพิสูจน์ตัวตนด้วย พลังงาน · มิเตอร์ที่ติดตั้งนอกบ้านคน อุปกรณ์อยู่ในมือคนอื่น จับต้องได้ แกะได้ รหัสในไฟล์จึงไม่พอ ต้องเป็นกุญแจในชิปที่ดึงออกไม่ได้ นี่คือเหตุผลที่ secure element มีอยู่บนโลก สินค้าผู้บริโภค · อัปเดตเฟิร์มแวร์ผ่านเน็ต ไฟล์อัปเดตต้องมีลายเซ็นที่อุปกรณ์ตรวจเองได้ ใช้กลไกเดียวกับใบรับรองที่เราเพิ่งเรียนวันนี้ TLS ปกป้องระหว่างทาง ลายเซ็นปกป้องตัวไฟล์เอง

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

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

Transport Layer Security (TLS) — Computerphile (Dr Mike Pound) · 15 นาที 33 วินาที · อังกฤษ — กรอบความคิดว่า TLS มีไว้ทำไม อธิบายสิ่งที่ต้องการปกป้องก่อนจะพูดถึงกลไก เหมาะดูก่อนคลิปถัดไป

TLS Handshake — EVERYTHING that happens when you visit an HTTPS website — Practical Networking · 27 นาที 58 วินาที · อังกฤษ — ทุกข้อความในการจับมือ พร้อม packet capture จริง ยาว ดูเป็นการบ้านหรือเลือกดูเฉพาะช่วง

อ่านและเล่นต่อสำหรับคนอยากรู้ลึก

คลิปเหล่านี้ไม่อยู่ในเกณฑ์ผ่าน แต่คนที่ดูจะออกแบบส่วนความปลอดภัยของ capstone คาบหน้าได้ลึกกว่าเพื่อน

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

1 · จับเวลาสองท่อ 1883 เทียบ 8884 ส่วนต่างหายไปกับอะไร 2 · ทำให้พังอย่างมีระบบ แก้ผิดทีละหนึ่งค่า ทำตารางอาการของทีมเอง 3 · ตัวตนที่มาจากชิป optiga.uid() · random() ทำไม uid ยังไม่พอ 4 · schema ของงานจริง ส่งอะไร ถี่แค่ไหน คำนวณข้อมูลต่อวัน

ข้อ 1 · จับเวลาสองท่อ — วัดด้วย time.ticks_ms() ว่าจากสั่ง connect() จนถึง is_connected() เป็น True ใช้เวลากี่มิลลิวินาที ทำสามรอบแล้วหาค่ากลาง เทียบกับเวลาที่ mqtt.connect() ของคาบ 10 ใช้ แล้วอธิบายว่าส่วนต่างนั้นหายไปกับอะไรบ้าง

ข้อ 2 · ทดลองทำให้พังอย่างมีระบบ — แก้ค่าให้ผิด ทีละหนึ่งตัว: device_id ผิดหนึ่งตัวอักษร / mqtt_pass ผิด / sni_hostname ผิด — จดว่าแต่ละแบบให้อาการต่างกันอย่างไร แล้วทำตารางอาการ → สาเหตุของทีมเอง

ข้อ 3 · ตัวตนที่มาจากชิป — เปิด REPL เรียก optiga.uid(), optiga.random(16) สามครั้ง และ optiga.sha256(b"...") จดผลไว้ แล้วตอบว่าทำไมเลขสุ่มจากชิปถึงเชื่อถือได้มากกว่าเลขสุ่มจากสูตรในซอฟต์แวร์ และทำไม uid() ถึงยังไม่พอที่จะเป็นตัวตนสำหรับ mTLS (บน Dev Kit ให้เรียก uid() ก่อนเป็นข้อแรก — ถ้าบอร์ดนั้นไม่มีชิป ข้อนี้ทำไม่ได้ และห้ามแตะ tesaiot.protected_update() ในข้อนี้เด็ดขาด)

ข้อ 4 · ออกแบบ schema ของงานจริง — สมมติทีมต้องส่ง telemetry ของเครื่องจักรจริงหนึ่งเครื่อง ออกแบบว่าจะส่งฟิลด์อะไรบ้าง ถี่แค่ไหน ฟิลด์ไหนควรส่งเฉพาะตอนผิดปกติ พร้อมคำนวณปริมาณข้อมูลต่อวัน

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

ปิดวงจร — ค่าที่วัดได้จริง ออกไปแบบที่คนกลางอ่านไม่ได้

หกไฟล์ที่ผ่านมาสอนกลไก TLS ด้วยค่าที่แต่งขึ้น เพื่อให้เห็นการจับมือชัด ๆ examples/s11/07_real_reading_over_tls.py↗ ปิดวง — ค่าที่ออกไปคือค่าที่ชิปบนบอร์ดวัดได้จริง · ค่านั้นคืออุณหภูมิ อ่านผ่าน read_temp() ในไฟล์: บน Dev Kit มาจาก SHT40 จริง ส่วน บน Eva ไม่มีเซนเซอร์อุณหภูมิ ไฟล์จึงให้ลูกบิดเล่นบทแทน (0–100 % = 15–45 °C) และบอกไว้บน console ทั้งสองกรณีว่าค่ามาจากไหน — sensors.snapshot() ไม่มีช่องอุณหภูมิบนบอร์ดไหนเลย

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

นี่คือเหตุผลที่คาบ 5 กับ 6 ต้องมาก่อนคาบนี้ ไม่ใช่มาทีหลัง — ความน่าเชื่อถือของระบบเริ่มที่เซนเซอร์ ไม่ได้เริ่มที่ใบรับรอง

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

ลองบนโต๊ะ: เปิด MQTT Explorer ที่ปลายทาง เทียบว่าเลขที่เห็นตรงกับเลขบนจอบอร์ดไหม

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

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

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

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

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

มาตรฐานและเอกสารโพรโทคอล

วิดีโอ

ภาพ (ทุกไฟล์เก็บไว้ใน slides/img/ ไม่ได้ลิงก์ข้ามเว็บ)

  • Wikimedia Commons — สาธารณสมบัติ: s11_keypair_encrypt.svg, s11_signature_verify.svg (Davidgothberg) · s11_hash_function.svg (Jorge Stolfi ต่อยอดจาก Helix84) · s11_tls12_handshake.svg, s11_tls13_handshake.svg (Fleshgrinder และ The Tango! Desktop Project)
  • Wikimedia Commons — CC BY-SA 4.0: s11_chain_of_trust.svg (Yuhkih) · s11_mitm_tls_inspection.svg (Rudolf.Achter) · s11_mqtt_broker_tls.svg (Ademant) · s11_secure_element.jpg (Raimond Spekking) — CC BY 3.0: s11_mutual_auth.svg (Essich)
  • s11_tls12_handshake_th.svg — วาดเองสำหรับหลักสูตรนี้ ดัดแปลงลำดับเวลาจาก Full TLS 1.2 Handshake (สาธารณสมบัติ) แปลป้ายเป็นไทยและเปลี่ยนปลายทางเป็น EMQX
  • s11_ce_dashboard.png, s11_ce_devices.png — ภาพหน้าจอ TESAIoT Community Edition v1.1.8, docs/images/screenshots/ (Apache-2.0)

ข้อเท็จจริงของเฟิร์มแวร์และแพลตฟอร์ม

พอร์ต 8884 ถูกเลือกจาก tls_mode ไม่ใช่จากคีย์ port · root CA ถูกคอมไพล์เข้าเฟิร์มแวร์และเปลี่ยนตอนรันไม่ได้ · tesaiot.connect() เป็น API แบบ asynchronous ต้องรอด้วย is_connected() · device_id ยาวได้ 31 ตัวอักษรฝั่งบอร์ด — ตรวจจาก modtesaiot.c ใน BENTO-TESAIoT-libraries ซึ่งเป็นโค้ดร่วมของทั้ง Eva Kit และ TESAIoT Dev Kit (นับชื่อได้ 28 จากตาราง globals) · protected_update() ทำงานจริงเฉพาะบิลด์ที่ ENABLE_OPTIGA_CLM=1 — Dev Kit ตั้งค่านี้ไว้ ส่วน Eva ตั้ง 0 (proj_cm33_ns/Makefile ของแต่ละโปรเจกต์) · สิบหกชื่อที่ข้ามคอร์ตายทั้งสองบอร์ดเพราะ proj_cm55/Makefile ของทั้งคู่ตั้ง ENABLE_OPTIGA ?= 0 · ข้อเท็จจริงที่ว่า TESAIoT CE สร้าง root CA ใหม่แบบสุ่มทุกการติดตั้ง (scripts/init-vault-pki.sh) จึงใช้ MQTTs กับ CE ที่ self-host ไม่ได้จนกว่าจะ build เฟิร์มแวร์ใหม่ — ตรวจจาก repo tesaiot/tesaiot-community-edition เมื่อ 2026-08-12

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

fit-css

VIDEO-SLOT: คลิป 60-90 วินาที ถ่ายสองจอพร้อมกัน — จอซ้ายรัน Wireshark กรอง tcp.port==1883 บนเครือข่ายห้องเรียน ให้เห็นแพ็กเก็ต CONNECT ของบอร์ดคาบที่แล้ว แล้วซูมช่อง username และ password ที่อ่านออกเป็นตัวอักษรชัด ๆ พร้อม payload JSON ของเซนเซอร์ จากนั้นสลับ filter เป็น tcp.port==8884 แล้วชี้ให้เห็นว่าเหลือแต่ Application Data ที่เป็นไบต์มั่ว ๆ ปิดท้ายด้วยการชี้ว่าเป็นบอร์ดตัวเดิม โค้ดเกือบเดิม ต่างกันแค่โมดูลที่เรียก

ที่ต้องบอกกันไว้ก่อน เพราะถ้าไม่บอก น้อง ๆ จะไปเจอเองตอนนั่งดีบักแล้วเข้าใจว่าตัวเองเขียนผิด ทั้งที่ไม่ได้ผิดเลย · แหล่งที่ดู: ipc_service.c handle_tesaiot() ตอบ status 0xFD เมื่อไม่มี ENABLE_OPTIGA · modtesaiot.c tesaiot_ipc_send() รอสูงสุด TESAIOT_IPC_RESPONSE_TIMEOUT_MS = 10000 เฉพาะเมื่อไม่มีคำตอบ และ wrapper แต่ละตัว mp_raise_msg(OSError, "... failed")

VIDEO-SLOT: คลิป 30-40 วินาที ถ่ายตอนผ่าน MVP — เริ่มที่จอบอร์ดที่ตัวนับเดินขึ้นสามครั้งห่างกัน 5 วินาที พร้อมบรรทัด tls_mode แล้วแพนไปจอคอมที่หน้า dashboard ของแพลตฟอร์มโชว์กราฟของ device_id ทีมนั้นกำลังขยับ จากนั้นให้ผู้เรียนหยิบบอร์ดขึ้นมาเอียง แล้วชี้ให้เห็นว่าเส้นกราฟขยับตามภายในไม่กี่วินาที ใช้เป็นภาพมาตรฐานของคำว่าผ่านให้ทุกทีมเทียบ

ระหว่างรอ แถบเดินขึ้นทุกครึ่งวินาที ตัวเลขเขียนใหม่แค่ตอนวินาทีเปลี่ยน — คนที่ยืนรอต้องเห็นว่าโปรแกรมยังทำงาน

☰ สารบัญ