I've been working on a program that would allow me to quickly capture and label data to upload to Edge Impulse.
Data capture program requirements
- Fixed sample size
- Fixed sample rate
- Selectable data labels
- Incrementable file name
- Save to SD card
To make things easy, I'm not going to allow user entered parameters because I don't have a keyboard. And I'm not going to get fancy and implement a GUI or pulldown selections (save that for another project ). Because a lot of data needs to be taken at each sample size and sample rate, I'm going to cheat and update the program if I need to change those parameters (at least to start).
For a simple interface, I'm going to use the 3 user buttons at the top of the Wio Terminal
- Left button - Start the data capture (also stop display update to improve data rate - not using DMA yet)
- Middle button - increment file number (I may change to auto-increment)
- Right button - select data label (see below)
Data labels and filename format
The first requirement for training an ML model is to have a dataset with labelled data. The labels correspond to the different categories that can be classified. In my test case I'm going to try to identify footsteps of two different people, so I'll have the labels of Person1 and Person2. It is recommended that 2 or 3 additional categories of data are provided. The Idle label will be for "no input" data, sort of a noise floor for ambient sensor noise. The Noise label will be for possible background noise sources (e.g. door opening and closing, refrigerator or other mechanical vibration). And finally, the Unknown label for other people's footsteps.
I am going to capture time series data, so I am going to create a CSV file with TimeStamp and Vibration value pairs. I'm initially going to try sampling at 500Hz, so I'm going to use the Arduino millis() function to generate the TimeStamp. If I decide to try sampling at a higher frequency, I'll need to shift to using the micros() function. I know with micros() works down to 4 microsecond resolution on faster Arduinos, but I haven't tried it with The WioTerminal. I have a much faster MCU (120MHz M4F), so I would think that I could get 1 microsecond resolution.
The filename format will "Label" + "_" + "Sample Number" + ".csv", so for example Idle_0.csv, Person1_0.csv. I originally did not have the "underscore" in the filename, but as I typed this I realized that I ended a couple of labels with a number and that would be confusing.
Separate data capture and SD card write functions
I had initially started sequentially capturing and writing each sample to the SD card. However, when I examined the timestamps I realized that I was only capturing at between 50-60Hz because of the write time to the SD card. Capturing the full set of data samples into a memory buffer and then writing to the SD card enables capturing at the 500Hz data rate. If I start sampling into the KHz range, I'll probably need to start using DMA with the ADC,
Data capture program
The program is a bit "raw" at this point, but it functions well enough for me to capture sample data. I'll need to go back and clean it up (I moved a bunch of stuff around and need to delete some of the commented out code). I really should automate it, but I want to evaluate some data first. Hate to take a bunch of bad data.
Here is the current program:
Wio_Terminal_Datalogger_Sample_new.ino
#include <SPI.h> #include <Seeed_FS.h> #include "SD/Seeed_SD.h" #include"seeed_line_chart.h" //include the library File myFile; TFT_eSPI tft; TFT_eSprite spr = TFT_eSprite(&tft); // Sprite #define max_size 30 //maximum size of data doubles data[3]; //Initilising a doubles type to store data int vibration; int dataCount; int keyPressed = 0; int keyUpdate = 0; int sampleNumber = 0; int typeIndex = 0; String sampleType[] = {"Idle","Noise","Unknown","Person1","Person2"}; String fileName; int fileHeader = 0; // define logging interval and sample size #define FREQUENCY_HZ 500 #define INTERVAL_MS (1000 / (FREQUENCY_HZ + 1)) #define SAMPLE_SZ 2500 // 500 * 5 seconds int sampleData[SAMPLE_SZ]; unsigned long timeData[SAMPLE_SZ]; void setup() { Serial.begin(115200); if (!SD.begin(SDCARD_SS_PIN, SDCARD_SPI)) { Serial.println("initialization failed!"); while(1); } pinMode(A0, INPUT); pinMode(WIO_KEY_A, INPUT_PULLUP); // rightmost button pinMode(WIO_KEY_B, INPUT_PULLUP); // center button pinMode(WIO_KEY_C, INPUT_PULLUP); // leftmost button pinMode(WIO_5S_PRESS, INPUT_PULLUP); // 5 way button tft.begin(); tft.setRotation(3); spr.createSprite(TFT_HEIGHT,TFT_WIDTH); tft.setTextSize(2); dataCount = 0; } void loop() { static unsigned long last_interval_ms = 0; // if (millis() > last_interval_ms + INTERVAL_MS) { // last_interval_ms = millis(); spr.fillSprite(TFT_WHITE); vibration = analogRead(A0); Serial.print("600,"); Serial.print(vibration); Serial.println(",400"); if (data[0].size() == max_size) { for (uint8_t i = 0; i<3; i++){ data[i].pop(); //this is used to remove the first read variable } //data[0].pop();//this is used to remove the first read variable } data[0].push(vibration); //read variables and store in data data[1].push(530); //top data[2].push(480); //bottom // if (dataCount < SAMPLE_SZ){ // saveData(); // dataCount++; // } //Settings for the line graph title auto header = text(0, 0) .value(" Vibration Readings") .align(center) .valign(vcenter) .width(tft.width()) .thickness(2); header.height(header.font_height() * 2); header.draw(); //Header height is the twice the height of the font //Settings for the line graph auto content = line_chart(20, header.height()); //(x,y) where the line graph begins content .height(tft.height() - header.height() * 1.5) //actual height of the line chart .width(tft.width() - content.x() * 2) //actual width of the line chart .based_on(450.0) //Starting point of y-axis, must be a float .show_circle(false) //drawing a cirle at each point, default is on. .value({data[1],data[0],data[2]}) //passing through the data to line graph .color(TFT_WHITE,TFT_RED,TFT_WHITE) //Setting the color for the line .draw(); spr.pushSprite(0, 0); checkpress(); // check for button press // } } void saveData(String fName,unsigned long tData, int sData){ // fileName = sampleType[typeIndex]; // fileName.concat(sampleNumber); // fileName.concat(".csv"); // tft.drawString(fileName,50,130); myFile = SD.open(fName,FILE_APPEND); // vibration = analogRead(A0); // Serial.println(vibration); if (fileHeader == 0) { myFile.println("timestamp,VSBV203"); // print file header if first pass fileHeader = 1; } myFile.print(tData); myFile.print(","); myFile.println(sData); myFile.close(); } void captureData(unsigned long timeStamp, int count){ vibration = analogRead(A0); timeData[count] = timeStamp; sampleData[count] = vibration; } // Check state of buttons void checkpress(){ char buffer[20]; static unsigned long last_interval_ms = 0; if (digitalRead(WIO_KEY_A) == LOW) { if (keyPressed == 3) { keyUpdate = 0; } else { keyUpdate = 1; keyPressed = 3; } } else if (digitalRead(WIO_KEY_B) == LOW) { if (keyPressed == 2) { keyUpdate = 0; } else { keyUpdate = 1; keyPressed = 2; } } else if (digitalRead(WIO_KEY_C) == LOW) { if (keyPressed == 1) { keyUpdate = 0; } else { keyUpdate = 1; keyPressed = 1; } } else if (digitalRead(WIO_5S_PRESS) == LOW) { keyPressed = 0; } delay(200); if (keyUpdate) { switch (keyPressed) { case 1: tft.fillScreen(TFT_BLACK); tft.drawString("Start Sampling",25,90); // tft.drawString(fileName,50,130); while (dataCount < SAMPLE_SZ){ if (millis() > last_interval_ms + INTERVAL_MS) { last_interval_ms = millis(); captureData(last_interval_ms,dataCount); dataCount++; } // if (dataCount >= SAMPLE_SZ) break; } fileName = sampleType[typeIndex]; fileName.concat("_"); fileName.concat(sampleNumber); fileName.concat(".csv"); tft.drawString(fileName,50,130); for (int i = 0; i < SAMPLE_SZ; i++){ saveData(fileName,timeData[i],sampleData[i]); } dataCount = 0; break; case 2: sampleNumber++; Serial.println(sampleNumber); tft.fillScreen(TFT_BLACK); tft.drawString("Sample Number: ",25,110); tft.drawString(String(sampleNumber),200,110); break; case 3: if (typeIndex++ > 3) typeIndex = 0; Serial.println(sampleType[typeIndex]); tft.fillScreen(TFT_BLACK); tft.drawString("Sample Type: ",25,110); tft.drawString(sampleType[typeIndex],200,110); break; } keyUpdate = 0; delay(2000); } }
Program Demo
The interface is a bit clunky, but it works. I realized that I can increment the sample number, but not decrement or reset it. I'll need to add those capabilities.
The data sampling starts as soon as you see the "Start Sampling" message and the SD card write starts as soon as you see the filename displayed. So for 2500 data points (5 seconds of data), it takes about 30 seconds to write to the SD card. I'm sure there are ways to reduce that time, but I'll work on that if I decide to automate data capture.
CSV File Idle_0.csv
Timestamps are 2 ms apart - 500Hz.
Plot of Data Sample (5 seconds - 2500 points)
Noise floor is around +/- 1.5 mV.