ทั้งไฟล์อ่านเป็นประโยคเดียว: "เตรียมหัวตาราง → รอกดปุ่ม label → อัด 200 sample ที่ 50 Hz ลงไฟล์ → นับ → ออกให้เรียบร้อย"
ตัวเลขบรรทัด (
:55,:69,:73,:76) ชี้ไปที่ 4 จุดที่คุณต้องเติมโค้ดในไฟล์ฝึก — ตรงกับสี่จังหวะ DAQ เป๊ะ
ช่องเติมที่ 1: บรรทัดแรกของ CSV คือหัวตาราง เขียนครั้งเดียวถ้าไฟล์ยังไม่มี
try:
open(PATH, "r").close() # เปิดอ่านได้ = ไฟล์มีอยู่แล้ว ข้ามไป
except OSError:
with open(PATH, "w") as f: # ยังไม่มีไฟล์ -> สร้างพร้อมหัวตาราง
# เติม: เขียนหัวตาราง (schema)
# f.write("label,ax,ay,az,gx,gy,gz\n")
pass
pass ด้วย f.write("label,ax,ay,az,gx,gy,gz\n") — สังเกต \n ปิดท้ายบรรทัดโหมด
"w"ใช้ตรงนี้ได้เพราะเรารู้แน่ว่าไฟล์ยังไม่มี (อยู่ในexcept OSError) — ที่อื่นเราใช้"a"เสมอ เพื่อไม่ทับของเก่า
ช่องเติมที่ 2: ในลูป record() อ่าน IMU หนึ่ง snapshot ครบ 6 แกน
with open(PATH, "a") as f:
for _ in range(BURST):
# เติม: อ่าน IMU หนึ่ง snapshot ครบ 6 แกน
# ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
ax = ay = az = gx = gy = gz = 0.0
pass
pass ด้วย ax, ay, az, gx, gy, gz = sensors.bmi270.motion() (บรรทัด = 0.0 มีไว้กัน error ตอนยังไม่เติม ลบทิ้งได้หรือปล่อยไว้ก็ได้เพราะจะถูกทับ)motion() คืน tuple 6 ค่า เรารับด้วยการ unpack เข้า 6 ตัวแปรพร้อมกัน0.0 หมด — dataset มีแต่ศูนย์ โมเดลเรียนอะไรไม่ได้ลองเดาก่อน: ถ้าเราวางบอร์ดนิ่งแล้วอ่าน
azจะได้ประมาณเท่าไร? (ใบ้: แรงโน้มถ่วง ~9.8) — ทดสอบใน REPL ได้เลยsensors.bmi270.motion()
ช่องเติมที่ 3 และ 4: เขียน sample เป็นหนึ่งบรรทัด แล้วเว้นจังหวะให้อัตราคงที่
# เติม: เขียน sample นี้เป็นหนึ่งบรรทัด (เรียงคอลัมน์ให้ตรง schema)
# f.write("%s,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f\n"
# % (label, ax, ay, az, gx, gy, gz))
pass
# เติม: เว้นจังหวะให้คงที่ คุมอัตราสุ่มให้ ~50 Hz
# time.sleep_ms(RATE_MS)
pass
f.write("%s,%.4f,...\n" % (label, ax, ay, az, gx, gy, gz)) — %s สำหรับ label, %.4f เก็บทศนิยม 4 ตำแหน่ง, ปิดด้วย \ntime.sleep_ms(RATE_MS) — นี่คือหัวใจที่ทำให้ได้ 50 Hz ถ้าลบทิ้งจะสุ่มเร็วสุดกำลัง อัตราจะไม่ตรงกับที่โมเดลกินBURST รอบเรียงคอลัมน์ต้องตรง schema เป๊ะ:
labelก่อน แล้วax,ay,az,gx,gy,gz— ถ้าสลับ ตอน train ข้อมูลจะเข้าคอลัมน์ผิดโดยไม่มี error เตือน (บั๊กเงียบที่อันตรายสุด)
หลังอัดครบหนึ่ง burst เราอัปเดตตัวนับบนจอ แล้วตอนออกก็สรุปให้เรียบร้อย (สองส่วนนี้ให้ไว้แล้ว)
total += BURST
count_lbl.text("บันทึกแล้ว: %d samples" % total)
...
finally:
lcd.console(" DAQ done: %d samples total in %s" % (total, PATH))
total นับ sample สะสมทั้งหมด โชว์บนจอเพื่อให้เห็น dataset โตขึ้นทุกครั้งที่กดfinally ให้ไว้แล้ว: ไม่ว่าจะออกด้วยปุ่ม back หรือ error ก็สรุปยอดเสมอ (ไฟล์ถูกปิด+flush ไปแล้วในแต่ละ with)ข้อดีของ
with open(...): แต่ละ burst ปิดไฟล์เอง ข้อมูล flush ลง flash ทันที ถ้าถอดสายกลางคัน อย่างน้อย burst ที่เขียนจบแล้วก็ปลอดภัย
time.sleep_ms(20) ไม่ได้แปลว่าได้ 50 Hz เป๊ะ เพราะแต่ละรอบยังมีเวลา อ่านเซนเซอร์ + เขียนไฟล์ บวกเพิ่ม
บทเรียน: อย่าเชื่อค่าที่ตั้ง จงวัดค่าที่ได้จริง — วิศวกร Edge AI วัดอัตราสุ่มจริงเสมอ เพราะมันกระทบว่า dataset จะตรงกับตอน deploy ไหม
เราตั้ง LABELS = ("idle", "circle", "shaking") ให้ ตรงกับคลาสของโมเดล Motion เป๊ะ — จะได้เทียบ dataset ของเรากับโมเดลสำเร็จรูปได้
LABELS = ("idle", "circle", "shaking") # ตรงกับคลาสโมเดล Motion จากบทเรียน 1.1–1.3
idle 3 burst แต่ shaking แค่ 1 burst โมเดลที่ train จากนี้จะเอนไปทาย idleนี่คือจุดที่ DAQ กลายเป็น "งานฝีมือ" — ไม่ใช่แค่กดปุ่มมั่ว แต่วางแผนว่าจะเก็บอะไร กี่ครั้ง ให้ครบและสมดุล เดี๋ยวบทเรียน 5.1–5.2 (Dataset engineering) เราจะจริงจังกับเรื่องนี้
ไฟล์ /gestures.csv ที่เราเก็บวันนี้ ไม่ได้จบในตัว มันคือ วัตถุดิบ ของโมดูล 5 (Training) ข้างหน้า
เก็บ dataset วันนี้ให้ดีๆ นะ เดี๋ยวโมดูล 5 (Training) คุณจะได้เอาไฟล์ของตัวเองไป train เป็นโมเดลของตัวเอง แล้วรันบน NPU จริง — เต็มวงจร
เก็บเสร็จอย่าเพิ่งเชื่อ ตรวจก่อนเสมอ ว่าไฟล์มีข้อมูลจริง หัวถูก จำนวนบรรทัดสมเหตุผล
# ที่ REPL บนบอร์ด: นับบรรทัด + ดู 3 บรรทัดแรก
f = open("/gestures.csv")
lines = f.readlines(); f.close()
print("จำนวนบรรทัด:", len(lines)) # ควร = 1 หัว + (จำนวน burst x 200)
for ln in lines[:3]:
print(ln.strip())
1 + 3*200 = 601 บรรทัด ถ้าน้อยกว่านี้แปลว่าบางอย่างพลาดlabel,ax,ay,az,gx,gy,gz เป๊ะ"ตรวจก่อนเชื่อ" เป็นนิสัยวิศวกรข้อมูล — dataset ที่ดูเผินๆ เหมือนโอเค แต่มี label ผิดปนอยู่ จะทำให้ train เสียเวลาเป็นวันโดยไม่รู้ตัว
โครง logger นี้ไม่ผูกกับ IMU มันคือ แม่แบบ เปลี่ยนแค่บรรทัด sample กับ schema ก็เก็บเซนเซอร์อื่นได้
| อยากเก็บ | เปลี่ยนบรรทัด sample เป็น | schema (หัวตาราง) |
|---|---|---|
| IMU (วันนี้) | sensors.bmi270.motion() |
label,ax,ay,az,gx,gy,gz |
| ความดัน/อุณหภูมิ | sensors.dps368.pressure_temperature() |
label,hpa,tempC |
| ความชื้น | sensors.sht40.temperature_humidity() |
label,tempC,rh |
| เรดาร์ (บอร์ดเท่านั้น) | sensors.radar() |
label,presence,energy |
RATE_MS สูงขึ้นได้ เช่น 500 (2 Hz) ก็พอ ไม่ต้อง 50 Hzลองเป็นการบ้าน: fork ไฟล์เป็น
s04_baro_logger.pyเก็บความดันทุก 1 วินาที แล้วยกบอร์ดขึ้น-ลง — คุณจะเห็น dataset ของสัญญาณช้าๆ ที่หน้าตาต่างจาก IMU สิ้นเชิง
ไม่มีบอร์ดก็เริ่มได้ Emulator จำลอง IMU ให้:
s04_daq_logger.py (หรือวางโค้ด)shaking แล้วใช้ตัวควบคุมของ Emulator จำลองการเขย่า ดู samples วิ่งขึ้น/gestures.csv ดูเนื้อในEmulator รองรับ IMU เต็มตัว เหมาะกับซ้อมเก็บ dataset ที่บ้าน — โค้ดชุดเดียวกับบอร์ดจริงเป๊ะ (สำหรับเสียง/เรดาร์ ต้องใช้บอร์ด เดี๋ยวบทเรียน 2.3–2.4 ว่ากัน)

จอ BENTO Emulator ขณะรันแดชบอร์ด IMU (ตัวอย่าง 01) — ค่าที่เห็นมาจากตัวจำลอง IMU ตัวเดียวกับที่ logger ของเราอ่าน พอรัน logger จอจะมีปุ่ม label และตัวนับ samples เพิ่มขึ้นมา
ax,ay,az,gx,gy,gz วิ่งตามที่เราขยับ/จำลองการเขย่า — เห็นข้อมูลก่อนมันลงไฟล์/gestures.csv ได้เหมือนกันเป๊ะเปิด ide.tesaiot.dev แล้วลองเขย่าดู ค่าบนจอต้องแกว่งตาม ถ้านิ่งสนิท แปลว่ายังไม่ได้เชื่อมตัวจำลอง IMU
บนบอร์ดจริงเราเก็บด้วยเซนเซอร์จริง ท่าจริง มือเราเอง:
s04_daq_logger.py ใน BENTO IDE กด Program to Deviceidle วางบอร์ดนิ่งๆ จนบันทึกครบ · กด circle วาดวงกลมในอากาศ · กด shaking เขย่า/gestures.csv (ใช้ REPL ตามสไลด์ก่อนหน้า)ข้อดีของบอร์ดจริง: ค่าที่ได้คือความเร่ง/ไจโรจริงจากการเคลื่อนไหวของคุณ — dataset นี้เอาไป train แล้วรันกลับบนบอร์ดเดิมได้เลย (บทเรียน 5.8–5.9) ครบวงจรของจริง
เปิด s04_daq_logger.py มี pass วางไว้ 4 จุด ตรงสี่จังหวะ DAQ เป๊ะ:
| # | จังหวะ | เติมด้วย | ถ้าลืม |
|---|---|---|---|
| 1 | schema | f.write("label,ax,ay,az,gx,gy,gz\n") |
ไฟล์ไม่มีหัว train อ่านผิด |
| 2 | sample | ax, ay, az, gx, gy, gz = sensors.bmi270.motion() |
ทุกบรรทัดเป็น 0.0 |
| 3 | record | f.write("%s,%.4f,...\n" % (label, ax, ...)) |
ไม่มีข้อมูลถูกเขียนลงไฟล์ |
| 4 | rate | time.sleep_ms(RATE_MS) |
สุ่มเร็วเกิน อัตราไม่ตรงโมเดล |
ขั้นตอน:
# เติม: ทีละจุด แล้วแทน pass ด้วยคำสั่งตามคำใบ้สี่ช่องนี้คือสี่จังหวะของ DAQ เป๊ะ — เติมครบเมื่อไร คุณได้ dataset ไฟล์แรกของตัวเอง
เก็บ dataset มีกับดักเงียบๆ ที่ไม่ขึ้น error แต่ทำ dataset เสีย ระวังสี่อย่างนี้:
"w" แทน "a" — เปิดแบบ "w" ทุกครั้งจะ ทับ ไฟล์ เก็บ 3 burst เหลือ burst เดียว (ต้องใช้ "a" append)shaking แต่วางนิ่ง ข้อมูลจะสอนโมเดลผิด (ฉบับเต็มมีปุ่ม Clear ล้างเริ่มใหม่)gx ก่อน ax ไม่ตรง schema เครื่อง train อ่านเข้าคอลัมน์ผิดโดยไม่เตือน\n — ไม่ปิดบรรทัด ทุก sample จะต่อกันเป็นบรรทัดเดียวยาวเหยียด อ่านไม่ออกกับดักพวกนี้ "เงียบ" — โปรแกรมรันผ่าน ไฟล์มีข้อมูล แต่ผิด นี่คือเหตุผลที่สไลด์ก่อนย้ำให้ เปิดไฟล์ตรวจเสมอ ก่อนเชื่อว่า dataset ใช้ได้
อยากเข้าใจ sampling theorem กับการเก็บข้อมูลให้ลึกกว่านี้ ลองตามลิงก์เหล่านี้:
วิดีโอ (ช่องการศึกษาที่น่าเชื่อถือ)
บทความ / ภาพอ้างอิง
วิดีโอ/ภาพภายนอกเป็นของเจ้าของต้นฉบับ ใช้เพื่อการศึกษา อ้างอิงลิงก์ต้นทาง — สไลด์นี้ให้ลิงก์ไว้เท่านั้น ไม่ได้ฝังตัววิดีโอหรือคัดลอกภาพมา
MVP ของบทเรียน 2.1–2.2 (เกณฑ์ผ่านของชุดบทเรียน): คุณเขียน logger ที่เก็บ N samples ที่ติด label ลง CSV ได้จริง — เลือก label, สุ่มที่อัตราคงที่, เขียนลงไฟล์บนบอร์ด, ตรวจได้ว่าไฟล์มีจำนวนบรรทัดถูกต้อง
schema / sample / record / rate ตรงไหน และทำไม 50 Hz"เก็บ dataset ได้" ไม่ใช่แค่ "เห็นตัวเลขวิ่ง" — คุณต้องเปิดไฟล์แล้วบอกได้ว่ากี่บรรทัด หัวถูกไหม แต่ละ label เก็บไปเท่าไร สมดุลหรือยัง
ถ้าติด ให้ไต่บันไดนี้ทีละขั้น อย่าเพิ่งกระโดดไปดูเฉลย เพราะของจะเข้าหัวตอนที่คุณพยายามเองก่อน:
# เติม: ทั้ง 4 จุดในไฟล์ฝึก + ตารางสี่จังหวะหน้าที่แล้ว บอกว่าแต่ละช่องเติมอะไรs04_daq_logger.py มีโครงครบทั้งไฟล์แล้ว เหลือแค่ 4 บรรทัดให้เติมs04_daq_logger.py เติมครบพร้อมคอมเมนต์อธิบายทุกช่อง (อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เอง)s04_daq_logger_full.py ฉบับขัดเรียบร้อย เพิ่มตัวนับแยก label (class balance) + วัดอัตราจริง Hz + ปุ่ม Clearอยากท้าทายเพิ่ม? ในไฟล์ example
s04_daq_logger.pyคือฉบับอ้างอิงที่ชุดบทเรียนนี้สร้างขึ้นรอบๆ — เปิดเทียบดูได้ว่าโครงเดียวกับที่คุณเติม
การเก็บ dataset ไฟล์แรกซ่อนแนวคิด DAQ หลายชั้นที่จะใช้ไปตลอดคอร์ส:
ฝั่ง DAQ / ข้อมูล
ฝั่ง MicroPython / โครงโปรแกรม
ui.poll (เหมือน โมดูล 1)open/write/with, โหมด "a" vs "w"sensors.bmi270.motion() 6 แกนใต้ lock เดียวทั้งหมดนี้คือ Pillar 1 ของวงจร — จากนี้เราจะไล่ขึ้น Processing (บทเรียน 3.1–3.2) → Analysis (บทเรียน 4.1–4.2) จนถึง Training (บทเรียน 5.1–5.2+) แล้วคุณจะได้เอาไฟล์ CSV วันนี้ไปสร้างเป็นโมเดลของตัวเอง
การเก็บข้อมูลเซนเซอร์ลงไฟล์ ไม่ใช่แบบฝึกหัด แต่เป็นงานที่ทำจริงในทุกสายที่มีเซนเซอร์:
logger ที่คุณเขียนวันนี้ เปลี่ยนเซนเซอร์ก็ใช้ได้กับทุกงานข้างบน — โครงสี่จังหวะ
schema/sample/record/rateเดิม ต่างแค่บรรทัดที่อ่านเซนเซอร์
งานทำเอง (ท้ายบทเรียน):
s04_daq_logger.py ให้ครบทั้ง 4 ช่อง เก็บ dataset ได้จริง (Emulator หรือบอร์ด)RATE_MS เป็น 40 (25 Hz) แล้วเก็บอีกชุด เทียบว่าจำนวนบรรทัดต่อวินาทีต่างกันยังไง อธิบายว่าทำไมอัตราต้องตรงกับโมเดลใบ้ข้อ 3 — ที่ 25 Hz หนึ่ง burst (200 sample) จะใช้เวลานานขึ้นเป็นสองเท่า และรูปคลื่นที่ได้จะ "ห่าง" กว่าเดิม ไม่เหมือนที่โมเดล 50 Hz เคยเห็น
วันนี้เราได้: เข้าใจว่า DAQ คืออะไรและอยู่ต้นวงจร · รู้จักอัตราสุ่มและทำไมต้องคงที่+ตรงโมเดล · เขียน schema/CSV บนบอร์ด · เก็บ dataset ไฟล์แรกด้วยสี่จังหวะ schema/sample/record/rate
ชุดบทเรียนถัดไป (บทเรียน 2.3–2.4) เราจะเก็บ เสียง + หลายเซนเซอร์พร้อมกันบนเส้นเวลาเดียว (audio + synchronized capture) — ต่อยอด dataset ให้รวยขึ้น เจอกันครับ