John R McAlpine V Mac
Published © GPL3+

Garage Door Sesame Interface

A Garage Door Automation system as it should be. Sensor feedback, phone notifications, Alexa skills, logged temperature and humidity.

IntermediateFull instructions provided6 hours4,524
Garage Door Sesame Interface

Things used in this project

Hardware components

Photon
Particle Photon
×1
Amazon Echo
Amazon Alexa Amazon Echo
×1
DHT11 Temperature & Humidity Sensor (4 pins)
DHT11 Temperature & Humidity Sensor (4 pins)
×1
Particle Relay Shield
×1

Software apps and online services

Ubidots
Ubidots
ControlEverything.com mobicle.io
Amazon Alexa service
IFTTT Amazon Alexa service

Story

Read more

Schematics

garage door controller schematic

Code

Updated code

Arduino
Includes a more reliable piettetech dht driver, Thingspeak instead of ubidots and takes advantage of the RTOS software timers
// This #include statement was automatically added by the Particle IDE.
#include "PietteTech_DHT/PietteTech_DHT.h"


 /*                           +-----+
 *                 +----------| USB |----------+
 *                 |          +-----+       *  |
 *                 | [ ] VIN           3V3 [ ] |
 *                 | [ ] GND           RST [ ] |
 *                 | [ ] TX           VBAT [ ] |
 *                 | [ ] RX  [S]   [R] GND [ ] |
 *                 | [ ] WKP            D7 [ ] |
 *                 | [ ] DAC +-------+  D6 [ ] |
 *   Opensensor -->| [*] A5  |   *   |  D5 [ ] |
 * Closedsensor -->| [*] A4  |Photon |  D4 [*] |<-- DHT21 AM2301 Sensor
 *                 | [*] A3  |       |  D3 [ ] |
 *                 | [ ] A2  +-------+  D2 [ ] |
 *                 | [ ] A1             D1 [ ] |
 *  Analog0     -->| [ ] A0             D0 [*] |<-- Garage Door Switch Relay
 *                 |                           |
 *                  \    []         [______]  /
 *                   \_______________________/
 *
 *
 */

const String key = "YOUR_KEY_GOES_HERE"; //write key

//--------------Begin DHT------------------
// Example testing sketch for various DHT humidity/temperature sensors
// Written by ladyada, public domain

#define DHTPIN D4     // what pin we're connected to

// Uncomment whatever type you're using!
//#define DHTTYPE DHT11		// DHT 11 
//#define DHTTYPE DHT22		// DHT 22 (AM2302)
#define DHTTYPE DHT21		// DHT 21 (AM2301)

// Connect pin 1 (on the left) of the sensor to +5V
// Connect pin 2 of the sensor to whatever your DHTPIN is
// Connect pin 4 (on the right) of the sensor to GROUND
// Connect a 10K resistor from pin 2 (data) to pin 1 (power) of the sensor


//declaration
void dht_wrapper(); // must be declared before the lib initialization

// Lib instantiate
PietteTech_DHT DHT(DHTPIN, DHTTYPE, dht_wrapper);
// globals
unsigned int DHTnextSampleTime;	    // Next time we want to start sample
bool bDHTstarted;		    // flag to indicate we started acquisition
int n;                              // counter

//---------------End DHT-------------------


unsigned int particleinterval = 100;
unsigned int particlefuncinterval = 10000;
unsigned int ubiinterval = 15000;
unsigned int dhtinterval = 5000;

int garageDoorFunction(String command);

int rssival = 0;
int doorstatus = 1;
int doorcounter = 0;
int closedsensorpin = A4;
int opensensorpin = A5; 
const int open = 0;
const int closed = 1;

// Define the pins we're going to call pinMode on
int led = D6;  // You'll need to wire an LED to this one to see it blink.
int led2 = D7; // This one is the built-in tiny one to the right of the USB jack


double analog1 = 0;
double analog2 = 0;
double analog1raw = 0;
double analog2raw = 0;
double garagetemp = 70;
double readtemp = 70;
double garagehumid = 0;


char publishString[40];
char doorpublishstring[40];
char doorstatusstring[40];

SYSTEM_MODE(SEMI_AUTOMATIC); //allows for control of Spark.connect() and Spark.process()

// This routine runs only once upon reset
Timer task50ms(50, dodoor);
Timer task10sec(10000,dopublish);
Timer task15sec(15000,updateThingspeak);
Timer taskdht(5000, readdht);

void setup() {
    task50ms.start();
    task10sec.start();
    task15sec.start();
    taskdht.start();
    
    pinMode(led, OUTPUT);
    pinMode(led2, OUTPUT);
    pinMode(A0, INPUT);
    pinMode(A1, INPUT);
    Serial1.begin(230400);   // open serial over USB
    //ip = {"IP:", WiFi.localIP()};
    //String ip = WiFi.localIP();

    Particle.function("GarageDoor", garageDoorFunction);
    Particle.variable("analog1", &analog1, DOUBLE);  //variables update automatically. Max 4 per particle
    Particle.variable("doorcounter", doorcounter);
    //Particle.variable("doorstatus", &doorstatus, INT); //variables are viewable on mobicle 
    Particle.variable("doorstatus", doorstatusstring, STRING); //variables are viewable on mobicle 
    Particle.variable("garagetemp", &garagetemp, DOUBLE); //variables are viewable on mobicle
    Particle.variable("garagehumid", &garagehumid, DOUBLE); //variables are viewable on mobicle
    
    pinMode(opensensorpin, INPUT_PULLUP);
    pinMode(closedsensorpin, INPUT_PULLUP);

    pinMode(D0, OUTPUT);

    //Do DHTINIT
    pinMode(DHTPIN, INPUT_PULLUP); //sensor needs pullup resistor.
	//End DHTINIT

//-------------Begin UBIDOTS---------------
//    request.hostname = "things.ubidots.com";
//    request.port = 80;
//    request.path = "/api/v1.6/variables/"VARIABLE_ID"/values?token="TOKEN;
//--------------End UBIDOTS----------------
}

// This wrapper is in charge of calling
// mus be defined like this for the lib work
void dht_wrapper() {
    DHT.isrCallback();
}

void loop() {
    if (Particle.connected() == false){
            Particle.connect();
    }
    analog1raw = analogRead(A0);
    analog1 = (analog1 *63 +analog1raw *3.3/4095)/64;
    analog2raw = analogRead(A1);
    analog2 = (analog2 *63 +analog2raw *3.3/4095)/64;

    doDht();
}

void dopublish(){
    rssival = WiFi.RSSI();
    sprintf(publishString,"%d",rssival);
                
    Particle.publish("RSSI",publishString);
}

void readdht() {
    if (!bDHTstarted) {		// start the sample
    Serial.print("\n");
    Serial.print(n);
    Serial.print(": Retrieving information from sensor: ");
    DHT.acquire();
    bDHTstarted = true;
    }
}

void dodoor() {
    
        digitalWrite(led, !digitalRead(led));   // Turn ON the LED pins
        digitalWrite(led2, !digitalRead(led2));
    
        int opensense = digitalRead(opensensorpin);
        int closedsense = digitalRead(closedsensorpin);


        if (opensense == LOW) // high,0 = open door, low,1 = closed door
        {  
            if (doorstatus == closed) //check the previous state, if closed, then generate an event
            {
                strcpy(doorpublishstring, "Open");
                Particle.publish("GARAGEDOOR",doorpublishstring);
                doorcounter++;
            }
            doorstatus = open;
            strcpy(doorstatusstring, "Open"); //update particle variable.
        } 
        else if (closedsense == LOW)
        {
            if (doorstatus == open) //check the previous state, if closed, then generate an event
            {
                strcpy(doorpublishstring, "Closed");
                Particle.publish("GARAGEDOOR",doorpublishstring);
                doorcounter++;
            }
            doorstatus = closed;
            strcpy(doorstatusstring, "Closed");  //update particle variable.
        }
            else 
            {  //if door is neither closed or open, it is in "limbo" 
                strcpy(doorstatusstring, "Limbo");  //update particle variable.
            }
}


/*******************************************************************************
 * Function Name  : garageDoor
 * Description    : based on door status, opens or closes the door
 * Input          : analog 5 - digital input.
 * Output         : digital 1 - relay to blip and open or close the door
 * Return         : Value of the pin (0 or 1) in INT type
                    Returns a negative number on failure
 *******************************************************************************/
int garageDoorFunction(String sesame)
{
    if(sesame.startsWith("C"))
	{
		if (doorstatus == open)
		{
	        pinMode(0, OUTPUT);
		    digitalWrite(0, 1);
		    delay(250);
		    digitalWrite(0, 0);
		return 1;
		}
	}
	else if (sesame.startsWith("O"))
		{
        if (doorstatus == closed)
		{
	        pinMode(0, OUTPUT);
		    digitalWrite(0, 1);
		    delay(250);
		    digitalWrite(0, 0);
		return 2;
		}
	}
	else if (sesame.startsWith("X"))
		{
        if (doorstatus == closed){
            return 999;
        } else
        return 123;
		}
	return -1;
	
}



/*******************************************************************************
 * Function Name  : updateThingspeak
 * Description    : sends variables to ts
 * Input          : doorstatus global variables
 * Output         : sends data to ts
 * Return         : void
                    
 *******************************************************************************/
bool updateThingspeak() 
{
    //delay (2000);
    static int count = 0;
    Serial.println(count++);
    int rssival = WiFi.RSSI();
    //sprintf(publishString,"%d",rssival);
    //bool success = Particle.publish("RSSI",publishString);
    
    //sprintf(publishString, "%1.4f", checkbattery());

      bool success = Particle.publish("thingSpeakWrite_All", +
     "{ \"1\": \"" + String(rssival) + "\"," +
       "\"2\": \"" + String(garagetemp) + "\"," +
       "\"3\": \"" + String(garagehumid) + "\"," +
       "\"4\": \"" + String(doorstatus) + "\"," +
       "\"5\": \"" + String(doorcounter) + "\"," +
       "\"6\": \"" + String(analog1) + "\"," +
       "\"7\": \"" + String(analog2) + "\"," +
       "\"k\": \"" + key + "\" }", 60, PRIVATE);
    return success; //if sent, then turn of the send flag, otherwise let it try again.
    
}

/*******************************************************************************
 * Function Name  : getDht
 * Description    : gets temp and humidity
 * Input          : 
 * Output         : 
 * Return         : void
                    
 *******************************************************************************/
void doDht() {
    if (!DHT.acquiring()) {		// has sample completed?

	    // get DHT status
	    int result = DHT.getStatus();

	    Serial.print("Read DHT sensor: ");
	    switch (result) {
		case DHTLIB_OK:
		    Serial.println("OK");
		   	garagehumid = DHT.getHumidity();
            // Read temperature as Celsius
	        //garagetemp = DHT.getCelsius();
            // Read temperature as Farenheit
	        garagetemp = DHT.getFahrenheit();
	        break;
		case DHTLIB_ERROR_CHECKSUM:
		    Serial.println("Error\n\r\tChecksum error");
		    break;
		case DHTLIB_ERROR_ISR_TIMEOUT:
		    Serial.println("Error\n\r\tISR time out error");
		    break;
		case DHTLIB_ERROR_RESPONSE_TIMEOUT:
		    Serial.println("Error\n\r\tResponse time out error");
		    break;
		case DHTLIB_ERROR_DATA_TIMEOUT:
		    Serial.println("Error\n\r\tData time out error");
		    break;
		case DHTLIB_ERROR_ACQUIRING:
		    Serial.println("Error\n\r\tAcquiring");
		    break;
		case DHTLIB_ERROR_DELTA:
		    Serial.println("Error\n\r\tDelta time to small");
		    break;
		case DHTLIB_ERROR_NOTSTARTED:
		    Serial.println("Error\n\r\tNot started");
		    break;
		default:
		    Serial.println("Unknown error");
		    break;
	    }


        /*
	    Serial.print("Humidity (%): ");
	    Serial.println(DHT.getHumidity(), 2);

	    Serial.print("Temperature (oC): ");
	    Serial.println(DHT.getCelsius(), 2);

	    Serial.print("Temperature (oF): ");
	    Serial.println(DHT.getFahrenheit(), 2);
        
	    Serial.print("Temperature (K): ");
	    Serial.println(DHT.getKelvin(), 2);

	    Serial.print("Dew Point (oC): ");
	    Serial.println(DHT.getDewPoint());

	    Serial.print("Dew Point Slow (oC): ");
	    Serial.println(DHT.getDewPointSlow());
        */
        
	    n++;  // increment counter
	    bDHTstarted = false;  // reset the sample flag so we can take another
	}

	//Serial.println(Time.timeStr());
}

Garage Door Code

Arduino
Particle IDE
//Garage Door control code 4-10-16
// This #include statement was automatically added by the Particle IDE.
#include "Adafruit_DHT/Adafruit_DHT.h"

 /*                           +-----+
 *                 +----------| USB |----------+
 *                 |          +-----+       *  |
 *                 | [ ] VIN           3V3 [ ] |
 *                 | [ ] GND           RST [ ] |
 *                 | [ ] TX           VBAT [ ] |
 *                 | [ ] RX  [S]   [R] GND [ ] |
 *                 | [ ] WKP            D7 [ ] |
 *                 | [ ] DAC +-------+  D6 [ ] |
 *   Opensensor -->| [*] A5  |   *   |  D5 [ ] |
 * Closedsensor -->| [*] A4  |Photon |  D4 [*] |<-- DHT21 AM2301 Sensor
 *                 | [*] A3  |       |  D3 [ ] |
 *                 | [ ] A2  +-------+  D2 [ ] |
 *                 | [ ] A1             D1 [ ] |
 *  Analog0     -->| [ ] A0             D0 [*] |<-- Garage Door Switch Relay
 *                 |                           |
 *                  \    []         [______]  /
 *                   \_______________________/
 *
 *
 */
// This #include statement was automatically added by the Particle IDE.
#include "HttpClient/HttpClient.h"
#include "application.h"

// This #include statement was automatically added by the Spark IDE.
#include "elapsedMillis/elapsedMillis.h"

//-------------Begin UBIDOTS---------------
    #define VARIABLE_ID_DOORSTATUS "ID HERE"
    #define VARIABLE_ID_VOLTAGE "ID HERE"
    #define TOKEN "TOKEN HERE"

    HttpClient http;
    int lightLevel = 0;
    unsigned int nextTime = 0;    // Next time to contact the server

    // Headers currently need to be set at init, useful for API keys etc.
    http_header_t headers[] = {
        { "Content-Type", "application/json" },
        { NULL, NULL } // NOTE: Always terminate headers will NULL
    };

    http_request_t request;
    http_response_t response;
//--------------End UBIDOTS----------------

//--------------Begin DHT------------------
// Example testing sketch for various DHT humidity/temperature sensors
// Written by ladyada, public domain

#define DHTPIN D4     // what pin we're connected to

// Uncomment whatever type you're using!
//#define DHTTYPE DHT11		// DHT 11 
//#define DHTTYPE DHT22		// DHT 22 (AM2302)
#define DHTTYPE DHT21		// DHT 21 (AM2301)

// Connect pin 1 (on the left) of the sensor to +5V
// Connect pin 2 of the sensor to whatever your DHTPIN is
// Connect pin 4 (on the right) of the sensor to GROUND
// Connect a 10K resistor from pin 2 (data) to pin 1 (power) of the sensor

DHT dht(DHTPIN, DHTTYPE);
//---------------End DHT-------------------

unsigned int interval = 50;
unsigned int particleinterval = 100;
unsigned int particlefuncinterval = 10000;
unsigned int ubiinterval = 5000;
unsigned int dhtinterval = 30000;

int garageDoorFunction(String command);

int rssival = 0;
int doorstatus = 1;
int doorcounter = 0;
int closedsensorpin = A4;
int opensensorpin = A5; 
const int open = 0;
const int closed = 1;

// Define the pins we're going to call pinMode on
int led = D6;  // You'll need to wire an LED to this one to see it blink.
int led2 = D7; // This one is the built-in tiny one to the right of the USB jack


double analog1 = 0;
double analog1raw = 0;
double garagetemp = 0;
double garagehumid = 0;

elapsedMillis timeElapsed; //declare global if you don't want it reset every time loop runs
elapsedMillis timeElapsed2; //declare global if you don't want it reset every time loop runs
elapsedMillis timeElapsed3;
elapsedMillis timeElapsed4;
elapsedMillis timeElapsed5;


char publishString[40];
char doorpublishstring[40];
char doorstatusstring[40];

SYSTEM_MODE(SEMI_AUTOMATIC); //allows for control of Spark.connect() and Spark.process()

// This routine runs only once upon reset
void setup() {
    timeElapsed = 0;
    pinMode(led, OUTPUT);
    pinMode(led2, OUTPUT);
    pinMode(A0, INPUT);
    Serial1.begin(230400);   // open serial over USB
    //ip = {"IP:", WiFi.localIP()};
    //String ip = WiFi.localIP();

    Particle.function("GarageDoor", garageDoorFunction);
    Particle.variable("analog1", &analog1, DOUBLE);  //variables update automatically. Max 4 per particle
    Particle.variable("doorcounter", doorcounter);
    //Particle.variable("doorstatus", &doorstatus, INT); //variables are viewable on mobicle 
    Particle.variable("doorstatus", doorstatusstring, STRING); //variables are viewable on mobicle 
    Particle.variable("garagetemp", &garagetemp, DOUBLE); //variables are viewable on mobicle
    Particle.variable("garagehumid", &garagehumid, DOUBLE); //variables are viewable on mobicle
    
    pinMode(opensensorpin, INPUT_PULLUP);
    pinMode(closedsensorpin, INPUT_PULLUP);

    pinMode(D0, OUTPUT);

    //Do DHTINIT
    pinMode(DHTPIN, INPUT_PULLUP); //sensor needs pullup resistor.
	dht.begin();
	//End DHTINIT

//-------------Begin UBIDOTS---------------
//    request.hostname = "things.ubidots.com";
//    request.port = 80;
//    request.path = "/api/v1.6/variables/"VARIABLE_ID"/values?token="TOKEN;
//--------------End UBIDOTS----------------

}

void loop() {
    if (Particle.connected() == false){
            Particle.connect();
    }
            analog1raw = analogRead(A0);
            analog1 = (analog1 *63 +analog1raw *3.3/4095)/64;
            
            
            if (timeElapsed> interval)
            {                 //sets up interval timer and fires on interval
                digitalWrite(led, !digitalRead(led));   // Turn ON the LED pins
                digitalWrite(led2, !digitalRead(led2));
            
	            int opensense = digitalRead(opensensorpin);
                int closedsense = digitalRead(closedsensorpin);


                if (opensense == LOW) // high,0 = open door, low,1 = closed door
                {  
                    if (doorstatus == closed) //check the previous state, if closed, then generate an event
                    {
                        strcpy(doorpublishstring, "Open");
                        Particle.publish("GARAGEDOOR",doorpublishstring);
                        doorcounter++;
                    }
                    doorstatus = open;
                    strcpy(doorstatusstring, "Open"); //update particle variable.
                } 
                else if (closedsense == LOW)
                {
                    if (doorstatus == open) //check the previous state, if closed, then generate an event
                    {
                        strcpy(doorpublishstring, "Closed");
                        Particle.publish("GARAGEDOOR",doorpublishstring);
                        doorcounter++;
                    }
                    
                    doorstatus = closed;
                    strcpy(doorstatusstring, "Closed");  //update particle variable.
                }
                else 
                {  //if door is neither closed or open, it is in "limbo" 
                    strcpy(doorstatusstring, "Limbo");  //update particle variable.
                }
                
                timeElapsed = 0;    //resets interval timer.
            }
            
            if (timeElapsed2>particleinterval)  //Run code every .1 seconds
            { 
            Particle.process(); //Process wifi events 
            }
            
            if (timeElapsed3> particlefuncinterval) //Run code every 10 seconds
            {
                rssival = WiFi.RSSI();
                sprintf(publishString,"%d",rssival);
                
                Particle.publish("RSSI",publishString);
                timeElapsed3 = 0; //reset interval timer
            }

            if (timeElapsed4> ubiinterval) //Run code every 10 seconds
            {
                updateUbidots();
                timeElapsed4 = 0; //reset interval timer
            }
            
            if (timeElapsed5> dhtinterval) //Run code every 10 seconds
            {
                getDht();
                timeElapsed5 = 0; //reset interval timer
            }

            //delay(2);
 // delay(1000);
}

/*******************************************************************************
 * Function Name  : garageDoor
 * Description    : based on door status, opens or closes the door
 * Input          : analog 5 - digital input.
 * Output         : digital 1 - relay to blip and open or close the door
 * Return         : Value of the pin (0 or 1) in INT type
                    Returns a negative number on failure
 *******************************************************************************/
int garageDoorFunction(String sesame)
{
    if(sesame.startsWith("C"))
	{
		if (doorstatus == open)
		{
	        pinMode(0, OUTPUT);
		    digitalWrite(0, 1);
		    delay(250);
		    digitalWrite(0, 0);
		return 1;
		}
	}
	else if (sesame.startsWith("O"))
		{
        if (doorstatus == closed)
		{
	        pinMode(0, OUTPUT);
		    digitalWrite(0, 1);
		    delay(250);
		    digitalWrite(0, 0);
		return 2;
		}
	}
	else if (sesame.startsWith("X"))
		{
        if (doorstatus == closed){
            return 999;
        } else
        return 123;
		}
	return -1;
	
}



/*******************************************************************************
 * Function Name  : updateUbidots
 * Description    : sends variables to ubidots
 * Input          : doorstatus global variables
 * Output         : sends data to ubidots
 * Return         : void
                    
 *******************************************************************************/
void updateUbidots() {
//-------------Begin UBIDOTS---------------
    request.hostname = "things.ubidots.com";
    request.port = 80;
    request.path = "/api/v1.6/variables/"VARIABLE_ID_DOORSTATUS"/values?token="TOKEN;

    Serial.println("Sending ubidots data ...");
    int ubidotsdoorstatus = ~doorstatus&0x01;
    request.body = "{\"value\":" + String(ubidotsdoorstatus) + "}";
    // Post request
    http.post(request, response, headers);
    Serial.println(response.status);
    Serial.println(response.body);
    


    request.hostname = "things.ubidots.com";
    request.port = 80;
    request.path = "/api/v1.6/variables/"VARIABLE_ID_VOLTAGE"/values?token="TOKEN;

    Serial.println("Sending ubidots data ...");
    request.body = "{\"value\":" + String(analog1) + "}";
    // Post request
    http.post(request, response, headers);
    Serial.println(response.status);
    Serial.println(response.body);
    
}

/*******************************************************************************
 * Function Name  : getDht
 * Description    : gets temp and humidity
 * Input          : 
 * Output         : 
 * Return         : void
                    
 *******************************************************************************/
void getDht() {
// Wait at least 2 seconds between measurements.
	

// Reading temperature or humidity takes about 250 milliseconds!
// Sensor readings may also be up to 2 seconds 'old' (its a 
// very slow sensor)
	float h = dht.getHumidity();
// Read temperature as Celsius
	float t = dht.getTempCelcius();
// Read temperature as Farenheit
	float f = dht.getTempFarenheit();
  
   garagetemp = f; //updates spark variable with current temp.
   garagehumid = h;
   
// Check if any reads failed and exit early (to try again).
	if (isnan(h) || isnan(t) || isnan(f)) {
		Serial.println("Failed to read from DHT sensor!");
		return;
	}

// Compute heat index
// Must send in temp in Fahrenheit!
	float hi = dht.getHeatIndex();
	float dp = dht.getDewPoint();
	float k = dht.getTempKelvin();

	Serial.print("Humid: "); 
	Serial.print(h);
	Serial.print("% - ");
	Serial.print("Temp: "); 
	Serial.print(t);
	Serial.print("*C ");
	Serial.print(f);
	Serial.print("*F ");
	Serial.print(k);
	Serial.print("*K - ");
	Serial.print("DewP: ");
	Serial.print(dp);
	Serial.print("*C - ");
	Serial.print("HeatI: ");
	Serial.print(hi);
	Serial.println("*C");
	Serial.println(Time.timeStr());
}

Thingspeak Integration

JSON
paste into the particle console integration custom JSON field
{
    "event": "thingSpeakWrite_",
    "url": "https://api.thingspeak.com/update",
    "requestType": "POST",
    "form": {
        "api_key": "{{k}}",
        "field1": "{{1}}",
        "field2": "{{2}}",
        "field3": "{{3}}",
        "field4": "{{4}}",
        "field5": "{{5}}",
        "field6": "{{6}}",
        "field7": "{{7}}",
        "field8": "{{8}}",
        "latitude": "{{a}}",
        "longitude": "{{o}}",
        "elevation": "{{e}}",
        "status": "{{s}}"
    },
    "mydevices": true,
    "noDefaults": true,
    "location": true
}

Credits

John R McAlpine V Mac

John R McAlpine V Mac

17 projects • 87 followers
www.MACSBOOST.com Assistant Teaching Professor at UNC Charlotte MEGR3171 Instrumentation, Motorsports Research

Comments