Darian Johnson
Published © CC BY-NC

Scent-terrific Smart Candle

The Scent-terrific Smart Candle is made of wax, flickers like a real flame, emits scents though a wax warmer, and is controllable by Alexa!

AdvancedFull instructions providedOver 3 days2,776

Things used in this project

Hardware components

Arduino Pro Mini 328 - 5V/16MHz
SparkFun Arduino Pro Mini 328 - 5V/16MHz
This is shown as retired on the Arduino.cc website, but you can still get them pretty easily. Both Sparkfun and Adafruit sell the 5V and 3V versions. You can also get cloned versions on Amazon. I bought a few clones for testing purposes, then moved to the "official" module for my product build.
×1
Adafruit Feather HUZZAH with ESP8266 WiFi
Adafruit Feather HUZZAH with ESP8266 WiFi
I attempted to use a Huzzah breakout, but had problems getting my code to upload. Went with the feather instead.
×1
LED (generic)
LED (generic)
You'll need a red 3mm LED for the temp indicator. You'll need a yellow 5mm LED for each candle you plan to use. The Arduino Pro Mini has 6 PWM pins. You'll need for for the heating pad/transistor... leaving 5 for the candles.
×4
DS18B20 Temperature Sensor
Used to measure temperature of heating pad
×1
Adafruit Heating Pad
Used to heat the ramekin/wax cubes
×1
2 oz (1 in tall by 2.5 in wide) Ceramic Ramekin
Note - I purchased mine at Bed Bath Beyond for ~$2
×1
Adafruit TIP 120 Darlington Transistors
×1
Adafruit 5V Voltage Regulator (7805)
×1
12V 2A Power Supply
Don't skimp with a 12V 1A or a 9V power supply. You'll need 12V and 1.2 A to get the electric pad hot enough to warm the ramekin and melt the wax
×1
Adafruit 2.1 MM panel mount for DC connector
Input for the power supply.
×1
Assorted Resistors
You'll need resistors for the yellow LEDs (220), the Temperature Sensor (4.7K), and the transistor (10K)
×5
Momentary Push Button
You'll need this if you want to control the candles by touch as well as by voice.
×1
Candles
I used 2in wide by 4 in tall candles.... you can use whatever makes sense for your project.
×3
Heat Sinks
You'll need at least one for the Voltage Regulator. I went ahead and used 2 (one for the voltage regulator, the other for the transistor).
×1
Minwax® PolyShades (Bombay Mahogany)
Used to stain the PLA. Use the PolyShades for 3D prints.... looks much better than the regular stain.
×1
3d filament (PLA) - Black and Wood
I'm a fan of Hatchbox PLA, but use whatever you prefer. I couldn't find a Hatchbox Wood PLA, so I went with MG Chemicals Wood PLA.
×1
Capacitors 10 nF
Not required, but I added anyway (on each side of the voltage regulator)
×2
1N4007 – High Voltage, High Current Rated Diode
1N4007 – High Voltage, High Current Rated Diode
×1
Wax Cubes
Scented wax cubes to go into the wax melter
×1
LED panel mount
You will need one 3MM holder and a 5MM holder for each candle
×3
Adafruit Perma-Proto Half-sized Breadboard PCB
You can use any protoboard. I used this one because it was a logical migration from my breadboard solution.
×1

Software apps and online services

Alexa Skills Kit
Amazon Alexa Alexa Skills Kit
Used to develop the Alexa Smart Home Skill
AWS IoT
Amazon Web Services AWS IoT
Used to create the certificates and create the IoT "thing"
Mongoose OS
Mongoose OS
Used to program the ESP8266
Arduino IDE
Arduino IDE
Used to program the Pro Mini
AWS Lambda
Amazon Web Services AWS Lambda
Used to create the smart home skill and to create the registration webpage.

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Drill
Used to make holes in the candles
Soldering iron (generic)
Soldering iron (generic)
You'll need an assortment of wires as well. I used both stranded and solid 22 gauge wire for the solution... the stranded was easier to fit into the cut-outs when I assembled the candle holder.
Crazy Glue

Story

Read more

Custom parts and enclosures

3D Parts

Candle Insert Holder

This "clips" to the LED holder and is used to hold the candle in the base

Candle Base Top

The main candle holder. I've also uploaded the Fusion 360 version (so that you can add a different name or design to the front).

Candle Base Top (Fusion 360 file)

LED Holder

Holds the LED

Candle Holder Bottom

The bottom of the candle holder

Schematics

Breadboard Diagram

Details on the wiring of the smart candle module.
Smartcandle bb3 sccpw8cog8

VUI diagram

High level voice interface between Alexa and the device
Vui large zanbykllur

Code

Arduino Pro Mini code (candle.ino)

C/C++
This is the code to be installed on the Arduinio Pro Mini. Controls the LEDs and Heating Pad
//This is the code that is installed on the Arduino Pro Mini for the Smart Candle

//Define the pins for the LEDs
#define FLICKER_LED_PIN1 5
#define FLICKER_LED_PIN2 6
#define FLICKER_LED_PIN3 9
#define FLICKER_LED_PIN4 10
#define FLICKER_LED_PIN5 11

#define PAD_PIN 3 //heating pad
#define TEMP_PIN 2 //temperature sensor
#define VOLTAGE_PIN 0


#include <OneWire.h>
#include <DallasTemperature.h>

OneWire oneWire(TEMP_PIN);
DallasTemperature sensors(&oneWire);

int state = 0;
long waitTime = 60000; // in Milliseconds
int lowPower = 64; //25%
int highPower = 255; //100%
float changeFactor = 0.1;
int safeTempLimit = 105;
int lowMin = 80;
int lowMax = 90;
int highMin = 110;
int highMax = 140;

void setup() {
  Serial.begin(115200);

  //set pin mode for LEDs, temp sensors, and heating pad
  pinMode(FLICKER_LED_PIN1, OUTPUT);
  pinMode(FLICKER_LED_PIN2, OUTPUT);
  pinMode(FLICKER_LED_PIN3, OUTPUT);
  pinMode(FLICKER_LED_PIN4, OUTPUT);
  pinMode(FLICKER_LED_PIN5, OUTPUT);
  pinMode(PAD_PIN, OUTPUT);
  pinMode(TEMP_PIN, OUTPUT);
  sensors.begin();



  //Get Initial Settings; if the voltage is 0, then the LEDs and Heating pad are off
  if (analogRead(VOLTAGE_PIN) < 500) {
    state = 0;
  } else {
    state = 1;
  }
  heatingpad(); //function to determine the voltage that needs to go to heating pad - using PWM to control temperature
}

//Sets the initial values to control flicker of LEDs
int flicker_random_low_start = 0;
int flicker_random_low_end = 0;
int flicker_random_high = 0;
int flicker_random_speed_start = 0;
int flicker_random_speed_end = 0;

//Sets the initial values to determine how ofter to check temperature and regulate voltage to heating pad
long msCounter = 0;

void loop() {

  if (analogRead(VOLTAGE_PIN) < 500) {
    state = 0;
  } else {
    state = 1;
  }

  flicker(FLICKER_LED_PIN1);
  flicker(FLICKER_LED_PIN2);
  flicker(FLICKER_LED_PIN3);
  flicker(FLICKER_LED_PIN4);
  flicker(FLICKER_LED_PIN5);

  //If the wait time has pasted, call the heatingpad function
  if (millis() - msCounter > waitTime) {
    msCounter = millis();
    heatingpad();
  }
}


//Heatingpad function either makes the heating pad hot, or keeps it warm
//The Heating pad needs to stay around 90 F in order to reduce the time it takes to get hot and warm wax
void heatingpad() {
  sensors.requestTemperatures();
  float temp = sensors.getTempFByIndex(0);

  if (state > 0) {
    keepHot(temp);
  } else {
    keepWarm(temp);
  }

}


//The Flicker function randomizes the flicked of the LEDs

void flicker(int FLICKER_LED_PIN) {

  // the start of the flicker (low)
  static int flicker_low_min = 200;
  static int flicker_low_max = 240;

  // the end value of the flicker (high)
  static int flicker_high_min = 230;
  static int flicker_high_max = 256;

  // delay between each low-high-low cycle
  // low->high |flicker_hold| high->low
  static int flicker_hold_min = 40; // milliseconds
  static int flicker_hold_max = 80; // milliseconds

  // delay after each low-high-low cycle
  // low->high->low |flicker_pause| low->high...
  static int flicker_pause_min = 100; // milliseconds
  static int flicker_pause_max = 200;  // milliseconds

  // delay low to high and high to low cycle
  static int flicker_speed_min = 900; // microseconds
  static int flicker_speed_max = 1000; // microseconds
  if (state > 0) {

    // random time for low
    flicker_random_low_start = random(flicker_low_min, flicker_low_max);
    flicker_random_low_end = random(flicker_low_min, flicker_low_max);

    // random time for high
    flicker_random_high = random(flicker_high_min, flicker_high_max);

    // random time for speed
    flicker_random_speed_start = random(flicker_speed_min, flicker_speed_max);
    flicker_random_speed_end = random(flicker_speed_min, flicker_speed_max);

    // low -> high
    for (int i = flicker_random_low_start; i < flicker_random_high; i++) {
      analogWrite(FLICKER_LED_PIN, i);
      delayMicroseconds(flicker_random_speed_start);
    }

    // hold
    delay(random(flicker_hold_min, flicker_hold_max));

    // high -> low
    for (int i = flicker_random_high; i >= flicker_random_low_end; i--) {
      analogWrite(FLICKER_LED_PIN, i);
      delayMicroseconds(flicker_random_speed_end);
    }

    // pause
    delay(random(flicker_pause_min, flicker_pause_max));
  }
  else  {
    analogWrite(FLICKER_LED_PIN, 0);

  }
}


//This function checks the temperature and attempts to keep the Heating Pad WARM: between 110 - 140 F degrees
//Wax melts around 130 F
void keepWarm(float temp) {
  Serial.println(temp);
  if (temp > lowMax) {
    lowPower = lowPower - lowPower * changeFactor;
  }

  if (temp < lowMin) {
    lowPower = lowPower + lowPower * changeFactor;
  }
  if (lowPower < 64) { //we should not drop below this level to keep system warm
    lowPower = 64;
  }
  analogWrite(PAD_PIN, lowPower);
}

//This function checks the temperature and attempts to keep the Heating Pad HOT: between 80 - 90 F degrees
void keepHot(float temp) {
  Serial.println(temp);
  if (temp > highMax) {
    highPower = highPower - highPower * changeFactor;
  }

  if (temp < highMin) {
    highPower = highPower + highPower * changeFactor;

  }
  if (highPower > 255) {
    highPower = 255; 
  }
  analogWrite(PAD_PIN, highPower);
}

Alexa Smart Home Skill code (index.js)

JavaScript
This is the main function used to control the Smart Home Devices
//This is the Alexa Smart Home skill code used to control Smart Candles
//Users need to understand the following concepts before creating/modifying this skill:
// - AWS IoT Shadows: https://docs.aws.amazon.com/iot/latest/developerguide/iot-device-shadows.html
// - AWS IoT: https://docs.aws.amazon.com/iot/latest/developerguide/iot-thing-management.html
// - Alexa Smart Home Skill Development: https://developer.amazon.com/docs/smarthome/understand-the-smart-home-skill-api.html
//Please reach out to Darian Johnson (@darianbjohnson via Twitter) for questions


var AWS = require('aws-sdk');
var requestCall = require('request');

//Config variables for IoT Thing
const config = {};
config.IOT_BROKER_ENDPOINT = "XXX.iot.XXX.amazonaws.com";  // REST API endpoint for you IoT Devices
config.IOT_BROKER_REGION = "XXX";  // the region where you build your IoT Thing; for example, us-east-1 for the N. Virginia region

exports.handler = function (request, context) {

    //used when you ask Alexa to discover your devices
    if (request.directive.header.namespace === 'Alexa.Discovery' && request.directive.header.name === 'Discover') {
        var token = request.directive.payload.scope.token;

        //You can comment out the validateUser call if you plan to hard code your ThingName(s)
        validateUser(request, context, token, (userId) => {
            handleDiscovery(request, context, userId);
        });
    }

    //used when you ask Alexa to turn on/off your smart devices
    else if (request.directive.header.namespace === 'Alexa.PowerController') {
        if (request.directive.header.name === 'TurnOn' || request.directive.header.name === 'TurnOff') {
            log("DEBUG:", "TurnOn or TurnOff Request", JSON.stringify(request));
            handlePowerControl(request, context);
        }
    }

    //used when the Alexa App attempts to determine the state of a device
    else if (request.directive.header.namespace === 'Alexa') {
        if (request.directive.header.name === 'ReportState') {
            handleReportState(request, context);
        }
    }

    else{
        var responseHeader = request.directive.header;
        responseHeader.messageId = responseHeader.messageId + "-R";
        responseHeader.name = "ErrorResponse";
        var response = {
            context: contextResult,
            event: {
                header: responseHeader,
                endpoint: {
                    "endpointId": request.directive.endpoint.endpointId
                },
                payload: {
                    "type": "INVALID_DIRECTIVE",
                    "message": "That command is not valid for this device."
                }
            }
        };
        log("DEBUG", "ERROR ", JSON.stringify(response));
        context.succeed(response);
    }

    function handleDiscovery(request, context, userId) {
        //getApplicances passes in a userId (from the validateUser function) and returns all devices
        //associated with a userID. You can remove this call and hardcode the return values.
        //See https://github.com/alexa/alexa-smarthome/blob/master/sample_messages/Discovery/Discovery.response.json
        //for an example of a hardcoded response.

        getApplicances(userId, (errCode, endpoints) => {
            var payload = {
                "endpoints": endpoints
            };
            var header = request.directive.header;
            header.name = "Discover.Response";
            log("DEBUG", "Discovery Response: ", JSON.stringify({ header: header, payload: payload }));
            context.succeed({ event: { header: header, payload: payload } });
        });
    }

    function log(message, message1, message2) {
        console.log(message + message1 + message2);
    }

    //
    function handlePowerControl(request, context) {

        var endpointId = request.directive.endpoint.endpointId; // get device ID for requested device
        var requestMethod = request.directive.header.name; //used to determine if device should be on or off
        var requestToken = request.directive.endpoint.scope.token; // get user token pass in request
        var powerResult;
        var desiredState;

        if (requestMethod === "TurnOn") {
            powerResult = "ON";
            desiredState = true;
        }
        else if (requestMethod === "TurnOff") {
            powerResult = "OFF";
            desiredState = false;
        }

        //this updates the IoT ShadowThing, based on the desired state (on or off)
        handleDevice(endpointId, desiredState, (result) => {

            var d = new Date();
            var isoD = d.toISOString();
            var contextResult = {
                "properties": [
                    {
                        "namespace": "Alexa.PowerController",
                        "name": "powerState",
                        "value": powerResult,
                        "timeOfSample": isoD,
                        "uncertaintyInMilliseconds": 50
                    }, 
                    {
                        "namespace": "Alexa.EndpointHealth",
                        "name": "connectivity",
                        "value": {
                            "value": "OK"
                        },
                        "timeOfSample": isoD,
                        "uncertaintyInMilliseconds": 0
                    }
                ]
            };
            var responseHeader = request.directive.header;
            var responseEndpoint = request.directive.endpoint;
            responseHeader.namespace = "Alexa";
            responseHeader.name = "Response";
            responseHeader.messageId = responseHeader.messageId + "-R";
            var response = {
                context: contextResult,
                event: {
                    header: responseHeader,
                    endpoint: responseEndpoint,
                    payload: {}
                }

            };
            //log("DEBUG", "Alexa.PowerController ", JSON.stringify(response));
            //context.succeed(response);

            //Send and error back if the Shadow Device Reported state is not updated in 6 seconds
            var timeoutObj = setTimeout(() => {
                console.log('timeout beyond time');
                handleError(request, context);
            }, 7000);


            //Check the Shadow Every 0.3 sec to see if the update has occured; if so, send success message
            var intervalObj = setInterval(() => {
                getDeviceStatus(endpointId,(results)=>{
                    if (results === powerResult){
                        log("DEBUG", "Alexa.PowerController ", JSON.stringify(response));
                        context.succeed(response);
                    }
                });
            }, 300);
        });
    }

    //this lets the Alexa app know if the device is on or off   
    function handleReportState(request, context) {
        var endpointId = request.directive.endpoint.endpointId;
        getDeviceStatus(endpointId, (powerResult) => {

            if (powerResult === "ERR") {
                handleError(request, context);
            }
            else {

                var d = new Date();
                var isoD = d.toISOString();
                var contextResult = {
                    "properties": [
                        {
                            "namespace": "Alexa.PowerController",
                            "name": "powerState",
                            "value": powerResult,
                            "timeOfSample": isoD, 
                            "uncertaintyInMilliseconds": 50
                        }
                    ]
                };

                var responseHeader = request.directive.header;
                responseHeader.messageId = responseHeader.messageId + "-R";
                responseHeader.name = "StateReport";
                var response = {
                    context: contextResult,
                    event: {
                        header: responseHeader,
                        payload: {}
                    }
                };
                log("DEBUG", "ReportState ", JSON.stringify(response));
                context.succeed(response);
            }

        });
    }

    //this handles errors if the device is unreachable
    function handleError(request, context) {
        var responseHeader = request.directive.header;
        responseHeader.messageId = responseHeader.messageId + "-R";
        responseHeader.name = "ErrorResponse";
        var response = {
            context: contextResult,
            event: {
                header: responseHeader,
                endpoint: {
                    "endpointId": request.directive.endpoint.endpointId
                },
                payload: {
                    "type": "ENDPOINT_UNREACHABLE",
                    "message": "Unable to reach device because it appears to be offline."
                }
            }
        };
        log("DEBUG", "ERROR ", JSON.stringify(response));
        context.succeed(response);

    }

    //this function is needed if you plan to authenticate against a device cloud
    //You can remove/ignore this function if you plan to hard code values
    function validateUser(request, context, token, callback) {
        if (token === 'access-token-from-skill') {
            callback('test1');
        } else {
            var amznProfileURL = 'https://api.amazon.com/user/profile?access_token=';
            amznProfileURL += token;
            requestCall(amznProfileURL, function (error, response, body) {
                if (error) {
                    var responseHeader = request.directive.header;
                    responseHeader.messageId = responseHeader.messageId + "-R";
                    responseHeader.name = "ErrorResponse";
                    var response = {
                        context: contextResult,
                        event: {
                            header: responseHeader,
                            payload: {
                                "type": "INVALID_AUTHORIZATION_CREDENTIAL",
                                "message": "The authorization credential provided by Alexa is invalid. Disable and re-enable the skill."
                            }
                        }
                    };
                    log("DEBUG", "ERROR ", JSON.stringify(response));
                    context.succeed(response);
                } else {
                    if (response.statusCode == 200) {
                        var parsedBody = JSON.parse(body);
                        console.log(parsedBody.user_id);
                        callback(parsedBody.user_id);
                    } else {
                        var responseHeader = request.directive.header;
                        responseHeader.messageId = responseHeader.messageId + "-R";
                        responseHeader.name = "ErrorResponse";
                        var response = {
                            context: contextResult,
                            event: {
                                header: responseHeader,
                                payload: {
                                    "type": "EXPIRED_AUTHORIZATION_CREDENTIAL",
                                    "message": "The authorization credential provided by Alexa has expired. Disable and re-enable the skill."
                                }
                            }
                        };
                        log("DEBUG", "ERROR ", JSON.stringify(response));
                        context.succeed(response);
                    }
                }
            });
        }
    
    }
};

//this function returns all the devices associated with an authenticaed user (listed in a DynamoDB table)
//You can remove/ignore this function if you plan to hard code values
function getApplicances(userId, callback) {

    var params = {
        TableName: "WaxOn_Appliances",
        IndexName: "userId-index",
        KeyConditionExpression: "#userId = :userId",
        ExpressionAttributeNames: {
            "#userId": "userId"
        },
        ExpressionAttributeValues: {
            ":userId": userId
        }
    };

    var docClient = new AWS.DynamoDB.DocumentClient();

    var endpoints = [];

    var p = new Promise((resolve, reject) => {
        docClient.query(params, function (err, data) {
            if (err) {
                console.log(err);
                reject(endpoints);
            } else {
                data.Items.forEach(function (item) {
                    var endpointDetails = {};
                    endpointDetails.endpointId = item.applianceId;
                    endpointDetails.description = item.description;
                    endpointDetails.friendlyName = item.friendlyName;
                    endpointDetails.displayCategories = [item.displayCategories];
                    endpointDetails.manufacturerName = "WaxOn Smart Candle - DIY";
                    endpointDetails.capabilities =
                        [
                            {
                                "type": "AlexaInterface",
                                "interface": "Alexa",
                                "version": "3"
                            },
                            {
                                "interface": "Alexa.PowerController",
                                "version": "3",
                                "type": "AlexaInterface",
                                "properties": {
                                    "supported": [{
                                        "name": "powerState"
                                    }],
                                    "retrievable": true,
                                    "proactivelyReported": false
                                    /*"proactivelyReported": true, "retrievable": true */
                                }
                            }
                        ];
                    endpoints.push(endpointDetails);
                });

                resolve(endpoints);
            }
        });
    });

    p.then(value => {

        if (value.length < 0) {
            callback(0, null);
        }
        else {
            callback(1, value);
        }

    }, reason => {
        console.log(reason); // Error!
        callback(-1, null);
    });
}

//This function updates the IoT Shadow Thing to turn the desired state on or off
//the ESP8266 will update the ShadowThing to reflect the "reported" state as on/off
//after it recieves the ShadowThing Update
function handleDevice(endpointId, desiredState, callback) {
    // update AWS IOT thing shadow
    AWS.config.region = config.IOT_BROKER_REGION;

    var param = { "on": desiredState }

    var paramsUpdate = {
        "thingName": endpointId,
        "payload": JSON.stringify(
            {
                "state":
                    {
                        "desired": param
                    }
            }
        )
    };

    var iotData = new AWS.IotData({ endpoint: config.IOT_BROKER_ENDPOINT });

    iotData.updateThingShadow(paramsUpdate, function (err, data) {
        if (err) {
            console.log("an error");
            callback("not ok");
        }
        else {
            console.log("success");
            callback("ok");
        }
    });

}

//This function returns the current state (on/off) of the device
function getDeviceStatus(endpointId, callback) {
    // update AWS IOT thing shadow
    AWS.config.region = config.IOT_BROKER_REGION;

    var params = {
        "thingName": endpointId
    };

    var iotData = new AWS.IotData({ endpoint: config.IOT_BROKER_ENDPOINT });

    iotData.getThingShadow(params, function (err, data) {
        if (err) {
            console.log(err);
            callback("ERR");
        }
        else {
            var results = JSON.parse(data.payload)
            var deviceState = results.state.reported.on;
            if (!deviceState) {
                callback("OFF");
            } else {
                callback("ON");
            }
        }
    });

}

ESP8266 Code (connects to AWS Iot) - (init.js)

JavaScript
This code is installed on the ESP8266 via Mongoose OS
//Code that is installed on ESP8266 via Mongoose OS


load("api_aws.js");
load('api_gpio.js');

let ledPin = 0; //led indicator on ESP8266 device
let buttonPin = 2; //button
let prominiPin = 15; //connected to Pro Mini to indicate if the LED should be on or off
let state = { on: false, counter: 0 };  // device state: shadow metadata

GPIO.set_mode(ledPin,GPIO.MODE_OUTPUT);
GPIO.set_mode(prominiPin,GPIO.MODE_OUTPUT);


// Upon startup, report current actual state, "reported"
// When cloud sends us a command to update state ("desired"), do it
AWS.Shadow.setStateHandler(function(data, event, reported, desired, reported_metadata, desired_metadata) {
  if (event === AWS.Shadow.CONNECTED) {
    AWS.Shadow.update(0, {reported: state});  // Report device state
  } else if (event === AWS.Shadow.UPDATE_DELTA) {
    for (let key in state) {
      if (desired[key] !== undefined) state[key] = desired[key];
    }
    AWS.Shadow.update(0, {reported: state});  // Report device state
  }
  print(JSON.stringify(reported), JSON.stringify(desired));
  if (!reported.on){
    GPIO.write(ledPin,1); 		//ESP8266 Light off
    GPIO.write(prominiPin,0); 	//Candle off
  }else{
    GPIO.write(ledPin,0);		//ESP8266 Light on
    GPIO.write(prominiPin,1); 	//Candle on
  }
}, null);

// On a button press, update press counter via the shadow
GPIO.set_button_handler(buttonPin, GPIO.PULL_UP, GPIO.INT_EDGE_NEG, 200, function() {
  state.on = !state.on;
  AWS.Shadow.update(0, {desired: {on: state.on, counter: state.counter + 1}}); //update ShadowThing to turn on/off device (desired state)
}, null);

Credits

Darian Johnson

Darian Johnson

7 projects • 90 followers
Technologist. Music lover. Fitness enthusiast. Movie buff. Fan of sci-fi and comic books.

Comments