Hands-on: a DAQ logger that writes a CSV dataset
Module 2 — Collecting sensor data (DAQ) · Slides: slides.md · Module overview · Course page
Write a DAQ logger following the four beats — schema → sample → record → rate — until it saves a labelled dataset to /gestures.csv on the board, then check the file, measure the real rate, and learn the silent traps that spoil a dataset.
Objectives
Section titled “Objectives”By the end of this lesson, you will:
- Fill the four blanks in practice/s04_daq_logger.py so that pressing a label button once adds 200 new lines to a file headed with label,ax,ay,az,gx,gy,gz, with values that aren’t all zero.
- Record all three labels, at least two bursts each, then open the file in the REPL and confirm the line count equals 1 + (number of bursts × 200), and that the labels are balanced.
- Explain why the real rate is below the 50 Hz that was set, and name the four silent traps (“w” mode, a wrong label, swapped columns, a missing \n).
Before you start
Section titled “Before you start”You’ve been through lesson 2.1, and remember the schema and the reasoning behind 50 Hz. Keep your learning log ready to note the number of bursts per label, and the line count you actually get.
- Hardware: a TESAIoT Dev Kit board already flashed with BENTO’s MicroPython firmware, or the BENTO Emulator inside BENTO IDE — the emulator fully simulates the IMU, good for practising collection, but data meant for training a real model should come from the board.
- Prior lesson: lesson 2.1 — Sampling to match the model: rate, Nyquist, windows and the CSV schema
Concepts
Section titled “Concepts”The logger follows the same four-beat skeleton, but its loop has DAQ’s own four beats: schema (write the header once if the file doesn’t exist yet, using try/except to check first) → sample (sensors.bmi270.motion(), all six axes in one call) → record (f.write one line, columns in schema order, ending with \n) → rate (time.sleep_ms(RATE_MS)), looping BURST times per press. Every DAQ system in the world follows these same four beats — they differ only in detail. Opening with with open(PATH, "a") for each burst closes and flushes the file every time, so unplugging mid-way doesn’t lose a burst that already finished writing.
time.sleep_ms(20) doesn’t give exactly 50 Hz, because each round also spends time reading the sensor and writing the file, so the real rate is a little lower (around 40–45 Hz). What’s acceptable is being consistently lower; what’s bad is a rate that drifts. The lesson here: don’t trust the value you set — measure what you actually get, which the full version shows with time.ticks_diff.
Record labels that match the Motion model’s classes (idle, circle, shaking), keep them balanced, and check before trusting, every time: count lines, check the header, check the values. Four traps produce no error at all: opening with "w" and overwriting old data, pressing the wrong label button, writing columns out of order, and forgetting \n. This skeleton is a template — change only the sample and schema lines, and you can collect a different sensor, such as sensors.dps368.pressure_temperature() or sensors.sht40.temperature_humidity(), at a much slower rate. This /gestures.csv file is the raw material for module 5 (Training).
Worked example
Section titled “Worked example”s04_daq_logger.py in the examples folder is the reference version this lesson builds around. s04_daq_logger_full.py adds a counter split by label (see class balance live), measures the real rate in Hz, and has a Clear button to wipe the file and start over. Open it for comparison once your practice file is done.
| File | What this file teaches |
|---|---|
| examples/s04_daq_logger.py | Recording sensor data to a CSV file (Data Acquisition) |
| examples/s04_daq_logger_full.py | Recording a dataset to CSV (full version) |
Practice
Section titled “Practice”The file has 4 blanks (lines 55, 69, 73 and 76), matching DAQ’s four beats exactly: 1) schema: f.write("label,ax,ay,az,gx,gy,gz\n") 2) sample: ax, ay, az, gx, gy, gz = sensors.bmi270.motion() 3) record: f.write("%s,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f\n" % (label, ax, ay, az, gx, gy, gz)) 4) rate: time.sleep_ms(RATE_MS). If every line is 0.0, blank 2 is still empty. If the file never grows at all, blank 3 is still empty.
| Practice file | Topic |
|---|---|
| practice/s04_daq_logger.py | Recording sensor data to a CSV file (Data Acquisition) (the fill-in-the-code version) |
Solution
Section titled “Solution”Open the solution after trying on your own at least once, and read how to use the solutions first.
| Solution | Pairs with |
|---|---|
| solution/s04_daq_logger.py | practice/s04_daq_logger.py |
Check your understanding
Section titled “Check your understanding”The same questions are in quiz.yaml for automated checking.
-
Put DAQ’s four beats in order, as they appear in the logger file (ordering · objective 1)
- a) record: write one line to the file
- b) schema: write the header once
- c) rate: time.sleep_ms(RATE_MS)
- d) sample: sensors.bmi270.motion()
Solution
b → d → a → c — schema happens once at the start, then sample → record → rate loop for BURST rounds per press.
-
You open the file and every line is 0.0000 except the label column. Which blank is still empty? (single choice · objective 1)
- a) Blank 1, schema
- b) Blank 2, the sample that reads motion()
- c) Blank 3, record
- d) Blank 4, rate
Solution
b — the placeholder line ax = ay = … = 0.0 lets the program run without error, but if the sensor is never actually read, every value stays zero, making the dataset useless.
-
You record idle 2 bursts, circle 2 bursts, and shaking 3 bursts. How many lines (including the header) should a correct file have? (single choice · objective 2)
- a) 1400
- b) 1401
- c) 1407
- d) 601
Solution
b — 7 bursts × 200 = 1400 data lines, plus 1 header line, giving 1401. And shaking has one more burst than the other classes — worth recording more of the others to balance it.
-
You set RATE_MS = 20, but the measured rate is really about 43 Hz. Why? (single choice · objective 3)
- a) The sensor is broken
- b) Each round also spends time reading the sensor and writing the file, on top of the 20 ms sleep, so one real round takes longer than 20 ms
- c) time.sleep_ms rounds to whole seconds
- d) Flash can only be written 43 times per second
Solution
b — this is a real fact of embedded work. As long as it’s consistently lower, it’s still usable. What matters is measuring the real value instead of trusting the one you set.
-
Which of these are silent traps where the program runs fine but the dataset is wrong? (select every correct answer) (multiple choice · objective 3)
- a) Writing gx before ax, not matching the schema
- b) Forgetting \n at the end of a line, so every sample runs together into one line
- c) Misspelling sensors, causing a NameError
- d) Opening the file with “w” inside the recording loop
Solution
a, b, d — a NameError shows an error right away, so it isn’t a silent trap. The other three run fine but produce wrong data — always open the file and check.
The MVP for lessons 2.1–2.2: a logger that genuinely saves N labelled samples to CSV, letting you choose the label, sampling at a steady rate, writing to a file on the board, and verifiable for the correct number of lines.
- All four blanks in the practice file are filled in, and a dataset can be recorded on the emulator or the board.
- Record all three labels, at least two bursts each, then count lines in the REPL and get 1 + (bursts × 200).
- Change
RATE_MSto 40 (25 Hz), record another set, and note in your learning log how the time per burst and the waveform differ. - Be able to explain where in the code the four beats — schema, sample, record, rate — happen, and why 50 Hz.
Going further
Section titled “Going further”In the next pair of lessons (2.3–2.4), we’ll record audio and the IMU together on one shared timeline, enriching the dataset.
Next lesson: lesson 2.3 — Audio and several sensors on one timeline: PDM at 16 kHz, timestamps and jitter
Reflect
Section titled “Reflect”- Is your dataset balanced yet? If not, how would you collect more without mixing up labels?
- If you wanted to record air pressure instead of the IMU, how many lines would you need to change, and what would you set RATE_MS to?
Review questions
Answer on your own first, then open the answer.
-
Order the four DAQ beats in the logger file. (Objective 1)
- record: เขียนหนึ่งบรรทัดลงไฟล์
- schema: เขียนหัวตารางครั้งเดียว
- rate: time.sleep_ms(RATE_MS)
- sample: sensors.bmi270.motion()
Show answer
Correct order: B. schema: เขียนหัวตารางครั้งเดียว → D. sample: sensors.bmi270.motion() → A. record: เขียนหนึ่งบรรทัดลงไฟล์ → C. rate: time.sleep_ms(RATE_MS)
schema ทำครั้งเดียวตอนเริ่ม แล้ววน sample → record → rate ครบ BURST รอบต่อการกดหนึ่งครั้ง
-
Every line in the file is 0.0000 except the label column. Which blank is still empty? (Objective 1)
- ช่อง 1 schema
- ช่อง 2 sample ที่อ่าน motion()
- ช่อง 3 record
- ช่อง 4 rate
Show answer
Answer: B. ช่อง 2 sample ที่อ่าน motion()
บรรทัดสำรอง ax = ay = ... = 0.0 ทำให้โปรแกรมไม่ error แต่ถ้าไม่ได้อ่านเซนเซอร์จริง ทุกค่าจะเป็นศูนย์ dataset จึงไร้ประโยชน์
-
You record idle twice, circle twice and shaking three times. How many lines should the correct file have, header included? (Objective 2)
- 1400
- 1401
- 1407
- 601
Show answer
Answer: B. 1401
7 burst × 200 = 1400 บรรทัดข้อมูล บวกหัวตาราง 1 บรรทัด เป็น 1401 และ shaking มีมากกว่าคลาสอื่นอยู่หนึ่ง burst ควรเก็บเพิ่มให้สมดุล
-
RATE_MS = 20 but the measured rate is about 43 Hz. Why? (Objective 3)
- เซนเซอร์เสีย
- แต่ละรอบมีเวลาอ่านเซนเซอร์และเขียนไฟล์บวกกับ sleep 20 ms หนึ่งรอบจริงจึงยาวกว่า 20 ms
- time.sleep_ms ปัดเป็นวินาที
- flash เขียนได้แค่ 43 ครั้งต่อวินาที
Show answer
Answer: B. แต่ละรอบมีเวลาอ่านเซนเซอร์และเขียนไฟล์บวกกับ sleep 20 ms หนึ่งรอบจริงจึงยาวกว่า 20 ms
นี่คือความจริงของงานฝังตัว ตราบใดที่ต่ำกว่าอย่างสม่ำเสมอก็ยังใช้ได้ สิ่งสำคัญคือวัดค่าจริงแทนการเชื่อค่าที่ตั้ง
-
Which are silent traps where the program runs but the dataset is wrong? (select all that apply) (Objective 3)
- เขียน gx ก่อน ax ไม่ตรง schema
- ลืม \n ท้ายบรรทัด ทุก sample ต่อกันเป็นบรรทัดเดียว
- สะกด sensors ผิดจนเกิด NameError
- เปิดไฟล์ด้วย "w" ในลูปบันทึก
Show answer
Answer: A. เขียน gx ก่อน ax ไม่ตรง schema · B. ลืม \n ท้ายบรรทัด ทุก sample ต่อกันเป็นบรรทัดเดียว · D. เปิดไฟล์ด้วย "w" ในลูปบันทึก
NameError ขึ้น error ให้เห็นทันทีจึงไม่ใช่กับดักเงียบ ส่วนอีกสามข้อรันผ่านแต่ข้อมูลผิด ต้องเปิดไฟล์ตรวจเสมอ
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"Hands-on: a DAQ logger that writes a CSV dataset" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
Thai attribution: "ลงมือทำ: DAQ logger เก็บ dataset ลง CSV" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/edge-ai-developer/m02-daq/l02-daq-logger-lab/
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA