Jeren Arellano Reeder
Published © GPL3+

L.I.F.T. Hot Air Balloon Sensors

L.I.F.T. (Lofted IoT Flight Tracker) Hot Air Balloon sensors made to improve pilot and crew safety and experience.

IntermediateFull instructions provided10
L.I.F.T. Hot Air Balloon Sensors

Things used in this project

Hardware components

Photon 2
Particle Photon 2
×1
Breadboard (generic)
Breadboard (generic)
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1
neopixel ring and single neopixel
×1
0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
×2
blue push button
×1
SRD-03SVDC-SL-C Relay
×1
hair dryer
×1
spliced extension cord
×1
Gravity: I2C BME280 Environmental Sensor
DFRobot Gravity: I2C BME280 Environmental Sensor
one with (0x76) and one with (0x77) for addresses
×2
Adafruit Ultimate GPS Breakout
Adafruit Ultimate GPS Breakout
×1

Software apps and online services

VS Code
Microsoft VS Code
Adobe Illustrator
Solidworks 2024
Adafruit.IO

Hand tools and fabrication machines

Bambu Studio X1 Carbon 3D Printer
Soldering iron (generic)
Soldering iron (generic)
Epilog laser cutter

Story

Read more

Custom parts and enclosures

Hair Dryer Mount

3D printed frame for holding the hair dryer above the basket and positioned to blow hot air into the balloon.

Sketchfab still processing.

Basket Files for Epilog Laser

Schematics

Balloon Fritzing

Code

L.I.F.T. Hot Balloon Sensor Code

C/C++
Must install included libraries and have physical components assembled. VSCODE
* Project: Go For Launch 
 * Author: Jeren Arellano Reeder
 * Date: 8/4/2025
 * For comprehensive documentation and examples, please visit:
 * https://docs.particle.io/firmware/best-practices/firmware-template/
 */

// Include Particle Device OS APIs

#include "Particle.h"
#include "Adafruit_BME280.h"
#include "Adafruit_SSD1306.h"
#include "Adafruit_GFX.h"
#include <Adafruit_MQTT.h>
#include "Adafruit_MQTT/Adafruit_MQTT_SPARK.h"
#include "Adafruit_MQTT/Adafruit_MQTT.h"
#include "neopixel.h"
#include "colors.h"
#include "button.h"
#include <Encoder.h>
#include "Adafruit_GPS.h" 
#include "credentials.h" 
// download GPS_CNM library
TCPClient TheClient; 
Adafruit_MQTT_SPARK mqtt(&TheClient,AIO_SERVER,AIO_SERVERPORT,AIO_USERNAME,AIO_KEY); 
Adafruit_MQTT_Publish pubFeed = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/buttonPressDuration");
Adafruit_MQTT_Publish pubFeed1 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/latitude");
Adafruit_MQTT_Publish pubFeed2 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/longitude");
Adafruit_MQTT_Publish pubFeed3 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/altitude");
Adafruit_MQTT_Publish pubFeed4 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/tempature1");
Adafruit_MQTT_Publish pubFeed5 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/tempature");
Adafruit_MQTT_Publish pubFeed6 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/humidity");
Adafruit_MQTT_Publish pubFeed7 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/pressure");
Adafruit_MQTT_Publish pubFeed8 = Adafruit_MQTT_Publish(&mqtt, AIO_USERNAME "/feeds/altChangeDuringPress");
// Adafruit_MQTT_Subscribe subFeed = Adafruit_MQTT_Subscribe(&mqtt, AIO_USERNAME "/feeds/balloonbutton");
void getGPS(float *latitude, float *longitude, float *altitude, int *satellites);

const int OLED_RESET=-1;
Adafruit_GPS GPS(&Wire);
Adafruit_SSD1306 display(OLED_RESET);
Adafruit_SSD1306 display1(OLED_RESET);

Encoder myEnc(D11,D12);
Button encBut(D13);

SYSTEM_MODE(MANUAL);
float subValue,pubValue,pubValue1,pubValue2,pubValue3,pubValue4,pubValue5,pubValue6,pubValue7,pubValue8;
// Run the application and system concurrently in separate threads
SYSTEM_THREAD(ENABLED);
char number = 164;

const int TIMEZONE = -6;
const unsigned int UPDATE = 10000;
int n;

//neopixel
const int PIXELCOUNT = 13;
void pixelFill(int startpixel, int endpixel, int pixelColor);
Adafruit_NeoPixel pixel(PIXELCOUNT, SPI1, WS2812B);

// Blue Button for relay
const int buttonPin = D3;

//relay
const int relayPin = D6;
int inputValue; 
unsigned int lastPublish;
// Declare Variables 
int theHour;
int maxAlt = 9800; // max ballon height relative to abq sea level in meters (max ride height)
int pixelsOn;
int startPixel;
int endPixel;
Adafruit_BME280 bme;
int hexAddress = 0x76;
float temp, pres, humid, Fahrenheit, inHg;
bool status;

Adafruit_BME280 bme1;
int hexAddress1 = 0x77;
float temp1, pres1, humid1, Fahrenheit1, inHg1;
bool status1;
//button timer and duration
unsigned long buttonPressStart = 0;
unsigned long buttonPressDuration = 0;
bool buttonWasPressed = false;
float altAtButtonPress = 0.0;


//altutude


float lat;
float lon;
float alt;
int sat;
unsigned int lastGPS;
float previousAlt;
float altitudeDrop;
//functions
void MQTT_connect();
bool MQTT_ping();
void getGPS(float *latitude, float *longitude, float *altitude, int *satellites) ;

void setup() {
  // pinMode(ledPin, OUTPUT);
  pinMode(relayPin, OUTPUT);
  pinMode(buttonPin, INPUT);

  Serial.begin(9600);
  waitFor(Serial.isConnected, 10000);
    WiFi.on();
  WiFi.connect();
  while(WiFi.connecting()) {
    Serial.printf(".");
  }
  Serial.printf("\n\n");

  // mqtt.subscribe(&subFeed);
  // while(!Serial);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display1.begin(SSD1306_SWITCHCAPVCC, 0x3D);
  display.clearDisplay();
  display1.clearDisplay();
  GPS.begin(0x10);  // The I2C address to use is 0x10
  GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
  GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); 
  GPS.sendCommand(PGCMD_ANTENNA);

  status = bme.begin(hexAddress);
  if (status == TRUE) {
  Serial.printf("BME280 at address 0x%02X started successfully!\n",hexAddress);
  }
    status = bme1.begin(hexAddress1);
  if (status == TRUE) {
  Serial.printf("BME280 at address 0x%02X started successfully!\n",hexAddress1);
  }
  pixel.begin();
  pixel.setBrightness(40);  
  pixel.show();
  display.display();
  display1.display();
}

void loop() {
  inputValue = digitalRead(buttonPin);
//gps
  GPS.read();
  if (GPS.newNMEAreceived()) {
    if (!GPS.parse(GPS.lastNMEA())) {  //cant parse/ retry
      return;
    }   
  }
      //ADAFRUIT THRUSTBUTTON
  // Adafruit_MQTT_Subscribe *subscription;
  // while ((subscription = mqtt.readSubscription(100))) {
  //   if (subscription == &subFeed) {
  //     subValue = atof((char *)subFeed.lastread);
  //   if (subValue == 1) {
  //   digitalWrite(relayPin, HIGH);
  //   Serial.println("Relaystate = ON");
  //     }
  //   }
  // }
if (millis() - lastGPS > UPDATE) { //if it can parse go here
    lastGPS = millis(); // reset the timer
    getGPS(&lat,&lon,&alt,&sat);
    Serial.printf("\n=================================================================\n");
    Serial.printf("Lat: %0.6f, Lon: %0.6f, Alt: %0.6f, Satellites: %i\n",lat, lon, alt, sat);
    Serial.printf("Pressure %f\nHumidity %f\nFahrenheit %f\n",inHg,humid,Fahrenheit);
    Serial.printf("=================================================================\n\n");

  theHour = GPS.hour + TIMEZONE;
  if(theHour < 0) {
    theHour = theHour + 24;
  }
   //DISPLAYS

    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(0,0);
    display.setRotation(0);
    display.printf("Lat: %0.6f\n", lat);
    display.printf("Lon: %0.6f\n", lon);
    display.printf("Alt: %0.2f ft.(ASL)", alt);
    display.printf("Satellites: %d\n", sat); 
    display.printf("Spd: %0.2f (Knots)\n", GPS.speed / 1.944);
    display.printf("Balloon Pres: %0.1f\nBalloon Humidity:%0.1fBalloon Temp: %0.1fF\n",inHg,humid,Fahrenheit);
    display.display();

  display1.clearDisplay();
  display1.setTextSize(1);
  display1.setTextColor(WHITE);
  display1.setCursor(0,0);
  display1.setRotation(0);
  display1.printf("Time: %02i:%02i:%02i\n", theHour, GPS.minute, GPS.seconds);
  display1.printf("Thrust Duration(s):\n");
  display1.printf("\nAltitude Change(ft):\n");
  display1.printf("\nTemp vv Button >> \n");
  display1.printf("Outside Temp %0.1fF\nBalloon Temp %0.1f \n",Fahrenheit1,Fahrenheit);

  display1.display();

}
    if (Fahrenheit > 115 ){
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(WHITE);
    display.setCursor(0, 0);
    display.setRotation(0);
    display.printf("\n  BALLOON\n   TEMP \n  WARNING");
    display.display();
}else{
      if (Fahrenheit < 99.9 )
    display.clearDisplay();

}

if (inputValue == HIGH) {
  digitalWrite(relayPin, HIGH);

  // thrust button is pressed
  if (!buttonWasPressed) {
    buttonPressStart = millis();  // Start the timer
    altAtButtonPress = alt;
    buttonWasPressed = true;
  }

} else {
  digitalWrite(relayPin, LOW);

  // If the button was just released
  if (buttonWasPressed) {
    buttonPressDuration = millis() - buttonPressStart;
    float altChangeDuringPress = alt - altAtButtonPress;
    Serial.printf("Thrust Duration(s): %.2f\n", buttonPressDuration / 1000.0);
    Serial.printf("Altitude Change(ft): %.2f\n", altChangeDuringPress);
    Serial.printf("Temp(F) %0.1f\n", Fahrenheit);

  display1.clearDisplay();
  display1.setTextSize(1);
  display1.setTextColor(WHITE);
  display1.setCursor(0,0);
  display1.setRotation(0);
  display1.printf("Time: %02i:%02i:%02i\n", theHour, GPS.minute, GPS.seconds);
  display1.printf("Thrust Duration(s): \n%.2f\n", buttonPressDuration / 1000.0);
  display1.printf("Altitude Change(ft): %.2f\n", altChangeDuringPress);
  display1.printf("Outside Temp(F) %0.1f\n", Fahrenheit1);
  display1.printf("Balloon Temp(F) %0.1f\n", Fahrenheit);

  display1.display();

  pubValue = buttonPressDuration / 1000.0;
  pubValue8 = altChangeDuringPress;
  pubFeed.publish(pubValue);
  pubFeed8.publish(pubValue8);
  Serial.printf("Publishing %0.2f \n",pubValue);
  Serial.printf("Publishing %0.2f \n",pubValue8);

  buttonWasPressed = false;
  }
}

 altitudeDrop = previousAlt - alt;
 if (altitudeDrop > 150) {
  Serial.printf("Descending To %.2f meters!!!\n", altitudeDrop);
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(WHITE);
    display.setCursor(0, 0);
    display.setRotation(0);
    display.printf("Descending To %.2f meters!!!\n", altitudeDrop);
    display.display();

}
previousAlt = alt;
temp = bme.readTemperature();
pres = bme.readPressure();
humid = bme.readHumidity();
Fahrenheit = (temp * 9/5) + 32;
inHg = (pres * 0.00029529983071445);

temp1 = bme1.readTemperature();
pres1 = bme1.readPressure();
humid1 = bme1.readHumidity();
Fahrenheit1 = (temp1 * 9/5) + 32;
inHg1 = (pres1 * 0.00029529983071445);
//neopixel calculate
pixelsOn = alt * PIXELCOUNT / maxAlt;
// pixelsOn = ((alt / maxAlt) * PIXELCOUNT) + 1;
pixel.setPixelColor(0, blue);
if (pixelsOn > PIXELCOUNT) {
    pixelsOn = PIXELCOUNT; 
}
if (pixelsOn < 0) {
    pixelsOn = 0;           
}
for (n = 1; n < PIXELCOUNT; n++) {
  startPixel = n;
   endPixel = n;
  if (n < pixelsOn) {
    pixelFill(startPixel, endPixel, green);  
  } else {
    pixelFill(startPixel, endPixel, 0);           
  }
}




   if((millis()-lastPublish > 30000)) {
    if(mqtt.Update()) {
      // pubValue = buttonPressDuration;
      pubValue1 = lat;
      pubValue2 = lon;
      pubValue3 = alt;
      pubValue4 = Fahrenheit1;
      pubValue5 = Fahrenheit;
      pubValue6 = humid;
      pubValue7 = inHg;
      // pubValue8 = altChangeDuringPress;
      // pubFeed.publish(pubValue);
      pubFeed1.publish(pubValue1);
      pubFeed2.publish(pubValue2);
      pubFeed3.publish(pubValue3);
      pubFeed4.publish(pubValue4);
      pubFeed5.publish(pubValue5);
      pubFeed6.publish(pubValue6);
      pubFeed7.publish(pubValue7);
      // pubFeed8.publish(pubValue8);
      // Serial.printf("Publishing %0.2f \n",pubValue);
      Serial.printf("Publishing %0.6f \n",pubValue1);
      Serial.printf("Publishing %0.6f \n",pubValue2); 
      Serial.printf("Publishing %0.2f \n",pubValue3); 
      Serial.printf("Publishing %0.2f \n",pubValue4);
      Serial.printf("Publishing %0.2f \n",pubValue5);   
      Serial.printf("Publishing %0.2f \n",pubValue6);   
      Serial.printf("Publishing %0.2f \n",pubValue7);   
      // Serial.printf("Publishing %0.2f \n",pubValue8);   
   
      } 
    lastPublish = millis();
}
}
void getGPS(float *latitude, float *longitude, float *altitude, int *satellites){
  int theHour;

  theHour = GPS.hour + TIMEZONE;
  if(theHour < 0) {
    theHour = theHour + 24;
  }
    
  Serial.printf("Time: %02i:%02i:%02i:%03i\n",theHour, GPS.minute, GPS.seconds, GPS.milliseconds);
  Serial.printf("Dates: %02i-%02i-20%02i\n", GPS.month, GPS.day, GPS.year);
  Serial.printf("Fix: %i, Quality: %i",(int)GPS.fix,(int)GPS.fixquality);
    if (GPS.fix) {  //gps fix returns a 0 if we are unable to get any data from a satalite 1 if you connected
      *latitude = GPS.latitudeDegrees;
      *longitude = GPS.longitudeDegrees; 
      *altitude = GPS.altitude;
      *satellites = (int)GPS.satellites; //tightcasting this data type to be a integer 
      Serial.printf("Lat: %0.6f, Lon: %0.6f, Alt: %0.6f\n",*latitude, *longitude, *altitude);
      Serial.printf("Speed (Knots): %0.2f\n",GPS.speed);
      Serial.printf("Angle: %0.2f\n",GPS.angle);
      Serial.printf("Satellites: %i\n",*satellites);

    }
}

void pixelFill(int startpixel, int endpixel, int pixelColor){
  for(int n = startpixel; n <= endpixel; n++){
    pixel.setPixelColor(n, pixelColor);
  }
  pixel.show();
}

Credits

Jeren Arellano Reeder
3 projects • 6 followers
Hardware engineer specializing in solidworks, embedded systems, circuit design, PCB layout, and integration for IoT.

Comments