Open Source Ornithological Recording Device

Brandon Finley, Anders Smitterberg, Suhayb Zeqlam

https://osf.io/dj6gz/wiki/Links%20and%20Resources/

Project Description and Incentive

The Bird Attractant and Deterrent device (BAD) aims to guide birds away from cities as they migrate south for the winter. Building collisions are the second leading cause of bird death, resulting in the death of over a billion birds a year. The BAD uses different sounds to attempt to, in a sense, shepherd birds away from potential hazards in the migratory path. By outputting sounds at a frequency at which birds commonly communicate, the birds may become annoyed and move to a different area to get away from the noise. A similar effect is planned to be created using predatory sounds. The BAD was created for researchers in the forestry department of Michigan Technological University. The researchers will use the BAD over the summer of 2023 to determine if the sounds output by the device have any effect on the migration of the birds. If there is a proven effect, the theory behind BAD may be incorporated into major cities and potentially save millions of birds annually.

Goals and Progress

The researchers had specified a set of requirements for their project that involved constructing eight devices. Almost all the goals have been met, except for one. The option to adjust the speaker system is currently inadequate. However, seven out of the eight devices have already been built, and the final one is pending construction as it is awaiting shipment. The built devices have been successful in meeting the defined requirements of the project. They have sufficient onboard power management, create high-quality audio from removable media, are weather-resistant, and are easily configurable. Moreover, they are well-documented and can be assembled by both professionals and amateurs. The project has been developed using open-source software and contributions.

Additionally, a battery charge indicator was included in the design of the devices. Furthermore, an alternative power system was designed using solar panels to sustain the devices in the field for a longer period. With these features, the devices have been able to remain operational for a sustained duration in the field. These value-added goals have been successfully achieved in the project.

In conclusion, the project’s functional requirements and value-added goals have been largely successful, with one minor exception. The project involved building eight devices for the purpose of studying bird migration patterns, out of which seven have been successfully constructed.

Mechanical Assembly

The mechanical assembly of the project was designed to withstand the harsh outdoor environment in which it will be installed. Given the location of the installation, a weatherproof enclosure for the electronics was necessary. We chose to use a waterproof gun case, which provided ample space for our components and was able to withstand harsh weather conditions.

Additionally, to ensure a reliable power source for the devices, a solar installation was designed to sustain them in the field for a longer period. The solar panel mount was installed at a 45 degree angle and is intended to be south-facing, ensuring maximum exposure to sunlight. The mount was made of treated lumber. The solar panels used are also weather-resistant, making them suitable for the forest environment.

The use of the waterproof gun case allowed us to easily assemble and organize the components. The two levels of foam provided a secure fit for each component, preventing any movement or damage to the devices during transport or installation.

Photos of the assembled devices and their waterproof enclosures, as well as the solar installation, can be found in the project files. The mechanical assembly of the devices was designed with durability and reliability in mind, ensuring that they will continue to operate effectively in the field.

Electrical Assembly

The project was designed around the use of a 12-volt battery and a complementary solar installation to power a sound system. The system needed to play audio from 100W speakers for 3 hours per day, so it was crucial to ensure that the system had enough power on board and some cushion. The team used the relationship of voltage * current = power to determine that a 35 Amp hour battery would be sufficient for these needs.

The rest of the system was designed around the 12V supply. The audio amplifier was selected with the power requirements in mind and runs off of the 12V rail, capable of supplying 100W to a pair of speakers. For the first iteration of the design, the amplifier outputs only one channel of audio, but this can be changed later if necessary.

To control the system, the team chose the ATMega2560 microcontroller, which can be easily programmed using the Arduino IDE. The ATMega2560 has ample serial ports and header pins while also being extremely low power, making it ideal for the setup. Several peripheral components were used in the design, including a real-time clock module, SD hard header, serial LCD, rotary encoder, and amplifier. All the devices, except for the rotary encoder and amplifier, were connected in series and given their dedicated digital pins.

The ATmega was connected to all of these devices through a hand-soldered shield, where all of the wire traces were located. The system was wired per the schematic located in the files, and the wire trace lengths were minimized to reduce stray inductances and maintain signal integrity on sensitive serial lines. The audio signal is output to the amplifier through PWM pins on the ATMega, as detailed in the Code section of the wiki.

The solar installation and battery management system were wired in accordance with the schematic and were locally sourced. The panels were selected to exceed the power requirements of the system over a day.

Code and Integration

The code for the project was written in Arduino C++ using the the Arduino IDE. The code needed to implement the UI used control the BAD, file input, as well as audio output. Several open source libraries were used within the code to accomplish these goals.

For the UI, the LiquidCrystal I2C library was used. The LiquidCrystal I2C library is a variant of the LiquidCrystal library that has support for serial communication rather than the traditional parallel communication. Using serial communication over parallel cuts down on the amount of wires needed in the hardware. The UI also includes a rotary encoder. Two interrupts were created in the code, one for when the rotary encoder was turned and another for when the push button on the rotary encoder was pressed. The rotation of the rotary encoder moves between different options on the LCD screen and pressing the rotary encoder selects.

For the file input, the SD library was used. The BAD needs to read audio files off an external SD card. The SD library contains functions that simplify navigating through files on an SD card. Easy traversal through the SD card make it possible for text files to be used to set up pre-programmed commands for the BAD to carry out. This allows for the BAD to be configured and left untouched for multiple days. Along with pre-programming the bad, easy file navigation allows for a config file to be created that tracks the current setup of the BAD in case power to the device is ever lost. On startup, the BAD will check if the config file is there, read the values on it, and configure the BAD to the settings that were set before the last shutdown.

For audio output, the TMRCPM library was used. The TMRCPM library reads .WAV files and outputs them through the PWM pin on the arduino. While the quality of the audio output is not the greatest, it can be made better through external hardware. Through testing, it was determined that for best audio quality, the .WAV file needs to have a 8-bit resolution (the highest resolution supported by TMRCPM) as well as a 32 kHz sampling rate. While the audio still won’t be perfectly clear, it is good enough for the current use case.

The code also incorporates the RTClib library which is used to configure and read from the RTC module in the BAD.

The code used can be found in the Files section of the OSF. The code contains comments that dive deeper into exactly what the code is doing as well as why.

#include <TMRpcm.h>
#include <pcmConfig.h>
#include <pcmRF.h>

#include <SD.h>
#include <hd44780.h>
#include <string.h>
#include <stdio.h>
#include <RTClib.h>
#include <Adafruit_MCP4725.h>
#include <LiquidCrystal_I2C.h>
#include <SPI.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);
RTC_DS3231 rtc;
Adafruit_MCP4725 dac;
TMRpcm tmrpcm;

//variables related to file I/O
File soundFile;
File dir;
File curConfig;
bool fileCheck = true;
bool fileSel = false;
bool fileChange = false;
bool fileSet = false;
bool isDir = true;
int numFiles = 0;
int curFile = -1;
char *files[25];
const int chipSelect = 53;

//variables related to menu/menu changing
int menuCounter = 1;  //current menu
bool mainMenu_selected = true;
bool settings_selected = false;
bool soundMenu_selected = false;
bool enableMenu_selected = false;
bool timeMenu_selected = false;
bool backLight = false;
bool menuChange = false;

unsigned long lastDebounceTime = 0;
unsigned long rotateDebounceTime = 0;
unsigned long rotateDebounceDelay = 10;
unsigned long debounceDelay = 150;

//variables related to rotary encoder
const int RotaryCLK = PB3;   //CLK pin on the rotary encoder
const int RotaryDT = PB4;    //DT pin on the rotary encoder
const int PushButton = PB2;  //Button to enter/exit menu

//Statuses for the rotary encoder
int CLKNow;
int CLKPrevious;
int DTNow;
int DTPrevious;

//time variables
int hours = 0;
int minutes = 0;
bool changeHour = false;
bool changeMin = false;
bool timeChange = false;
bool timeSet = false;
char screenTime[6];  //string used to print time

int lastPress = 0;  //used to track the last switch press on rotary encoder, negates switch bounce

//enable menu variables
bool enable = false;
bool statusChanged = false;
bool configModeEnabled = false;

bool refreshLCD = false;  //refreshes values

DateTime currentTime;  //stores current time


void setup() {

  sprintf(screenTime, "%02d:%02d", hours, minutes);  //formats selected time and minutes to hh:MM

  //initialize pins used with rotary encoder
  pinMode(PB3, INPUT_PULLUP);  //RotaryCLK
  pinMode(PB4, INPUT_PULLUP);  //RotaryDT
  pinMode(PB2, INPUT_PULLUP);  //Button

  //initilize rtc
  rtc.begin();
  //rtc.adjust(DateTime(__DATE__, __TIME__));


  //initilice lcd
  lcd.begin(2, 16);
  lcd.backlight();
  lcd.clear();             //clear the whole LCD
  printMain();             //print the main menu onto screen
  updateCursorPosition();  //put cursor onto the screen


  //initialize SD reader
  pinMode(53, OUTPUT);
  if (!SD.begin(53)) {
    Serial.println("SD initialization error");
  }
  dir = SD.open("/");

  readFiles();
  readConfig();

  tmrpcm.speakerPin = 46;
  tmrpcm.setVolume(4);
  //tmrpcm.quality(1);

  //DAC
  //dac.begin(0x62);

  //initialize rotary encoder status variables
  CLKPrevious = digitalRead(RotaryCLK);
  DTPrevious = digitalRead(RotaryDT);

  //declare interrupts
  attachInterrupt(digitalPinToInterrupt(RotaryCLK), rotate, CHANGE);        //CLK pin is an interrupt pin
  attachInterrupt(digitalPinToInterrupt(PushButton), pushButton, FALLING);  //PushButton pin is an interrupt pin
}

void loop() {

  /*if(backLight){
    lastPress = 0;
    backLight = false;
    lcd.backlight();
    }

    //puts the lcd to "sleep"
    if( (lastPress >= 10000) && !backLight){
    lcd.noBacklight();
    }
    else if(lastPress < 10000){
    */
  // lastPress++;

  if (fileCheck == false) {
    fileCheck = true;
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Bad Config File");
  }

  //updates lcd when cursor has moved
  if (refreshLCD == true) {
    refreshLCD = false;
    updateCursorPosition();
  }

  //updates lcd when a new menu is selected
  if (menuChange == true) {
    updateLCD();
    menuChange = false;
  }

  //prints the name of the currently selected file if it has been changes
  if (fileChange) {
    lcd.setCursor(1, 0);
    lcd.print("               ");
    lcd.setCursor(1, 0);
    lcd.print(files[curFile]);
    fileChange = false;
  }

  //updates the displayed status in enable menu when it is changed
  if (enableMenu_selected) {
    if (enable) {
      lcd.setCursor(15, 0);
      lcd.print(byte(1));
    } else if (configModeEnabled) {
      lcd.setCursor(15, 0);
      lcd.print(byte(2));
    } else {
      lcd.setCursor(15, 0);
      lcd.print(0);
    }
  }

    //updates time on main menu in real time
    if (mainMenu_selected && (currentTime.minute() != rtc.now().minute())) {
      printMain();
      updateCursorPosition();
    }

    //for time menu
    //if the time was changed, this will update it
    if (timeChange) {
      timeChange = false;
      sprintf(screenTime, "%02d:%02d", hours, minutes);
      lcd.setCursor(11, 0);
      lcd.print(screenTime);
    }

    //if the status is set to enable, and the current time is equal to the set time, the audio will play
    if (enable && (currentTime.hour() == hours) && (currentTime.minute() == minutes)) {
      tmrpcm.play(soundFile.name());
      // tmrpcm.disable();
    }
  }

/*
This function is called when the rotary encoder is rotated.
It is only ever called by the interupt
*/
  void rotate() {
    //rotating inside of seettings menu
    if ((millis() - rotateDebounceTime) > rotateDebounceDelay) {
      rotateDebounceTime = millis();
      if (settings_selected == true) {
        CLKNow = digitalRead(RotaryCLK);  //get current clk value
        if (CLKNow != CLKPrevious && CLKNow == 1) {
          if (digitalRead(RotaryDT) != CLKNow) {
            if (menuCounter < 3)  //we do not go above 100
            {
              menuCounter++;
            } else {
              menuCounter = 0;
            }
          } else {
            if (menuCounter < 1)  //we do not go below 0
            {
              menuCounter = 3;
            } else {
              // Encoder is rotating B direction so decrease
              menuCounter--;
            }
          }
        }
        CLKPrevious = CLKNow;  // Store last CLK state
      }
      //rotating in timeMenu
      else if (timeMenu_selected == true) {
        CLKNow = digitalRead(RotaryCLK);  //get current clk value
        //if the time is being set, rotating the rotary encoder will change the hour/minute
        //changes the hour
        if (changeHour == true) {
          if (CLKNow != CLKPrevious && CLKNow == 1) {
            if (digitalRead(RotaryDT) != CLKNow) {
              if (hours < 23) {
                hours++;
              } else {
                hours = 0;
              }
            } else {
              if (hours < 1) {
                hours = 23;
              } else {
                // Encoder is rotating B direction so decrease
                hours--;
              }
            }
          }
          CLKPrevious = CLKNow;  // Store last CLK state
          timeChange = true;
          return;
        }
        //changes the minute
        if (changeMin == true) {
          if (CLKNow != CLKPrevious && CLKNow == 1) {
            if (digitalRead(RotaryDT) != CLKNow) {
              if (minutes < 59) {
                minutes++;
              } else {
                minutes = 0;
              }
            } else {
              if (minutes < 1) {
                minutes = 59;
              } else {
                // Encoder is rotating B direction so decrease
                minutes--;
              }
            }
          }
          CLKPrevious = CLKNow;
          timeChange = true;
          return;
        }

        //if the time ins't being change, rotating moves the cursor
        if (CLKNow != CLKPrevious && CLKNow == 1) {
          if (digitalRead(RotaryDT) != CLKNow) {
            if (menuCounter < 2)  //we do not go above 100
            {
              menuCounter++;
            } else {
              menuCounter = 0;
            }
          } else {
            if (menuCounter < 1)  //we do not go below 0
            {
              menuCounter = 2;
            } else {
              // Encoder is rotating B direction so decrease
              menuCounter--;
            }
          }
        }
        CLKPrevious = CLKNow;  // Store last CLK state
      }
      //if the enable menu is the current menu
      else if (enableMenu_selected == true) {
        CLKNow = digitalRead(RotaryCLK);  //get current clk value
        if (CLKNow != CLKPrevious && CLKNow == 1) {
          if (digitalRead(RotaryDT) != CLKNow) {
            if (menuCounter < 2)  //we do not go above 100
            {
              menuCounter++;
            } else {
              menuCounter = 0;
            }
          } else {
            if (menuCounter < 1)  //we do not go below 0
            {
              menuCounter = 2;
            } else {
              // Encoder is rotating B direction so decrease
              menuCounter--;
            }
          }
        }
        CLKPrevious = CLKNow;  // Store last CLK state
      } else if (soundMenu_selected == true) {
        CLKNow = digitalRead(RotaryCLK);  //get current clk value
        //LOOK AT THIS AND FIGURE OUT HOW TO MAKE IT NOT SUCK
        if (fileSel) {
          if (curFile == -1) {
            curFile = 0;
          }
          if (CLKNow != CLKPrevious && CLKNow == 1) {
            if (digitalRead(RotaryDT) != CLKNow) {
              if (curFile < numFiles - 1)  //we do not go above 100
              {
                curFile++;
              } else {
                curFile = 0;
              }
            } else {
              if (curFile < 1)  //we do not go below 0
              {
                curFile = numFiles - 1;
              } else {
                // Encoder is rotating B direction so decrease
                curFile--;
              }
            }
          }
          CLKPrevious = CLKNow;  // Store last CLK state
          fileChange = true;
        } else {
          if (CLKNow != CLKPrevious && CLKNow == 1) {
            if (digitalRead(RotaryDT) != CLKNow) {
              if (menuCounter < 1)  //we do not go above 100
              {
                menuCounter++;
              } else {
                menuCounter = 0;
              }
            } else {
              if (menuCounter < 1)  //we do not go below 0
              {
                menuCounter = 1;
              } else {
                // Encoder is rotating B direction so decrease
                menuCounter--;
              }
            }
          }
          CLKPrevious = CLKNow;  // Store last CLK state
        }
      } else {
        menuCounter = 1;
      }
      refreshLCD = true;
    }
  }


  //function ran when the button on the rotary encoder is pressed
  //is only called my interupt
  void pushButton() {
    if ((millis() - lastDebounceTime) > debounceDelay) {
      lastDebounceTime = millis();
      if (mainMenu_selected == true) {
        mainMenu_selected = false;
        settings_selected = true;
        menuChange = true;
      } else if (settings_selected == true) {
        switch (menuCounter) {
          case 0:
            enableMenu_selected = true;
            settings_selected = false;
            menuChange = true;
            break;

          case 1:
            soundMenu_selected = true;
            settings_selected = false;
            menuChange = true;
            break;

          case 2:
            timeMenu_selected = true;
            settings_selected = false;
            menuChange = true;
            break;

          case 3:
            mainMenu_selected = true;
            settings_selected = false;
            menuChange = true;
            break;
        }
      } else if (enableMenu_selected == true) {
        switch (menuCounter) {
          case 0:
            if (enable) {
              enable = false;
              configModeEnabled = true;
            } else if (configModeEnabled) {
              configModeEnabled = false;
              enable = true;
            }
            if (configModeEnabled == false) {
              enable = true;
              //sort of a debug mode
              minutes = currentTime.minute() + 1;
              hours = currentTime.hour();
              //save current data to config file
              saveConfig();
            }
            enableMenu_selected = false;
            mainMenu_selected = true;
            menuChange = true;
            break;
          case 1:
            enable = false;
            configModeEnabled = false;
            break;
          case 2:
            enableMenu_selected = false;
            settings_selected = true;
            menuChange = true;
            break;
        }
      } else if (soundMenu_selected == true) {
        switch (menuCounter) {
          case 0:
            if (fileSel) {
              fileSet = true;
              soundFile = SD.open(files[curFile]);
            }
            fileSel = !fileSel;
            break;
          case 1:
            soundMenu_selected = false;
            settings_selected = true;
            menuChange = true;
            break;
        }
      } else if (timeMenu_selected == true) {
        switch (menuCounter) {
          case 0:
            changeHour = !changeHour;
            break;
          case 1:
            changeMin = !changeMin;
            break;
          case 2:
            timeMenu_selected = false;
            settings_selected = true;
            menuChange = true;
            break;
        }
      }
      refreshLCD = true;
    }
    sei();    
  }

  void updateCursorPosition() {

    //needed for main menu because there is only one place for the curosr to go
    if (mainMenu_selected == true) {
      lcd.setCursor(0, 1);
      lcd.print(">");
      return;
    }

    if (settings_selected == true) {
      //delete previous cursor
      lcd.setCursor(0, 0);
      lcd.print(" ");
      lcd.setCursor(0, 1);
      lcd.print(" ");
      lcd.setCursor(8, 0);
      lcd.print(" ");
      lcd.setCursor(8, 1);
      lcd.print(" ");
      switch (menuCounter) {
        case 0:
          lcd.setCursor(0, 0);  //1st line, 1st block
          lcd.print(">");
          break;
        case 1:
          lcd.setCursor(0, 1);  //2nd line, 1st block
          lcd.print(">");
          break;
        case 2:
          lcd.setCursor(8, 0);  //2nd line, 1st block
          lcd.print(">");
          break;
        case 3:
          lcd.setCursor(8, 1);  //2nd line, 1st block
          lcd.print(">");
          break;
      }
    } else if (soundMenu_selected == true) {
      lcd.setCursor(0, 0);
      lcd.print(" ");
      lcd.setCursor(11, 1);
      lcd.print(" ");
      switch (menuCounter) {
        case 0:
          if (fileSel) {
            lcd.setCursor(0, 0);  //1st line, 1st block
            lcd.print("X");
          } else {
            lcd.setCursor(0, 0);  //1st line, 1st block
            lcd.print(">");
          }
          break;
        case 1:
          lcd.setCursor(11, 1);
          lcd.print(">");
      }
    } else if (timeMenu_selected == true) {
      lcd.setCursor(0, 0);
      lcd.print(" ");
      lcd.setCursor(0, 1);
      lcd.print(" ");
      lcd.setCursor(11, 1);
      lcd.print(" ");
      switch (menuCounter) {
        case 0:
          if (changeHour) {
            lcd.setCursor(0, 0);  //1st line, 1st block
            lcd.print("X");
          } else {
            lcd.setCursor(0, 0);  //1st line, 1st block
            lcd.print(">");
          }
          break;
        case 1:
          if (changeMin) {
            lcd.setCursor(0, 1);  //1st line, 1st block
            lcd.print("X");
          } else {
            lcd.setCursor(0, 1);  //1st line, 1st block
            lcd.print(">");
          }
          break;
        case 2:
          lcd.setCursor(11, 1);
          lcd.print(">");
          break;
      }
    } else if (enableMenu_selected == true) {
      lcd.setCursor(0, 0);
      lcd.print(" ");
      lcd.setCursor(0, 1);
      lcd.print(" ");
      lcd.setCursor(11, 1);
      lcd.print(" ");
      switch (menuCounter) {
        case 0:
          lcd.setCursor(0, 0);  //1st line, 1st block
          lcd.print(">");
          break;
        case 1:
          lcd.setCursor(0, 1);
          lcd.print(">");
          break;
        case 2:
          lcd.setCursor(11, 1);
          lcd.print(">");
          break;
      }
    }
  }

  void updateLCD() {

    if (mainMenu_selected == true) {
      menuCounter = 0;
      printMain();
    }
    //-------------------
    else if (settings_selected == true) {
      menuCounter = 0;
      printSettings();
    }
    //-------------------
    else if (soundMenu_selected == true) {
      menuCounter = 0;
      printSound();
    }
    //-------------------
    else if (enableMenu_selected == true) {
      menuCounter = 0;
      printEnable();
    } else if (timeMenu_selected == true) {
      menuCounter = 0;
      printTime();
    }
    updateCursorPosition();
  }

  void printMain() {
    currentTime = rtc.now();
    lcd.clear();
    lcd.setCursor(1, 0);  //1st line, 2nd block
    lcd.print("BAD1.0");  //text
    //----------------------
    lcd.setCursor(1, 1);    //2nd line, 2nd block
    lcd.print("Settings");  //text

    lcd.setCursor(8, 0);   //1st line, 14th block
    lcd.print("Status:");  //counts - text
    if (enable) {
      lcd.print(1);
    } else if (configModeEnabled) {
      lcd.print(2);
    } else {
      lcd.print(0);
    }

    lcd.setCursor(11, 1);
    lcd.print(currentTime.hour());
    lcd.print(":");
    if (currentTime.minute() < 10) {
      lcd.print(0);
      lcd.print(currentTime.minute());
    } else {
      lcd.print(currentTime.minute());
    }
  }

  void printSettings() {
    lcd.clear();
    lcd.setCursor(1, 0);
    lcd.print("Enable");
    lcd.setCursor(1, 1);
    lcd.print("Sound");
    lcd.setCursor(9, 0);
    lcd.print("Time");
    lcd.setCursor(9, 1);
    lcd.print("Back");
  }

  void printSound() {
    lcd.clear();
    lcd.setCursor(1, 0);
    if (curFile == -1) {
      lcd.print("Select File");
    } else {
      lcd.print(soundFile.name());
    }

    lcd.setCursor(1, 1);
    lcd.print("");
    lcd.setCursor(12, 1);
    lcd.print("Back");
  }

  void printEnable() {
    lcd.clear();
    lcd.setCursor(1, 0);
    lcd.print("Enable");
    lcd.setCursor(1, 1);
    lcd.print("Disable");
    lcd.setCursor(12, 1);
    lcd.print("Back");
    lcd.setCursor(8, 0);
    lcd.print("Status:");
    lcd.setCursor(15, 0);
    if (enable) {
      lcd.print(1);
    } else if (configModeEnabled) {
      lcd.print(2);
    } else {
      lcd.print(0);
    }
  }


  void printTime() {
    lcd.clear();
    lcd.setCursor(1, 0);
    lcd.print("Hour");
    lcd.setCursor(1, 1);
    lcd.print("Minutes");
    lcd.setCursor(12, 1);
    lcd.print("Back");
    lcd.setCursor(11, 0);
    lcd.print(screenTime);
  }

  void readFiles() {
    File temp;
    while ((temp = dir.openNextFile())) {
      if ((temp.isDirectory() == false) && (strstr(temp.name(), ".WAV") != NULL)) {
        files[numFiles] = malloc(sizeof(char) * strlen(temp.name()) + 1);
        strncpy(files[numFiles], temp.name(), strlen(temp.name()));
        files[numFiles][strlen(temp.name())] = '\0';
        numFiles++;
      }
    }
    temp.close();
  }

  void saveConfig() {
    SD.remove("cur.txt");
    curConfig = SD.open("cur.txt", FILE_WRITE);
    if (curConfig == NULL) {
      fileCheck = false;
    }
    curConfig.println(soundFile.name());
    curConfig.println(hours);
    curConfig.println(minutes + '\n');
    curConfig.close();
  }


  void readConfig() {
    curConfig = SD.open("cur.txt", FILE_READ);
    if (curConfig != NULL) {
      curFile = 0;
      char sound[12];
      soundFile = SD.open(strcpy(sound,curConfig.readStringUntil('\n').c_str()));
      hours = curConfig.readStringUntil('\n').toInt();
      minutes = curConfig.readStringUntil('\n').toInt();
      sprintf(screenTime, "%02d:%02d", hours, minutes);
    }
    curConfig.close();
  }

  void configMode() {
  }

Use Instructions

The BAD was made in way that makes interacting with the device simple and easy to understand. On startup, the user is presented with the main screen. On the main screen, the current status of the device is shown as well as the current time. The status can appear as a 0, 1, or 2. When the status shows 0, the BAD is not configured. If the status shows 1, the BAD was equipped and will play the selected sound file at the time that was set. If the status shows 2, the BAD is in hands off mode and will configure itself based on the contents found in the handsOff file on the SD. There is only one option to select on the main menu which takes the user to the setting menu.

In the settings menu the user is presented with 4 options which are enable, time, sound, and back. If the user plans to configure the BAD theirself, the time and sound menus should be visited before the enable menu.

In the sound menu, the user can scroll through the .WAV files that were found on the SD card. To select a file, press the rotary encoder, and then turn it to scroll through the found files. The file that is desired can be selected by pressing the rotary encoder again.

In the time menu, the user selects what time they want the device to start outputting sound. The time is in 24 hour time. Both the hour and minute can be set.

The enable menu allows the user to arm the device. selecting enable once will set the BAD to status 1 and take the user back to the home screen. Going back into the enable menu and selecting enable again will set the BAD to status 2. Selecting disable will always Set the BAD to status 0.

For best sound quality, when uploading .WAV files to the SD card, make sure the resolution is set to 8-bit and the sampling rate is set to 32 kHz.

Bill of Materials

The project in question was funded by esteemed Michigan Technological University researchers, who have a large budget but had strict cost requirements to ensure that the project’s specifications were met without exceeding the budget. To achieve this goal, the team documented every item purchased in the bill of materials, including the quantity, source, cost, and notes about each product.

It is worth noting that not all items purchased were intended for use in the final product, but rather served as prototypes or placeholders for future versions. This allowed the team to explore different options and make informed decisions based on the project’s evolving needs. Furthermore, items were primarily sourced from domestic suppliers to expedite delivery times. They were also chosen to be cost effective so that anybody can replicate the project so that researchers can economically deploy their own versions of the device.

Links and Resources

Playing sound from an Arduino

Arduino library for lcd display

Arduino library for playing sounds out of an Arduino

Arduino library for connecting the RTC to the Arduino

Wiring and code for the LCD and Rotary Encoder