openstatic.org

openstatic.org

IoT Bubble Machine

(Jump to downloads)

For a while i've had this bubble machine kicking around and not too long ago I was asked to set it up for a baby shower. My niece seems to love bubbles as most children do, so i agreed. After i finally dug it out of storage i remembered one key problem. Left unattended it makes quite the slippery mess so i began to think to myself, "how can i make this less of a distaster without managing it the entire time?"

After pulling it apart i realized that it was actually quite simple, and there was a lot of open space! In fact the only real componets were a computer fan and a motor to rotate the bubble carousel. Even better, the fan had a speed control input which i could take advantage of to control the flow of bubbles. Extra better, the whole thing seemed to run on 12V DC.

I started by selecting a suitable microcontroller, I went with one of my favorites (Adafruit HUZZAH ESP8266 Breakout) I didn't need many control pins, and they are easy to work with. After that i pulled out a little stepdown converter to create a 5v circuit drawing from the 12V source.

Connecting the speed control from the fan to the board was also easy, no fancy conversions needed!

Arduino ESP8266 Code
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266WebServer.h>
#include <ArduinoJson.h>
#include <ESP8266mDNS.h>
#include <WiFiUdp.h>
#include "AppleMidi.h"
#include <ESP8266mDNS.h>
#include <WebSocketsClient.h>
#include <WebSocketsServer.h>
#include "LittleFS.h"
#include <ButtonDebounce.h>

#define PWMRANGE

ESP8266WiFiMulti WiFiMulti;
ESP8266WebServer httpServer(80);
WebSocketsServer webSocketServer(81);
wl_status_t wl_status;

const int FAN_PIN = 16; // Speed Control GPIO for fan
const int CAROUSEL_PIN = 14; // Relay Control GPIO for bubble carousel

const int MIDI_CHANNEL = 11;
const int CONTROL_MIDI_CC = 20;

boolean carouselState;
int fanSpeed;

int whiteButtonValue = 0;
int blackButtonValue = 0;

long lastSecondAt = 0;

int sleep_for = 0;
int run_for = 0;
int sleep_for_reset = 0;
int run_for_reset = 0;

APPLEMIDI_CREATE_INSTANCE(WiFiUDP, AppleMIDI); // see definition in AppleMidi_Defs.h

// Forward declaration
void OnAppleMidiControlChange(byte channel, byte number, byte value);

// When we have a new websocket connection lets report the state of the bubble machine.
void greetConnection(uint8_t num)
{
  String out;
  StaticJsonDocument<256> jsonBuffer;
  jsonBuffer["fan"] = fanSpeed;
  jsonBuffer["carousel"] = carouselState;
  jsonBuffer["runFor"] = run_for;
  jsonBuffer["sleepFor"] = sleep_for;
  jsonBuffer["runForReset"] = run_for_reset;
  jsonBuffer["sleepForReset"] = sleep_for_reset;
  serializeJson(jsonBuffer, out);
  webSocketServer.sendTXT(num, out);
}

// Handle websocket events from html interface
void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t lenght)
{
  switch (type)
  {
    case WStype_DISCONNECTED:
      Serial.println("WS Disconnection");
      break;
    case WStype_CONNECTED:
      Serial.println("New WS Connection");
      greetConnection(num);
      break;
    case WStype_TEXT:
      if (payload[0] == '{') {
        processJSONPayload(num, payload);
      }
      break;
  }
}

// Handle JSON Payload events from html/websocket interface
void processJSONPayload(int num, uint8_t * payload)
{
  Serial.println("Recieved Websocket payload:");
  Serial.println((char*)payload);
  DynamicJsonDocument jsonBuffer(4096);
  DeserializationError error = deserializeJson(jsonBuffer, payload);
  if (error)
  {
    Serial.println("Deserialize Error");
  } else {
    if (jsonBuffer.containsKey("fan"))
    {
      setFanSpeed(jsonBuffer["fan"].as<int>());
    }
    if (jsonBuffer.containsKey("carousel"))
    {
      setCarousel(jsonBuffer["carousel"].as<int>() != 0);
    }
    if (jsonBuffer.containsKey("sleepForReset"))
    {
      sleep_for_reset = jsonBuffer["sleepForReset"].as<int>();
    }
    if (jsonBuffer.containsKey("runForReset"))
    {
      run_for_reset = jsonBuffer["runForReset"].as<int>();
    }
    if (jsonBuffer.containsKey("sleepFor"))
    {
      sleep_for = jsonBuffer["sleepFor"].as<int>();
    }
    if (jsonBuffer.containsKey("runFor"))
    {
      run_for = jsonBuffer["runFor"].as<int>();
    }
  }
}

void everySecond()
{
  if (sleep_for > 0 && !carouselState)
  {
    sleep_for--;
    if (wl_status == WL_CONNECTED)
    {
      String out;
      StaticJsonDocument<256> jsonBuffer;
      jsonBuffer["sleepFor"] = sleep_for;
      serializeJson(jsonBuffer, out);
      webSocketServer.broadcastTXT(out);
    }
    if (sleep_for == 0)
    {
      setCarousel(true);
    }
  } else if (run_for > 0 && carouselState) {
    run_for--;
    if (wl_status == WL_CONNECTED)
    {
      String out;
      StaticJsonDocument<256> jsonBuffer;
      jsonBuffer["runFor"] = run_for;
      serializeJson(jsonBuffer, out);
      webSocketServer.broadcastTXT(out);
    }
    if (run_for == 0)
    {
       setCarousel(false);
    }
  } 
}

void setup()
{
  // put your setup code here, to run once:
  analogWriteRange(127);
  Serial.begin(115200);
  if (LittleFS.begin())
  {
    //Serial.println("LittleFS INIT OK!");
  } else {
    //Serial.println("LittleFS INIT FAILED!");
  }
  pinMode(FAN_PIN, OUTPUT);
  pinMode(CAROUSEL_PIN, OUTPUT);

  setFanSpeed(127);
  setCarousel(true);
  WiFi.hostname("bubblemachine");
  WiFiMulti.addAP("NETWORK", "PASSWORD");
  
  wl_status = WiFiMulti.run();

  AppleMIDI.begin("bubblemachine");
  AppleMIDI.OnReceiveControlChange(OnAppleMidiControlChange);

  delay(200);
  Serial.println("INIT");
  httpServer.on("/", handleRoot);
  httpServer.on("/api", jsonRespond );
  httpServer.onNotFound( handleNotFound );
  httpServer.begin();
  webSocketServer.begin();
  webSocketServer.onEvent(webSocketEvent);
  webSocketServer.enableHeartbeat(15000, 5000, 2);
  if (wl_status == WL_CONNECTED)
  {
      tryMDNS();
  }
}

void tryMDNS()
{
  if (MDNS.begin("bubblemachine", WiFi.localIP()))
  {
    MDNS.addService("http", "tcp", 80);
    MDNS.addService("ws", "tcp", 81);
    MDNS.addService("apple-midi", "udp", 5004);
    //Serial.println("mDNS Started");
  } else {
    Serial.println("mDNS Failed to start");
  }
}

void OnAppleMidiControlChange(byte channel, byte number, byte value)
{
  if (channel == MIDI_CHANNEL)
  {
    if (number == CONTROL_MIDI_CC)
    {
      setCarousel(value >= 64);
    }
  }
}

void loop() 
{
  long ts = millis();
  if (ts - lastSecondAt >= 1000)
  {
    everySecond();
    lastSecondAt = ts;
  }
  wl_status_t wl_status_new = WiFiMulti.run();
  if (wl_status_new != wl_status)
  {
    wl_status = wl_status_new;
    // Capture moments when we reconnect to the network
    if (wl_status == WL_CONNECTED)
    {
        tryMDNS();
    }
  }
  if (wl_status == WL_CONNECTED)
  {
    MDNS.update();
    AppleMIDI.run();
    httpServer.handleClient();
    webSocketServer.loop();
  }
}

// what we respond to / with....
void jsonRespond( void )
{
    for ( uint8_t i = 0; i < httpServer.args(); i++ )
    {
      String argname = httpServer.argName(i);
      String argvalue = httpServer.arg(i);
      if (argname.equals("fan"))
      {
        setFanSpeed(argvalue.toInt());
      } else if (argname.equals("carousel")) {
        setCarousel(argvalue.toInt());
      }
    }
  String out;
  StaticJsonDocument<256> jsonBuffer;
  jsonBuffer["fan"] = fanSpeed;
  jsonBuffer["carousel"] = carouselState;
  serializeJson(jsonBuffer, out);
  httpServer.send(200, "text/javascript", out);
}


void setCarousel(boolean newValue)
{
  if (newValue)
  {
    digitalWrite(CAROUSEL_PIN, HIGH);
    analogWrite(FAN_PIN, fanSpeed);
    run_for = run_for_reset;
  } else {
    digitalWrite(CAROUSEL_PIN, LOW);
    analogWrite(FAN_PIN, 0);
    sleep_for = sleep_for_reset;
  }
  carouselState = newValue;
  String out;
  StaticJsonDocument<256> jsonBuffer;
  jsonBuffer["carousel"] = carouselState;
  serializeJson(jsonBuffer, out);
  webSocketServer.broadcastTXT(out);
  if (carouselState)
  {
    Serial.println("carousel=1");
  } else {
    Serial.println("carousel=0");
  }
}

void setFanSpeed(int newValue)
{
  
  fanSpeed = newValue;
  if (carouselState)
  {
    analogWrite(FAN_PIN, fanSpeed);
  }
  String out;
  StaticJsonDocument<256> jsonBuffer;
  jsonBuffer["fan"] = fanSpeed;
  serializeJson(jsonBuffer, out);
  webSocketServer.broadcastTXT(out);
  Serial.print("fan=");
  Serial.println(fanSpeed);
}

// EVERYTHING BELOW HERE IS JUST WEBSERVER STUFF

String getContentType(String filename)
{
  if (filename.endsWith(".htm")) {
    return "text/html";
  } else if (filename.endsWith(".html")) {
    return "text/html";
  } else if (filename.endsWith(".css")) {
    return "text/css";
  } else if (filename.endsWith(".js")) {
    return "application/javascript; charset=utf-8";
  } else if (filename.endsWith(".png")) {
    return "image/png";
  } else if (filename.endsWith(".gif")) {
    return "image/gif";
  } else if (filename.endsWith(".jpg")) {
    return "image/jpeg";
  } else if (filename.endsWith(".ico")) {
    return "image/x-icon";
  } else if (filename.endsWith(".svg")) {
    return "image/svg+xml";
  } else if (filename.endsWith(".xml")) {
    return "text/xml";
  } else if (filename.endsWith(".pdf")) {
    return "application/x-pdf";
  } else if (filename.endsWith(".zip")) {
    return "application/x-zip";
  } else if (filename.endsWith(".gz")) {
    return "application/x-gzip";
  }
  return "text/plain";
}

void handleFileRead(String path)
{
  bool found = false;
  if (path.endsWith("/"))
  {
    path += "index.html";
  }
  String contentType = getContentType(path);
  File file = LittleFS.open(path, "r");
  if (!file)
  {
    //Serial.print("file not found: ");
    //Serial.println(path);
  } else {
    httpServer.streamFile(file, contentType);
    file.close();
    found = true;
  }
  if (!found)
  {
    String message = "File Not Found\n\n";
    message += "URI: ";
    message += httpServer.uri();
    message += "\nMethod: ";
    message += ( httpServer.method() == HTTP_GET ) ? "GET" : "POST";
    message += "\nArguments: ";
    message += httpServer.args();
    message += "\n";
    message += "Path: ";
    message += path;
    message += "\n";
    
    for ( uint8_t i = 0; i < httpServer.args(); i++ )
    {
      message += " " + httpServer.argName ( i ) + ": " + httpServer.arg ( i ) + "\n";
    }
    httpServer.send ( 404, "text/plain", message );
  }
}

void handleRoot()
{
  handleFileRead(httpServer.uri());
}

void handleNotFound()
{
  handleFileRead(httpServer.uri());
}


If you are feeling generous and would like to support this project

Downloads

Latest Update: August 17 2021 02:55:20 PM EDT

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.