Week 8: Input Devices


Part I: Microphone input

Having already played already with the camera sensor on the XIAO ESP32-S3 Sense, I decided to already experiment with the built-in microphone as well. I modified the camera code according to the microphone tutorial code. Empirically, I found an amplitude threshold of 5000 to record 5 seconds of audio to the micro SD card in 16-bit WAV format.

Amplitude triggered recording code

// Monitors microphone signal amplitude and records when threshold is activated
#include <ESP_I2S.h>
#include "FS.h"
#include "SD.h"

#define RECORD_TIME 5 // max value is 240
#define WAV_FILE_NAME "mic_rec_trig"
#define REC_THRESHOLD 5000 // threshold to activate recording
#define SD_PIN 21 // SD card pin
#define FILE_NAME_SIZE 64 // max number of chars in filename
#define DEBOUNCE_DELAY 1000 // Prevent immediate re-triggering

int rec_count = 1; // counter to enusure recording file name is unique
char rec_filename[FILE_NAME_SIZE]; // use to get max existing number of recordings and for saving

I2SClass I2S; // instantiate I2S class


uint8_t *wav_buffer; // Create variable to store audio data
size_t wav_size;


void setup() {

  // Open serial communications and wait for port to open:
  // A baud rate of 115200 is used instead of 9600 for a faster data rate
  // on non-native USB ports
  Serial.println("Initializing serial port");
  Serial.begin(115200);
  while (!Serial) {
    delay(10); // wait for serial port to connect. Needed for native USB port only
  }

  Serial.println("Initializing I2S bus");

  // setup 42 PDM clock and 41 PDM data pins for audio input
  I2S.setPinsPdmRx(42, 41);

  // start I2S at 16 kHz with 16-bits per sample
  if (!I2S.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
    Serial.println("Failed to initialize I2S!");
    while (1); // stay in error state
  }

  // Set up the pins used for SD card access
  if(!SD.begin(SD_PIN)){
    Serial.println("Failed to mount SD Card!");
    while (1) ;
  }
  Serial.println("SD card initialized.");

  snprintf(rec_filename, FILE_NAME_SIZE, "/%s_%%d.wav", WAV_FILE_NAME); // use escape sequence to defer format specification

  // Scan SD card to find the last recording number
  Serial.println("Checking for existing recordings...");

  File root = SD.open("/");
  if (root) {
    int max_num = 0;
    File file = root.openNextFile();
    while(file){
      if (strstr(file.name(), WAV_FILE_NAME) && strstr(file.name(), ".wav")) {
        int num = 0;
        sscanf(file.name(), rec_filename, &num); // Parse the number from the filename
        if (num > max_num) {
          max_num = num;
        }
      }
      file.close();
      file = root.openNextFile();
    }
    root.close();

    rec_count = max_num + 1; // Start counting from the next available number
    Serial.printf("Starting count from: %d\n", rec_count);
  }
}

void loop() {
  // read a sample
  int sample = I2S.read();
  if (sample && sample != -1 && sample != 1) {
    Serial.printf("%d, 0, %d\n", sample, REC_THRESHOLD);
    if (sample > REC_THRESHOLD){
      Serial.println("Threshold triggered!");
      record_wav();
      delay(DEBOUNCE_DELAY);
    }
  }
}

void record_wav() {
  wav_buffer = i2s.recordwav(record_time, &wav_size);

  snprintf(rec_filename, file_name_size, "/%s_%d.wav", wav_file_name, rec_count);

  file file = sd.open(rec_filename, file_write);
  if (!file) {
    serial.printf("failed to open file %s\n", rec_filename);
    return;
  }
  serial.println("writing audio data to file...");

  // write the audio data to the file
  if (file.write(wav_buffer, wav_size) != wav_size) {
    serial.println("failed to write audio data to file!");
    file.close();
    return;
  }

  file.close();
  serial.printf("recording saved to %s\n", rec_filename);
  rec_count = rec_count + 1; // increment counter after successful save
}
                

In the future, it might be useful to make the camera activiated by the microphone instead of constantly detecting if person is in frame. Although there may be other more low-power sensors to achieve this such as PIR motion sensor.


Part II: Measuring desk height with ToF sensor

After reading Mateo's page on comparing the Ultrasonic HC-SR04 sensor, PIR motion sensor, and VL53L1X time-of-flight (ToF) sensor, I decided to use the ToF sensor to measure the distance between the height of the standing desk for my final project. I wanted to reuse the sensors so I soldered female and male pins to a ribbon cable to connect directly to the XIAO ESP32S3-Sense. The VL53L1X uses I2C communication protocol so only four pins need to be connected: VIN (3.3V), GND, SDA, and SCL. There is also a XSHUT pin that according to the datasheet needs to be kept HIGH to enable the sensor but I didn't need to connect for the sensor to work. I guess the breakout board must have an onboard pull-up resistor.

Soldered pins to ribbon cable.
Soldering pins to ribbon cable.
Connection of VL53L1X sensor
Connection to VL53L1X sensor.
Connection to XIAO ESP32S3.
Connection to XIAO ESP32S3.

I modified Adrian's code to average over measurement readings every second. I then compared the measured desk height to floor distance of my standing desk display from 30 inches to 50 inches in 5 inch increments to the ToF sensor. I found there was a fairly consistent bias of 80 mm (3.14 inches) between the "ground truth" height and the measured height. I wonder if the standing desk is measured with respect to a different baseline. Anyways, this confirms that the VL53L1X is good enough to be used to measure standing desk height for feedback to my XIAOESP32 to control the standing desk build-in PCB.


Measuring desk height with time-of-flight sensor

/*
  Measure distance with VL53L1X ToF sensor. The readings are in mm units and averages over one second interval.
*/
#include "Wire.h"
#include "VL53L1X.h"

VL53L1X sensor;

// Variables for averaging
unsigned long totalDistance = 0;
int readingCount = 0;
unsigned long lastPrintTime = 0;
const unsigned long printInterval = 1000; // 1000 ms = 1 second

void setup() {
  Serial.begin(115200); // set baud rate
  Wire.begin();
  Wire.setClock(400000); // use 400 kHz I2C

  sensor.setTimeout(500);
  if (!sensor.init()) {
    Serial.println("Failed to detect and initialize sensor!");
    while (1); // stay in error state
  }

  // Use long distance mode and allow up to 50000 us (50 ms) for a measurement.
  // You can change these settings to adjust the performance of the sensor, but
  // the minimum timing budget is 20 ms for short distance mode and 33 ms for
  // medium and long distance modes. See the VL53L1X datasheet for more
  // information on range and timing limits.
  sensor.setDistanceMode(VL53L1X::Long);
  sensor.setMeasurementTimingBudget(50000);

  // Start continuous readings at a rate of one measurement every 50 ms (the
  // inter-measurement period). This period should be at least as long as the
  // timing budget.
  sensor.startContinuous(50);

  // Initialize the print timer
  lastPrintTime = millis();
}

void loop() {
  // This is a non-blocking check.
  if (sensor.dataReady()) {
    int distance = sensor.read(); // sensor.read() clears the dataReady flag.

    if (!sensor.timeoutOccurred()) {
      totalDistance += distance;
      readingCount++;
    }
  }

  // Check if 1 second has passed since the last print
  unsigned long currentTime = millis();
  if (currentTime - lastPrintTime >= printInterval) {
    if (readingCount > 0) {
      float avgDistance = (float) totalDistance / readingCount;

      Serial.print("Average Distance: ");
      Serial.print(avgDistance, 1); // 1 decimal places
      Serial.print(" mm (from ");
      Serial.print(readingCount);
      Serial.println(" readings)");
    } else {
      Serial.println("No valid readings in the last second.");
    }

    // Reset accumulators for the next 1-second interval
    totalDistance = 0;
    readingCount = 0;
    lastPrintTime = currentTime;
  }
}
                
Plot of measured standing desk heights.
Plot of measured standing desk heights.