Mark Brady IngleDan Thyer
Published © CC BY

Alexa Controlled Bubble Fogger

Impress your friends with this amazing Alexa controlled fogger using a Particle Photon, a vape device, and 3D printed parts.

AdvancedFull instructions provided12 hours1,050
Alexa Controlled Bubble Fogger

Things used in this project

Hardware components

Photon
Particle Photon
×1
Amazon Echo
Amazon Alexa Amazon Echo
×1
TFV12 Cloud Beast Vape
×1
INTERNATIONAL RECTIFIER IRLB3034PBF N CH MOSFET, 40V, 195A, TO-220AB
×2
Sidiou Group 26650 Lithium Ion Battery Protected 3.7V 4800mAh Rechargeable Battery
×2
26650 Li-ion Rechargeable Battery Holder SMD Surface Mount
×2
TrustFire TR-006 Dual-Slot 25500 / 26650 / 26700 / 18650 / 16340 Battery Charger (AC 110~240V)
×2
15K Ohm Resistors, 1/4 W
This comes in a pack of many resistors, but only 2 Resistors are needed (1 for each MOSFET)
×1
High Airflow 12V Computer Fan
The fan needs to move a bunch of air for it to work well.
×1
HS-422 Servo Motor
×1
M3 10mm SS Screws
×2
M3 35mm SS Screw
×4
M3 15mm SS Screw
×2
M3 20mm SS Screw
×1
M3 Lock Nut
×2
M3 Washer
×2
M3 15mm Nylon Screw
×1
M3 20mm Nylon Screw
×1
Breadboard (generic)
Breadboard (generic)
×1
Super Miracle Bubbles
×1

Software apps and online services

Fusion 360
Autodesk Fusion 360

Hand tools and fabrication machines

Tevo Little Monster

Story

Read more

Custom parts and enclosures

Single 40mm Fan Mount

The fan mount should be printed in TPU with Shore Rating of no less than 85A. .8mm nozzle, 20% Infill, 15mm/s, 1 Perimeter, 4 Top and Bottom Layers, and supports are recommended.

Fogger Stand

The stand can be printed in plain PLA.

Male Spout Swivel Mount

This piece can be printed in PLA. 90% infill is required since the two arms will need to flex when attaching the male spout

Female Spout with Reservoir

This piece can be printed in PLA. To get good quality female screw threads you should print with a .3mm nozzle.

Pull Arm

This piece can be printed in PLA

Servo Arm

This piece can be printed in PLA. A .3mm nozzle is required so that you get the fine grove walls in the hole that attaches to the servo gear axil.

Servo Anchor Mount

This piece can be printed in PLA

Fogger Tub

This piece can be printed in PLA. Its best to print this using an .8mm nozzle and over extrude by 10%-20% so that you get a good seal between layers to prevent any leaks.

Vape Mouth Piece

This mouth piece replaces the one that comes with your vape device. The end of the mouth piece is angled to get a good seal with the male spout as it is raised up by the servo

Male Spout

This piece can be printed in PLA. To get good quality male screw threads you should print with a .3mm nozzle.

Schematics

AWS Lambda to Particle Photon

This diagram shows how Alexa Skill invokes the Lambda Function which posts to the Particle Photon cloud to invoke a function on Particle Photon.
Alexatophoton pymfemgvgg

Fogger Circuit Fritzing

Fritzingfogger keywu5fx2r

Code

Particle Photon Fog Machine

Arduino
This is the embedded code for the fog machine.
int pwmFoggerPin = D1;
int pwmLedPin = D2;
int pwmFanPin = D3;
int notifyLedPin = D7;
int servoBubbleWandPin = A7;

// Adjust the SERVO_ANGLE_OFFSET to set the spout to the proper angle to pick up bubble juice and seal tightly with mouth piece.
// ********************************************************************************************************************
int SERVO_ANGLE_OFFSET = 0; 
//*********************************************************************************************************************

double FOGGER_VOLTS = 5;
double FAN_VOLTS = 14;
int MAX_MILLISECONDS_TO_FOG_WITHOUT_CATCHING_ON_FIRE = 1200; //3000 or more is a fire hazard!!

Servo bubbleWandServo;

void setup() {
    pinMode(pwmFoggerPin, OUTPUT);
    pinMode(pwmFanPin, OUTPUT);
    pinMode(pwmLedPin, OUTPUT);
    pinMode(notifyLedPin, OUTPUT);
    
    bubbleWandServo.attach(servoBubbleWandPin);
    moveBubbleWandDown();
    
    RGB.control (true);
    
    Particle.subscribe("SpitFire", spitFire, MY_DEVICES);
    Particle.function("flame", flame);
    Particle.function("blowBubbles", blowBubbles);
    Particle.function("makeFog", makeFog);
    Particle.function("fogBubbles", fogBubbles);
    Particle.function("servoUp", servoUp);
    Particle.function("servoDown", servoDown);
    Particle.function("fanOn", fanOn);
    Particle.function("fanOff", fanOff);
    
    turnFanOn(FAN_VOLTS); delay(1000);
    makeSmoke(false);
    //Running indicator...Used wnen calibrating servo arm and starting up fogger.
    digitalWrite(notifyLedPin, HIGH);   // turn the LED on
    delay(2000);              // wait for two seconds
    digitalWrite(notifyLedPin, LOW);    // turn the LED off by making the voltage LOW
}

void loop() {

}

void makeSmoke(bool makeSmoke) {
    for (int repeatCount = 0; repeatCount < 2; repeatCount++) {
        turnFanOn(FAN_VOLTS); delay(1000);
        RGB.color(255,0,0);
        moveBubbleWandHome();
        delay(100);
        if (makeSmoke){
            turnFogOn(FOGGER_VOLTS);
        }
        analogWrite(pwmLedPin, 255);
        RGB.color(0,255,0);
        delay(MAX_MILLISECONDS_TO_FOG_WITHOUT_CATCHING_ON_FIRE);
        turnFogOff(); 
        RGB.color(0,255,0);
        
        blinkEyes(4,475);

        moveBubbleWandDown();
        analogWrite(pwmLedPin, 0);
        turnFanOff();
        delay(800);
    }
}

void blinkEyes(int number, int delayBetweenBlinks) {
    bool turnOn = (analogRead(pwmLedPin)>512); //Range = 0 to 1023
    for (int count = 0; count < number * 2; count++) {
        if (turnOn) {
            analogWrite(pwmLedPin, 0);
            delay(delayBetweenBlinks/2);
        } else {
            analogWrite(pwmLedPin, 255);
            delay(delayBetweenBlinks/2);
        }
        turnOn = !turnOn;
    } 
}

void moveBubbleWandDown() {
    bubbleWandServo.write(50 + SERVO_ANGLE_OFFSET);//50
}

void moveBubbleWandHome() {
    bubbleWandServo.write(160 - SERVO_ANGLE_OFFSET);//165
}

int computeAnalogWriteValueToSetVoltage(double volts) {
    double batteryVolts = 4;
    int numberOfBatteries = 4;
    int analogWriteValueToSetVoltage = volts / (batteryVolts * numberOfBatteries) * 255;
    
    if (analogWriteValueToSetVoltage > 255) {
        analogWriteValueToSetVoltage = 255;
    }
    
    return analogWriteValueToSetVoltage;
}

void turnFogOn(double volts){
    analogWrite(pwmFoggerPin, computeAnalogWriteValueToSetVoltage(volts)); 
}

void turnFogOff(){
    analogWrite(pwmFoggerPin, 0);
}

void turnFanOn(double volts){
    //analogWrite(pwmFanPin, 255-computeAnalogWriteValueToSetVoltage(volts)); //For Marks' Fan
    analogWrite(pwmFanPin, computeAnalogWriteValueToSetVoltage(volts)); 
}

void turnFanOff(){
    //analogWrite(pwmFanPin, 255);  //For Marks' Fan
    analogWrite(pwmFanPin, 0);
}

int blowBubbles(String Command) {
    makeSmoke(false);
    return 1;
}

//Invoked by alexa skill
int makeFog(String Command) {
    RGB.color(0,128,128);
    makeSmoke(true);
    
    return 1;
}

//Invoked by alexa skill
int fogBubbles(String Command) {
    RGB.color(0,0,255);
    makeSmoke(true);
    
    return 1;
}

//Invoked by alexa skill
int servoUp(String Command) {
    moveBubbleWandHome();
    return 1;
}

//Invoked by alexa skill
int servoDown(String Command) {
    moveBubbleWandDown();
    return 1;
}

//Invoked by alexa skill
int fanOn(String Command) {
    turnFanOn(FAN_VOLTS);
    return 1;
}

//Invoked by alexa skill
int fanOff(String Command) {
    turnFanOff();
    return 1;
}

//Invoked by alexa skill
int flame(String Command) {
    makeSmoke(true);
    return 1;
}

//Invoked by alexa skill
void spitFire(const char *event, const char *data) {
    makeSmoke(true);
}

Fog Machine Alexa Skill

JavaScript
This code runs in Amazon Lambda to invoke functions in the Particle Photon code.
'use strict';
var http = require('https');

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `SessionSpeechlet - ${title}`,
            content: `SessionSpeechlet - ${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}


// --------------- Functions that control the skill\'s behavior -----------------------
function getWelcomeResponse(callback) {
    const sessionAttributes = {};
    const cardTitle = 'Welcome';
    const speechOutput = 'Welcome to Mark and Dans fog bubble awesomeness spooky fog bubble thingy! Please say blow bubbles, make fog, or make fog bubbles!';
    const repromptText = 'Please say blow bubbles, make fog, or make fog bubbles!';
    const shouldEndSession = false;

    callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Happy Halloween!';
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

function blowBubbles(intent, session, callback) {
    const cardTitle = intent.name;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = 'Blowing bubbles';

    invokeParticleMicrocontroller('blowBubbles', function(resp){
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    });
}

function makeFog(intent, session, callback) {
    const cardTitle = intent.name;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = 'Make Fog';
    
    invokeParticleMicrocontroller('makeFog', function(resp){
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    });
    
}

function makeFogBubbles(intent, session, callback) {
    const cardTitle = intent.name;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = 'Make Fog Bubbles';

    invokeParticleMicrocontroller('fogBubbles', function(resp){
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    });
}

function moveServoDown(intent, session, callback) {
    const cardTitle = intent.name;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = 'Move Servo Down';

    invokeParticleMicrocontroller('servoDown', function(resp){
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    });
}

function moveServoUp(intent, session, callback) {
    const cardTitle = intent.name;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = 'Move Servo Up';

    invokeParticleMicrocontroller('servoUp', function(resp){
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    });
}

function turnFanOn(intent, session, callback) {
    const cardTitle = intent.name;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = 'Turn Fan On';

    invokeParticleMicrocontroller('fanOn', function(resp){
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    });
}

function turnFanOff(intent, session, callback) {
    const cardTitle = intent.name;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = 'Turn Fan Off';

    invokeParticleMicrocontroller('fanOff', function(resp){
        callback(sessionAttributes,buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
    });
}

// ----------------------------------------------
function invokeParticleMicrocontroller(particleFunction, callback){

    var deviceId = '2d0030000d499999999999';  // This is the Particle Photon Device ID
    
    var options = {
        hostname: 'api.particle.io',
        port: 443,
        path: '/v1/devices/' + deviceId + '/' + particleFunction,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Accept': '*.*'
        }
    };

    var postData = "access_token=999999999999999999999999999999999999999";   // This is the Particle Photon access token

    console.log("Post Data: " + postData);

    // Call Particle API
    var req = http.request(options, function(res) {
        console.log('STATUS: ' + res.statusCode);
        console.log('HEADERS: ' + JSON.stringify(res.headers));

        var body = "";

        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            console.log('BODY: ' + chunk);

            body += chunk;
        });

        res.on('end', function () {
            callback(body);
        });
    });

    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
    });

    // write data to request body
    req.write(postData);
    req.end();
}

// --------------- Events -----------------------

function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);

    getWelcomeResponse(callback);
}

function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    if (intentName === 'BlowBubblesIntent') {
        blowBubbles(intent, session, callback);
    } else if (intentName === 'MakeFogIntent') {
        makeFog(intent, session, callback);
    } else if (intentName === 'MakeFogBubblesIntent') {
        makeFogBubbles(intent, session, callback);
    } else if (intentName === 'MoveServoUpIntent') {
        moveServoUp(intent, session, callback);
    } else if (intentName === 'MoveServoDownIntent') {
        moveServoDown(intent, session, callback);
    } else if (intentName === 'TurnFanOnIntent') {
        turnFanOn(intent, session, callback);
    } else if (intentName === 'TurnFanOffIntent') {
        turnFanOff(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getWelcomeResponse(callback);
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else {
        throw new Error('Invalid intent');
    }
}

function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
}

// --------------- Main handler -----------------------

// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = (event, context, callback) => {
    try {
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            callback();
        }
    } catch (err) {
        callback(err);
    }
};

Alexa Fogger Intent Schema

JSON
This JSON goes in the Interaction Model of the Alexa Developer's Console.
{
  "intents": [
    {
      "intent": "BlowBubblesIntent"
    },
    {
      "intent": "MakeFogIntent"
    },
    {
      "intent": "MakeFogBubblesIntent"
    },
    {
      "intent": "MoveServoDownIntent"
    },
    {
      "intent": "MoveServoUpIntent"
    },
    {
      "intent": "TurnFanOnIntent"
    },
    {
      "intent": "TurnFanOffIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    }
  ]
}

Fogger Alexa Utterances

AsciiDoc
This needs set in the Interaction Model Sample Utterances of the Alexa Developers Console. These are what people say to interact with the Alexa Skill.
BlowBubblesIntent blow bubbles
MakeFogIntent make fog
MakeFogBubblesIntent make fog bubbles
MakeFogBubblesIntent to say hello
MakeFogBubblesIntent happy halloween
MoveServoDownIntent move servo down
MoveServoUpIntent move servo up
MoveServoUpIntent move servo home
TurnFanOnIntent turn fan on
TurnFanOnIntent fan on
TurnFanOffIntent turn fan off
TurnFanOffIntent fan off

Credits

Mark Brady Ingle

Mark Brady Ingle

2 projects • 7 followers
Suffering from tinkeritis! I love learning new things and developing new applications that make people say...Wow! Thats Cool!
Contact
Dan Thyer

Dan Thyer

2 projects • 7 followers
Husband, father, business owner, software architect, IoT, web/mobile development, microcontrollers- basically a total geek. Microsoft RD/MVP
Contact

Comments

Add projectSign up / Login