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.
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;
}
}