Matthew Call
Published © MIT

Smart room controller and MP3 player

Automatically turn on room lights, outlets, and music as soon as you sit at your desk. Turns everything off after you leave.

IntermediateShowcase (no instructions)203
Smart room controller and MP3 player

Things used in this project

Hardware components

Photon 2
Particle Photon 2
×1
Generic Push button
×3
Ultrasonic Sensor - HC-SR04 (Generic)
Ultrasonic Sensor - HC-SR04 (Generic)
×1
Adafruit BME280
×1
Graphic OLED, 128 x 64
Graphic OLED, 128 x 64
×1
Adafruit Neopixel Ring 12 pixels
×1
Speaker: 3W, 4 ohms
Speaker: 3W, 4 ohms
×1
RGB LED Encoder
×1
DFPlayer - A Mini MP3 Player
DFRobot DFPlayer - A Mini MP3 Player
×1
16GB MicroSD card
×1

Hand tools and fabrication machines

Bambu X1 Carbon 3D printer

Story

Read more

Custom parts and enclosures

Solidworks main enclosure model

Solidworks enclosure backplate model.

Schematics

Fritzing

Use a Particle Photon 2 to control a variety of components including: several buttons, an encoder, an OLED screen, BME to get temperature, and an MP3 player.

Schematics

Code

Main Code

C/C++
Code to control the features of this project.
/* 
 * Project midterm_remote
 * Author: Matthew Call
 * Date: 7/9/24
 */

// Most of the header is in a seperate file to increase readability.
#include "header_includes.h"

SYSTEM_MODE(SEMI_AUTOMATIC);

// SYSTEM_THREAD(ENABLED);

void setup() {
    Serial.begin(9600);
    waitFor(Serial.isConnected, 2500);

    // currently disabled because I'm not in range of this network.
    /* WiFi.on();
    WiFi.clearCredentials();
    WiFi.setCredentials("IoTNetwork");
    WiFi.connect();
    while(WiFi.connecting()) {
        Serial.printf(".");
    } */
    
    // encloder switch RGB
    pinMode(ENCODERSWITCHRED, OUTPUT);
    pinMode(ENCODERSWITCHGREEN, OUTPUT);
    pinMode(ENCODERSWITCHBLUE, OUTPUT);

    // ultrasonic sensor stuff
    pinMode(TRIGPIN, OUTPUT);  
	pinMode(ECHOPIN, INPUT);

    // pixel stuff
    pixel.begin();
    pixel.clear();
    pixel.show();

    // OLED stuff
    myOLED.begin(SSD1306_SWITCHCAPVCC, 0x3C);
    myOLED.clearDisplay();
    myOLED.setTextSize(1);
    myOLED.setTextColor(1, 0);
    myOLED.setCursor(0, 0);
    myOLED.printf("BME Monitor\n------------\n");
    myOLED.display();
    delay(2000);

    // BME stuff.
    bool status = bmeSensor.begin(sensorAddress);
    if(status == false) {
        Serial.printf("BME280 at address %02X failed to start", sensorAddress);
    }

    // MP3 Player stuff.
    pinMode(MP3BUSYPIN, INPUT);
    Serial.printf("Initializing MP3 Player. Please wait...\n");
    Serial2.begin(9600);
    mp3Player.begin(Serial2);
    delay(1000);

    // check that the MP3 is connected.
    if (!mp3Player.begin(Serial2)) {  //Use softwareSerial to communicate with mp3.
        Serial.printf("Unable to begin:\n");
        Serial.printf("1.Please recheck the connection!\n");
        Serial.printf("2.Please insert the SD card!\n");
        while(true);
    }

    // default startup output settings.
    mp3Player.loopFolder(1);
    mp3Player.enableLoopAll();
    mp3Player.pause();
    encoder.write(62);
    encoderInput = 62;
    mp3Player.volume(20);
    cycleHueBulbs(HueBlue, 255);
    Serial.printf("playing quiet playlist.");
}

void loop() {
    //start the timer
    currentTime = millis();

    // ultrasonic sensor stuff
    digitalWrite(TRIGPIN, LOW);  
	  delayMicroseconds(2);  
  	digitalWrite(TRIGPIN, HIGH);  
  	delayMicroseconds(10);  
  	digitalWrite(TRIGPIN, LOW);
    duration = pulseIn(ECHOPIN, HIGH);
    distance = (duration * 0.0343) / 2;

    // Pause the playback and turn everything off if I'm away from my desk for more than 10 seconds. 
    if(distance > 70) {
        if((currentTime - previousTimeSensor) > 10000) {
            mp3Player.pause();
            toggleProximityStart = false;
            wemoToggleState = false;
            digitalWrite(ENCODERSWITCHRED, HIGH);
            digitalWrite(ENCODERSWITCHGREEN, HIGH);
            digitalWrite(ENCODERSWITCHBLUE, HIGH);
            pixel.setBrightness(0);
            pixel.show();
            myOLED.clearDisplay();
            myOLED.setCursor(0, 0);
            myOLED.display();
            cycleHueBulbs(blue, 0);
            wemoWrite(MYWEMO, LOW);
            wemoWrite(MYWEMO2, LOW);
            wemoWrite(MYWEMO3, LOW);
            previousTimeSensor = currentTime;
        }
    }

    // Auto start if sensor reads less than 70cm.
    else {

        // Reset the 10 second pause timer if I move back into range.
        previousTimeSensor = previousTime;
        
        // read the encoder input
        encoderInput = encoder.read();

        //start a cycle timer to prevent excessive inputs
        if ((currentTime - previousTime) > 50) {
            currentTrack = mp3Player.readCurrentFileNumber(Serial2);
            
            // change the encoder color depending on the playlist. This encoder turns on if the output is LOW.
            if(encoderSwitchToggle == true) {
                digitalWrite(ENCODERSWITCHRED, HIGH);
                digitalWrite(ENCODERSWITCHGREEN, LOW);
                digitalWrite(ENCODERSWITCHBLUE, HIGH);
            }
            else {
                digitalWrite(ENCODERSWITCHRED, HIGH);
                digitalWrite(ENCODERSWITCHGREEN, HIGH);
                digitalWrite(ENCODERSWITCHBLUE, LOW);
            }
            if(toggleStartStop == false) {
                digitalWrite(ENCODERSWITCHRED, LOW);
                digitalWrite(ENCODERSWITCHGREEN, HIGH);
                digitalWrite(ENCODERSWITCHBLUE, HIGH);
            }

            // prevent the encoder input from going out of range
            if(encoderInput <= 0) {
                encoderInput = 0;
                encoder.write(0);
            }
            else if(encoderInput >= 96) {
                encoderInput = 96;
                encoder.write(96);
            }

            // get temperature and converts it to farhenheight.
            tempCel = bmeSensor.readTemperature();
            tempFar = celToFar(tempCel);

            // turn on the wemos if temperature goes over 75 degrees farhenheight.
            if(tempFar > 75) {
                if(wemoToggleState == false) {
                    wemoWrite(MYWEMO, LOW);
                    wemoWrite(MYWEMO2, LOW);
                    wemoWrite(MYWEMO3, HIGH);
                    wemoToggleState = !wemoToggleState;
                }
            }

            // turn the wemos off if temperature goes below 75 degrees farhenheight.
            else {
                if(wemoToggleState == true) {
                    if(tempFar < 75) {
                        wemoWrite(MYWEMO, HIGH);
                        wemoWrite(MYWEMO2, HIGH);
                        wemoWrite(MYWEMO3, LOW);
                        wemoToggleState = !wemoToggleState;
                    }
                }
            }

            // get neopixel colors based on the MBE readings while keeping the values inside the analog output limits.
            color = tempFar;
            if((color * 2) >= 254) {
                color = 127;
            }
            if((color * 2) <= 1) {
                color = 1;
            }

            // maps volume percentage to encoder.
            slopeVolume = findSlope(x1EncoderLow, y1VolumeLow, x2EncoderHigh, y2VolumeHigh);
            yInterceptVolume = findYInstercept(slopeVolume, x1EncoderLow, y1VolumeLow);
            mappedEncoderToVolume = findLinearConversion(slopeVolume, yInterceptVolume, encoderInput);

            // maps pixelcount to encoder
            slopePixel = findSlope(x1EncoderLow, y1PixelLow, x2EncoderHigh, y2PixelHigh);
            yInterceptPixel = findYInstercept(slopePixel, x1EncoderLow, y1PixelLow);
            mappedEncoderToPixel = findLinearConversion(slopePixel, yInterceptPixel, encoderInput);
            pixelCount = mappedEncoderToPixel;

            // maps huebulb and neopixel brightness to encoder. Due to input lag issues with the huebulbs, this only works for the neopixel brightness currently.
            slopeHueBulb = findSlope(x1EncoderLow, y1BrightnessLow, x2EncoderHigh, y2BrightnessHigh);
            yInterceptHueBulb = findYInstercept(slopeHueBulb, x1EncoderLow, y1BrightnessLow);
            mappedEncoderToBrightness = findLinearConversion(slopeHueBulb, yInterceptHueBulb, encoderInput);
            pixel.setBrightness(mappedEncoderToBrightness * 0.5);
            
            // if the pause button is clicked, turn all the huebulbs red, pause the music, and turn off the wemos. Pressing again plays the music again and turns the huebulbs to the playlist color.
            if(buttonStartStop.isClicked()) {
                if(toggleStartStop == true) {
                    mp3Player.pause();
                    toggleStartStop = !toggleStartStop;
                    cycleHueBulbs(HueRed, 255.0);
                    wemoWrite(MYWEMO, LOW);
                    wemoWrite(MYWEMO2, LOW);
                    wemoWrite(MYWEMO3, LOW);
                }
                else {
                    if(toggleProximityStart == true) {
                        mp3Player.start();
                    }
                    toggleStartStop = !toggleStartStop;
                    if(!encoderSwitchToggle) {
                        Serial.printf("playing quiet playlist.");
                        mp3Player.volume(mappedEncoderToVolume);
                        cycleHueBulbs(HueBlue, mappedEncoderToBrightness);
                        
                    }
                    else {
                        Serial.printf("playing loud playlist.");
                        mp3Player.volume(mappedEncoderToVolume);
                        cycleHueBulbs(HueGreen, mappedEncoderToBrightness);
                        
                    }
                }
            }
               
            // switch playlists, encoder switch LED color, and huebulb color if the encoder switch is clicked.
            if(encoderButton.isClicked()) {
                if(toggleStartStop == true) {
                    encoderSwitchToggle = !encoderSwitchToggle;
                    if(!encoderSwitchToggle) {
                        mp3Player.start();
                        Serial.printf("playing quiet playlist.");
                        cycleHueBulbs(HueBlue, mappedEncoderToBrightness);
                    }
                    else {
                        mp3Player.start();
                        Serial.printf("playing loud playlist.");
                        cycleHueBulbs(HueGreen, mappedEncoderToBrightness);
                    } 
                } 
            }

            // choose the playlist depending on the encoder switch toggle. Prevents looping to different playlists after completing the current one.
            if(!encoderSwitchToggle) {
                if(currentTrack > 10) {
                    mp3Player.loopFolder(1);
                    Serial.printf("playing quiet playlist.");
                }
            }
            else {
                if(currentTrack < 11) {
                    mp3Player.loopFolder(2);
                    Serial.printf("playing loud playlist.");
                }
            }

            // if the encoder input changes, update the neopixel output.
            if(encoderInput != previousEncoderInput) {

                // sets color based on temperature. Pure blue at 32 degrees farenheight, pure red at 100 degrees farenheight.
                pixelFill(0, pixelCount, (color * 2) - 64, 0, 200 - (color * 2));

                if(tempFar < previousTempFar) {
                    pixel.clear();
                    pixel.show();
                    pixelFill(0, pixelCount, (color * 2) - 64, 0, 200 - (color * 2));
                }

                // turns pixels off if the current pixelcount is less than the previous pixelcount then turns on the correct pixelcount
                if(pixelCount < previousInputPixel) {
                    pixel.clear();
                    pixel.show();
                    pixelFill(0, pixelCount, (color * 2) - 64, 0, 200 - (color * 2));
                }
                
                // sets volume to match the encoder and pixelcount
                mp3Player.volume(mappedEncoderToVolume);
            }

            // check encoder input for 0.
            if(encoderInput == 0) {
                checkEncoderPositionZero();
                if(encoderInput != previousEncoderInput) {
                    mp3Player.volume(mappedEncoderToVolume);
                }
            }

            // prints data to the OLED display at 1 second intervals
            if((currentTime - previousTime2) > 1000) {

                // change the color based on temperature.
                if(tempFar != previousTempFar) {
                    pixel.clear();
                    pixel.show();
                    pixelFill(0, pixelCount, (color * 2) - 64, 0, 200 - (color * 2));
                }

                // display the temperature on the OLED.
                myOLED.clearDisplay();
                myOLED.setCursor(0, 0);
                myOLED.printf("Temp: %0.2f F. \n---\nCurrent Track:\n \n", tempFar);
                
                // include the file with the playlist artists and song names and output them to the OLED
                #include "playlist.h"
                myOLED.display(); 
                Serial.printf("\nTemp in Fahrenheit is: %0.2f.\n", tempFar);
                previousTime2 = currentTime;

                // prints the Ultrasonic sensor distance to the serial monitor.
                Serial.printf("\nDistance: %0.4f\n", distance);

                // ensures that the music keeps looping even if it was paused. Without this, the music stops when the current song finishes.
                mp3Player.enableLoopAll();
            }

            // match previous inputs to current inputs.
            previousTempFar = tempFar;
            previousInputPixel = pixelCount;
            previousEncoderInput = encoderInput;
            previousEncoderToBrightness = mappedEncoderToBrightness;
            previousTime = currentTime;

            // Turn on the lights and music if the ultrasonic sensor detects someone in range.
            if(toggleProximityStart == false && toggleStartStop == true) {
                toggleProximityStart = !toggleProximityStart;
                pixelFill(0, pixelCount, (color * 2) - 64, 0, 200 - (color * 2));
                mp3Player.start();
            }
        }
    }  
}

// Functions
// converts Celsius to Fahrenheit
float celToFar(float inputTempCel) {
    float outputTempFar = (inputTempCel * 1.8) + 32.0;
    return outputTempFar;
}

// pixelFill function for lighting up a changable quantity of mini pixels.
void pixelFill(int startPixel, int endPixel, int red, int green, int blue) {
    for(int i = startPixel; i <= endPixel; i++) {
        pixel.setPixelColor(i, red, green ,blue);
        pixel.show();
    }
    checkEncoderPositionZero();

}

// checks if the encoder output is 0 and turns off the all minipixels if true.
void checkEncoderPositionZero() {
    if(encoderInput == 0) {
        pixel.clear();
        pixel.show();
    }
}

void cycleHueBulbs(int playlistColor, float brightness) {
    for(int i = 1; i <= 6; i++) {
        setHue(i, true, playlistColor, 255, 255);
    }
}

Header file

C/C++
File to include to improve readability for the main file.
#ifndef _HEADER_INCLUDES_H_
#define _HEADER_INCLUDES_H_

#include "Particle.h"
#include "Adafruit_BME280.h"
#include "Adafruit_SSD1306.h"
#include "neopixel.h"
#include "linear_conversion.h"
#include "IoTClassroom_CNM.h"
#include "DFRobotDFPlayerMini.h"
#include "Encoder.h"

// MP3 player constants, variable, objects
const int MP3BUSYPIN = D15;
const int MP3TXPIN = D4;
const int MP3RXPIN = D5;
const int BUTTONPINSTARTSTOP = D10;
bool toggleStartStop = 1;
bool togglePlaylist;
unsigned int currentFolder, currentTrack;
DFRobotDFPlayerMini mp3Player;
Button buttonStartStop(BUTTONPINSTARTSTOP);

// Hue Bulb and Wemo constants, variable, objects
const int BULB1 = 1;
const int BULBALL[] = {1, 2, 3, 4, 5, 6};
// lava lamp
const int MYWEMO = 0;
// desk outlet
const int MYWEMO2 = 1;
// fan
const int MYWEMO3 = 2;
bool wemoToggleState = true;
int color, previousColor, playlistColor;

// Encoder constants, variable, objects
const int ENCODERPINA = D8;
const int ENCODERPINB = D9;
const int ENCODERSWITCHPIN = D7;
const int ENCODERSWITCHRED = D16;
const int ENCODERSWITCHGREEN = D6;
const int ENCODERSWITCHBLUE = D14;
bool encoderSwitchToggle;
int encoderInput = 1;
float previousEncoderInput;
Encoder encoder(ENCODERPINA, ENCODERPINB); // used to control volume and neopixel brightness
Button encoderButton(ENCODERSWITCHPIN); // used to switch playlists.

// Ultrasonic Sensor constants, variable, objects
const int TRIGPIN = D18;
const int ECHOPIN = D11;
float duration, distance; 
bool toggleProximityStart;

// BME/OLED constants, variable, objects
const int SDAPIN = D0;
const int SCLPIN = D1;
const int OLED_RESET = -1;
byte sensorAddress = 0x76;
float tempCel, tempFar, previousTempFar;
Adafruit_SSD1306 myOLED(OLED_RESET);
Adafruit_BME280 bmeSensor;

// Neopixel constants, variable, objects
int pixelCount = 12;
Adafruit_NeoPixel pixel(pixelCount, SPI1, WS2812B);

// Linear conversion variables
// Linear conversion HueBulb and neopixel brightness
float x1EncoderLow = 0.0;
float y1BrightnessLow = 0.0;
float x2EncoderHigh = 96.0;
float y2BrightnessHigh = 255.0;
float slopeHueBulb, yInterceptHueBulb, mappedEncoderToBrightness, previousEncoderToBrightness;

// Linear conversion volume
float y1VolumeLow = 0.0;
float y2VolumeHigh = 30.0;
float slopeVolume, yInterceptVolume, mappedEncoderToVolume, previousInputVolume;

// Linear conversion pixels
float y1PixelLow = 0.0;
float y2PixelHigh = 12.0;
float slopePixel, yInterceptPixel, mappedEncoderToPixel, previousInputPixel;

// Timer variables and objects
int currentTime, previousTime, previousTime2, previousTimeSensor;

// Function Prototypes
void checkEncoderPositionZero();
void pixelFill(int startPixel, int endPixel, int red, int green, int blue);
float celToFar(float inputTempCel);
void cycleHueBulbs(int playlistColor, float brightness);

#endif

Playlist

C/C++
File to hold the playlist artist and song names. The listed tracks are the ones I currently have on the SD card in the MP3 player. Playlist 2 starts with track 11.
#ifndef _PLAYLIST_H_
#define _PLAYLIST_H_

switch (currentTrack) {
    case 1: 
        myOLED.printf("Disturbed: \nRemnants");
        break;
    case 2: 
        myOLED.printf("Fastball: \nThe Way");
        break;
    case 3: 
        myOLED.printf("Periphery: \nPriestess");
        break;
    case 4: 
        myOLED.printf("Kansas: \nDust In The Wind");
        break;
    case 5: 
        myOLED.printf("Greenday: \nWake Me Up When \nSeptember Ends");
        break;
    case 6: 
        myOLED.printf("War of Ages: \nInstrumental");
        break;
    case 7: 
        myOLED.printf("Slipknot: \nSnuff");
        break;
    case 8: 
        myOLED.printf("Disturbed: \nThe Sound of\nSilence");
        break;
    case 9: 
        myOLED.printf("Demon Hunter: \nThe Tides Begin\nTo Rise");
        break;
    case 10: 
        myOLED.printf("10 Years\nThe Autumn\nEffect");
        break;
    case 11: 
        myOLED.printf("Breaking Benjamin:\nBlow Me Away");
        break;
    case 12: 
        myOLED.printf("Trivium:\nThe Sin And The\nSentence");
        break;
    case 13: 
        myOLED.printf("Avenged Sevenfold:\nUnholy Confessions");
        break;
    case 14: 
        myOLED.printf("Avenged Sevenfold:\nAfterlife");
        break;
    case 15: 
        myOLED.printf("Trivium:\nInto The Mouth Of\nHell We March");
        break;
    case 16: 
        myOLED.printf("Dragonforce:\nSoldiers Of The\nWasteland");
        break;
    case 17: 
        myOLED.printf("Periphery:\nFlatline");
        break;
    case 18: 
        myOLED.printf("Avenged Sevenfold:\nStrength Of The\nWorld");
        break;
    case 19: 
        myOLED.printf("Periphery:\nLune");
        break;
    case 20: 
        myOLED.printf("Trivium:\nThe Broken One");
        break;
    default:
        break;
};
#endif

Github repository

Credits

Matthew Call
2 projects • 4 followers
I worked in a solar electronics plant for 6 years. I'm taking a course to code microcontrollers and design, prototype, and build devices.
Contact

Comments

Please log in or sign up to comment.