| คำถาม | คำตอบของคาบนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| 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 เราทำให้ ส่งได้ · คาบนี้เราทำให้คนอื่นแอบฟังไม่ได้ และรู้ด้วยว่ากำลังส่งให้ใคร
tesaiot.config_set() / connect() / is_connected() / publish() ได้ถูกต้องตามข้อจำกัดจริงของเฟิร์มแวร์ปลายทางของวันนี้: กราฟของ device_id ทีมเราขยับอยู่บน dashboard ของแพลตฟอร์ม โดยข้อมูลเดินผ่านช่องที่เข้ารหัสตลอดทาง
คาบนี้โค้ดสั้นที่สุดในตอนที่ 4 แต่เป็นคาบที่ ความเข้าใจผิดแพงที่สุด

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

บอร์ดทุกตัวออกจากโรงงานมาพร้อม device_id ค่าเริ่มต้นตัวเดียวกันหมด และ MQTT บังคับว่า client id ต้องไม่ซ้ำกันบน broker เดียวกัน
พอบอร์ดตัวที่สองต่อเข้ามาด้วย id เดิม broker จะ เตะตัวแรกออก ตัวแรกต่อใหม่แล้วเตะตัวที่สองออก วนแบบนี้ไปทั้งห้อง โดยที่โค้ดของทุกทีม ถูกต้องหมด
ผู้สอน provision ตัวตนรายทีมให้ก่อนคาบ ทีมต้องได้ครบสี่ค่า: device_id · api_key · mqtt_pass · ชื่อโฮสต์ของ broker — กรอกลงกล่องข้อ 4.1 ในใบงานก่อนแตะโค้ด
ทีมที่ยังไม่ได้ค่าครบสี่ตัว ห้ามเริ่มท่าที่ 2 — ไม่ใช่กฎห้องเรียน แต่เป็นเพราะมันจะทำให้ทั้งห้องต่อไม่ติดพร้อมกัน
ซ้าย — กุญแจคู่ ล็อกด้วยกุญแจสาธารณะของ Alice แล้ว มีแต่กุญแจส่วนตัวของ Alice ที่เปิดได้ จึงส่งความลับให้คนที่ไม่เคยเจอกันได้ โดยไม่ต้องนัดรหัสกันก่อน
กลาง — ลายเซ็น คือการกลับทิศ เซ็นด้วยกุญแจส่วนตัว ใครก็ตรวจได้ด้วยกุญแจสาธารณะ — พิสูจน์ว่า "คนที่ถือกุญแจส่วนตัวใบนี้เป็นผู้เขียน" นี่คือกลไกที่ CA ใช้รับรองใบรับรอง
ขวา — แฮช ข้อความเปลี่ยนแค่ตัวอักษรเดียว ค่าที่ได้เปลี่ยนทั้งก้อน จึงใช้ย่อเอกสารยาว ๆ ให้เหลือค่าเดียวก่อนเซ็น และใช้ตรวจว่าข้อมูลระหว่างทางถูกแก้หรือไม่
จำสามคำนี้ให้แม่น: เข้ารหัส = ปิดไม่ให้อ่าน · เซ็น = พิสูจน์ว่าใครเขียน · แฮช = จับได้ว่าถูกแก้ TLS ใช้ทั้งสามพร้อมกันเสมอ

สิ่งที่ทำให้ใบรับรองมีค่าไม่ใช่เนื้อหาข้างใน (ใครก็พิมพ์ได้) แต่คือ ลายเซ็นของ CA ที่อยู่ท้ายใบ — และเราตรวจลายเซ็นนั้นได้ก็ต่อเมื่อ มีกุญแจสาธารณะของ CA อยู่ในมือแล้วตั้งแต่ต้น
ประโยคที่ควรจำไปใช้ทำงาน: ใบรับรองแปลว่า "มีคนที่คุณเชื่ออยู่แล้ว ยืนยันว่ากุญแจนี้เป็นของชื่อนี้" ไม่มากกว่านั้นแม้แต่นิดเดียว
ซ้าย — ใบของเซิร์ฟเวอร์ (leaf) ถูกเซ็นโดย intermediate, intermediate ถูกเซ็นโดย root และ root เซ็นตัวเอง ห่วงโซ่จบตรงนั้นเสมอ เพราะ root คือสิ่งที่เรา "ตัดสินใจเชื่อ" ไว้ล่วงหน้า ไม่ใช่สิ่งที่พิสูจน์ได้ · ขวา — ถ้ามีกล่องกลางทางที่เรา (หรือผู้ดูแลเครือข่าย) ใส่ CA ของมันไว้ในเครื่อง มันจะออกใบรับรองชื่อเดียวกันได้ และเราจะเชื่อโดยไม่รู้ตัว — นั่นคือเหตุผลว่าทำไม รายชื่อ CA ที่เชื่อ ถึงสำคัญพอ ๆ กับตัวการเข้ารหัส
PKI Bootcamp — Basics of Certificate Chain Validation — Paul Turner · 3 นาที 42 วินาที · อังกฤษ — ตอบคำถาม "ทำไมบอร์ดต้องมี root CA ติดตัว" ได้ครบใน 4 นาที
ความเชื่อไม่ได้เกิดจากการพิสูจน์ทั้งเส้น มันเกิดจาก จุดเริ่มต้นที่เราเลือกเชื่อไว้ก่อน แล้วพิสูจน์ต่อจากจุดนั้นลงมา
พื้นฐาน 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.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_hostname ในโค้ดวันนี้ และเป็นเหตุผลที่มันต้อง เท่ากับชื่อ broker เป๊ะ ๆ
ตั้ง
sni_hostnameผิด อาการที่ได้คือ "ต่อไม่ติด โดยไม่มีข้อความอะไรเลย" — ไม่ใช่ error ที่บอกว่าชื่อผิด จำอาการนี้ไว้ตั้งแต่ตอนนี้
การเข้ารหัสทั้งหมดเกิดบน CM33_NS คอร์เดียวกับที่รันโค้ด Python ทุกไบต์ที่ tesaiot.publish() ส่งออกไปจะถูกเข้ารหัสก่อนลงสายอากาศ ส่วน CM55 ที่วาดจอไม่รู้เรื่องด้วยเลย — ภาพนี้จริงทั้งสองบอร์ด เพราะโค้ด Wi-Fi/MQTT/TLS เป็นชุดเดียวกัน ต่างกันแค่ว่าใครอ่าน IMU: บน Eva คอร์จออ่านให้ บน Dev Kit CM33 อ่านเองจาก I2C (CapSense กับลูกบิดยังมาจากคอร์จอทั้งคู่)
ข้อที่ต้องจำ: ช่วงจับมือคือช่วงที่กินหน่วยความจำและเวลามากที่สุด ถ้าสคริปต์ของเราสร้าง widget เพียบหรือเก็บลิสต์ใหญ่ ๆ ไว้ก่อนเรียก connect() โอกาสล้มจะสูงขึ้นทันที — ต่อให้เน็ตดีทุกอย่าง
เรียก
tesaiot.connect()ตอนต้นสคริปต์ ตอนหน่วยความจำยังโล่ง แล้วค่อยไปทำอย่างอื่น อย่าเรียกกลางลูปที่ของเต็มมือ
tls_mode ไม่ใช่คีย์ portนี่คือรูปแบบที่จะเจอไปทั้งชีวิตการทำงาน: ค่าที่ตั้งได้ ไม่ได้แปลว่าค่านั้นมีผล และ ค่าที่จอแสดง ไม่ได้แปลว่าระบบใช้ค่านั้น ในใบงานข้อ 5.3 น้อง ๆ จะได้ลองตั้ง port ให้ผิดแล้วดูเองว่าพอร์ตจริงไม่ขยับ · "จอ" ในกล่องล่างขวาคือการ์ด TESAIoT Connectivity ซึ่งมีบนหน้า Home ของ Eva Kit เท่านั้น — บน Dev Kit ไม่มีการ์ดนี้ ให้ดูจาก print(tesaiot.config()) แทน ผลเหมือนกันทุกประการ: คีย์ port เปลี่ยน แต่พอร์ตที่ต่อจริงไม่เปลี่ยน
ถ้าอยากรู้ว่าระบบใช้พอร์ตอะไรจริง ๆ ให้ดู
tls_mode— และถ้าอยากรู้ว่า API ตัวไหนหลอกเรา ให้ วัดผลที่ปลายทาง ไม่ใช่อ่านค่าที่ตัวเองเพิ่งตั้ง
คาบ 10 ใช้ MQTT ธรรมดา (1883) ยิงเข้า CE ที่ทีมติดตั้งเอง — ได้จริง · คาบ 11 ใช้ MQTTs (8884) ยิงเข้าแพลตฟอร์ม TESAIoT อย่างเป็นทางการ ที่เฟิร์มแวร์ฝัง CA ไว้ตรงกัน — ได้จริง แต่ การเอา MQTTs ไปยิง CE ที่ self-host ทำไม่ได้ในวันนี้ เพราะสคริปต์ติดตั้งของ CE สร้าง root CA ใหม่แบบสุ่มทุกครั้ง ใบรับรองของ broker จึงห้อยจาก root ที่บอร์ดไม่มีทางรู้จัก
จะทำให้ได้ต้องเอา CA ของ CE ชุดนั้น ใส่กลับเข้าไปในซอร์สแล้ว build เฟิร์มแวร์ใหม่ทุกบอร์ด ต่อการติดตั้งหนึ่งชุด — เป็นงานที่ทำได้ แต่ไม่ใช่งานของคาบนี้
ถ้ามีทีมไหนลองแล้วต่อไม่ติด ไม่ต้องดีบักโค้ด — มันไม่ใช่โค้ดของทีม มันคือกุญแจที่ไม่ตรงรู กลับไปใช้โฮสต์ที่ผู้สอนให้มา
สิ่งที่วันนี้ทำได้จริง
สิ่งที่วันนี้ยัง ไม่ ได้ทำ
device_id กับ mqtt_pass ที่ถูกต้องประโยคที่ต้องตอบได้โดยไม่เปิดสไลด์: serverTLS พิสูจน์ช่องทางและพิสูจน์เซิร์ฟเวอร์ — ไม่ได้พิสูจน์อุปกรณ์ ตัวตนอุปกรณ์วันนี้มาจากรหัสผ่าน ซึ่งเป็นความลับที่ถูกก๊อปได้
| คาบ 10 · 1883 | วันนี้ · 8884 | |
|---|---|---|
| การเข้ารหัส | ไม่มีเลย | TLS 1.2 |
| ตัวตนของ broker | ไม่มีการพิสูจน์ | ใบรับรองที่ CA เซ็น + ตรวจ SNI |
| ตัวตนของอุปกรณ์ | client_id ที่ใครก็อ้างได้ | credentials ที่ provision รายทีม |
| ปลายทาง | CE ที่ทีมติดตั้งเอง | แพลตฟอร์ม TESAIoT |
| โมดูล | mqtt |
tesaiot |
พอร์ต 1883 ไม่ได้ "ปลอดภัยน้อยกว่า" — มันไม่มีความปลอดภัยเลย ทั้งเรื่องเนื้อหาและเรื่องตัวตน สิ่งที่ 8884 เพิ่มเข้ามาคือสองอย่างพร้อมกัน: การเข้ารหัส และการรู้ว่ากำลังคุยกับใคร
อย่าท่องตารางนี้ — ให้ ดักจับเอง แล้วกรอกข้อ 4.3 ในใบงานจากสิ่งที่เห็นด้วยตา นั่นคือหลักฐานที่ใช้ได้จริง
สิ่งที่ทำให้แล้ว (70%)
การจับมือ TLS ทั้งหมด · การตรวจใบรับรองย้อนขึ้นไปถึง root · root CA ที่ฝังมากับเฟิร์มแวร์ · การเลือกพอร์ตจาก tls_mode · การประกอบ topic จาก device_id · ฝั่งแพลตฟอร์ม: broker EMQX, บริดจ์ที่รอ subscribe อยู่แล้ว, ฐานข้อมูลอนุกรมเวลา และกราฟที่สร้างจากชื่อคีย์ JSON อัตโนมัติ
สิ่งที่เป็นงานของเรา (30%)
ตั้งค่าตัวตนของอุปกรณ์ให้ถูกทั้งสี่ค่า · รอให้การเชื่อมต่อเสร็จจริงก่อนส่ง · เลือกว่าจะส่งฟิลด์อะไรเป็นตัวเลข · และแสดงหลักฐานบนจอให้คนอื่นตรวจได้โดยไม่ต้องเปิดโค้ด
ยิ่งไลบรารีทำให้เยอะ ความผิดพลาดที่เหลือยิ่ง เงียบ — เพราะสิ่งที่เหลือให้เราพลาดคือเรื่องที่ไลบรารีไม่มีทางรู้ว่าเราตั้งใจอะไร
import tesaiot ได้มา 28 ชื่อ — ใช้ได้จริงเก้าตัว ทั้ง Eva Kit และ Dev Kitพูดให้ตรง: ทั้งสองบอร์ดประกอบเฟิร์มแวร์ของคอร์จอด้วย 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: ตั้งค่าตัวตนของอุปกรณ์ ---
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 ตัวอักษร เกินแล้วถูกตัดเงียบ แล้วแพลตฟอร์มจะหาอุปกรณ์ไม่เจอ
sni_hostnameไม่ใช่ค่าเสริม — มันคือชื่อที่ทำให้เซิร์ฟเวอร์ หยิบใบรับรองใบที่ถูกมายื่นให้เรา ตั้งไม่ตรงเมื่อไร การจับมือล้มโดยไม่มีข้อความบอก
# --- 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)
tesaiot.connect() เป็น API แบบ asynchronous คืนค่าทันทีเพื่อไม่บล็อกโปรแกรม — ค่าที่คืนมา ไม่ใช่สถานะสุดท้าย สิ่งเดียวที่ตอบได้ว่าต่อเสร็จหรือยังคือ tesaiot.is_connected() · ใบงานข้อ 5.2 ให้ลบลูปนี้ออกแล้วรันหนึ่งรอบ — จะเห็นด้วยตาว่า publish ที่ยิงเร็วเกินไป ไม่ error และไม่ถึงแพลตฟอร์ม
"ฟังก์ชันคืนค่าแล้ว" กับ "งานเสร็จแล้ว" เป็นคนละเรื่องเสมอสำหรับ API แบบ async — ความต่างนั้นวัดได้เป็นวินาที
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")
tesaiot.publish(payload) รับ ตัวข้อมูลอย่างเดียว — ต่างจาก mqtt.publish(topic, payload) ของคาบที่แล้ว เพราะเฟิร์มแวร์ประกอบ topic ให้เองจาก device_id ที่เราตั้งไว้ในท่าที่ 1 (จะระบุ topic เองก็ได้ แต่คาบนี้ไม่ต้อง) · ส่ง ตัวเลขจริง ไม่ใช่สตริง ไม่งั้น dashboard จะขึ้นค่าแต่วาดกราฟไม่ได้ — กับดักเดียวกับคาบ 10
จอบอร์ดต้องตอบคำถาม MVP ได้เอง: ทีมไหน · โหมดอะไร · ส่งไปกี่ครั้งแล้ว สามอย่างนี้ทำให้คนอื่นตรวจงานเราได้โดยไม่ต้องถาม
ระบบยาวหกกล่อง แต่การดีบักไม่เคยยาวกว่า "หาให้เจอว่ากล่องสุดท้ายที่ยังเห็นข้อมูลคือกล่องไหน" — คาบนี้กล่องแรกที่ต้องตรวจคือ
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() ล้างของจริง ส่วน 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↗ ปิดแล้วเปิดใหม่
wifi.connect() ของคาบ 9 บล็อกได้นานถึงราว 85 วินาทีถ้ารหัสผิด)practise_codes/s11_secure_telemetry.py↗ แก้ห้าบรรทัดบนหัวไฟล์ให้เป็นค่าของทีมprint(tesaiot.config()) ขึ้นค่าครบและสะกดถูก ก่อน ไปท่าที่ 2is_connected() จึงเป็น True แล้วจดลงใบงานข้อ 4.2ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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 อยู่ตรงกลาง จุดที่พังได้มีมากกว่าคาบที่แล้ว และไม่มีจุดไหนส่งเสียงเลย

telemetry ของทีมขึ้น dashboard แพลตฟอร์มผ่าน TLS + ตอบได้ว่าต่างจากคาบ 10 ตรงไหน
device_id ทีมนั้น ขยับ บน dashboard ของแพลตฟอร์มจริง และขยับตามเมื่อเอียงบอร์ดdevice_id + โหมด tls_mode + ตัวนับที่เดินขึ้นต่อเนื่องdevice_id ไม่ซ้ำทีมอื่น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" แปลว่าเสียหายไปแล้ว
เปิด 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 ทั้งท่า

บน 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
วันนี้เราได้: ส่ง 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
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 มาก่อนเพราะตรวจได้โดยไม่ต้องใช้เน็ต — print(config()) บอกทันทีว่าค่าเข้าครบไหม · ท่า 2 แยกเป็นท่าของตัวเอง เพราะ "ต่อเสร็จ" เกิดทีหลังคำสั่ง ไม่ใช่ผลของคำสั่ง · ท่า 3 ส่งข้อมูลจริง เมื่อสองท่าแรกยืนยันแล้ว ถ้ากราฟยังว่าง รู้แน่ว่าปัญหาอยู่ที่รูปร่าง JSON · ท่า 4 คือหลักฐาน ที่ให้คนอื่นตรวจงานได้โดยไม่ต้องเปิดโค้ด
ท่า 5 คือปุ่มบนจอ — ตัดสาย เป็นคำสั่งที่ถอยกลับไม่ได้ทันที เพราะต้องจับมือ TLS ใหม่ทั้งชุด มันจึงมีกล่องยืนยันที่บอก สิ่งที่จะเกิด ไม่ใช่ถามลอย ๆ ว่า "แน่ใจไหม" ส่วน ต่อใหม่ ไม่ต้องยืนยัน เพราะถ้ากดพลาดก็แค่ต่อซ้ำ — ระดับของการยืนยัน มาจากราคาของความผิดพลาด ไม่ได้มาจากความสำคัญของปุ่ม
เรียงจาก "ตรวจง่ายที่สุด" ไป "ตรวจยากที่สุด" แล้วความล้มเหลวจะบอกที่อยู่ของตัวเองเสมอ
คำถามคิดต่อ: ถ้ารหัส mqtt_pass ของทีมหลุดออกไป จะรู้ตัวได้อย่างไร และควรทำอะไรเป็นอย่างแรก · ใครควรเป็นคนตัดสินใจว่าอุปกรณ์ตัวไหนถูกถอนสิทธิ์ ระหว่างคนดูแลแพลตฟอร์มกับคนเขียนเฟิร์มแวร์ · ถ้ามีอุปกรณ์ 10,000 ตัวและแต่ละตัวต้องมีตัวตนไม่ซ้ำกัน ขั้นตอนการ provision ที่โรงงานควรหน้าตาเป็นอย่างไร
สี่มุมนี้ใช้ชิ้นส่วนชุดเดียวกับที่เราเพิ่งเรียน ต่างกันที่ ตัวตนของอุปกรณ์มาจากไหน — ไฟล์ข้อความ หรือชิปที่ก๊อปไม่ได้
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 · จับเวลาสองท่อ — วัดด้วย 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 ต้องมาก่อนคาบนี้ ไม่ใช่มาทีหลัง — ความน่าเชื่อถือของระบบเริ่มที่เซนเซอร์ ไม่ได้เริ่มที่ใบรับรอง

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


มาตรฐานและเอกสารโพรโทคอล
วิดีโอ
ภาพ (ทุกไฟล์เก็บไว้ใน slides/img/ ไม่ได้ลิงก์ข้ามเว็บ)
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)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 (สาธารณสมบัติ) แปลป้ายเป็นไทยและเปลี่ยนปลายทางเป็น EMQXs11_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 ทีมนั้นกำลังขยับ จากนั้นให้ผู้เรียนหยิบบอร์ดขึ้นมาเอียง แล้วชี้ให้เห็นว่าเส้นกราฟขยับตามภายในไม่กี่วินาที ใช้เป็นภาพมาตรฐานของคำว่าผ่านให้ทุกทีมเทียบ
ระหว่างรอ แถบเดินขึ้นทุกครึ่งวินาที ตัวเลขเขียนใหม่แค่ตอนวินาทีเปลี่ยน — คนที่ยืนรอต้องเห็นว่าโปรแกรมยังทำงาน