Altisense : Compact Altimeter Using ESP32

by gokux in Circuits > Gadgets

368 Views, 4 Favorites, 0 Comments

Altisense : Compact Altimeter Using ESP32

s.jpg

In this project, we’ll build a compact altimeter using the MS5611 precision pressure sensor and a Seeed Studio XIAO ESP32-C3 microcontroller.

We'll display temperature, pressure, and altitude on a small OLED screen, and use a rotary encoder with a button to calibrate and switch units between meters and feet!

Supplies

DSC03962.JPG

Enclosure Design and 3D Printing

Page 1.jpg

I utilised Fusion 360 to plan and design my project, which required careful space optimisation. I needed to fit all the parts into the smallest form factor possible while ensuring practicality, including sufficient space for wiring and easy assembly. First, I imported all 3d models of the parts and tried different configurations by placing the parts in various positions. Once I found the optimal configurations, I built the enclosure around them. All design files are provided below


I printed the main body in orange PLA, also the knobe and front plate are printed using Black

Code

Here’s what the code will do:

  1. Main Screen:
  2. Display temperature, pressure, and calculated altitude.


  1. Settings Menu (after long-press on encoder button):
  2. Calibration: Adjust the displayed altitude based on the real-world reading.
  3. Units: Switch altitude display between meters and feet.
  4. Back: Return to main screen.

https://github.com/jarzebski/Arduino-MS5611: We are using this library for MS5611. Make sure to install this to IDE before flashing this code to XAIO

#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <MS5611.h> // https://github.com/jarzebski/Arduino-MS5611
#include <EEPROM.h>
#include <Arduino.h> // Required for ESP32 and IRAM_ATTR
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C // See datasheet for Address
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
MS5611 ms5611;
// Rotary Encoder Inputs - Using the working code's definitions
#define ENCODER_PIN_A D0 // D0
#define ENCODER_PIN_B D1 // D1
#define ENCODER_BUTTON_PIN D2 // D2
// EEPROM Addresses
#define EEPROM_REFERENCE_PRESSURE 0
#define EEPROM_UNITS 8
// Menu States
enum MenuState {
MAIN_PAGE,
SETTINGS_MENU,
CALIBRATION_PAGE,
UNITS_PAGE
};
MenuState currentMenuState = MAIN_PAGE;
// Calibration Variables
double currentReferencePressure;
float currentSetAltitude = 0; // User-defined altitude for calibration
// Units Variable (0 for meters, 1 for feet)
uint8_t currentUnits = 0;
// Encoder Variables - Using the working code's logic
volatile long encoderCount = 0;
int lastStateCLK;
String currentDir = "";
unsigned long lastButtonPress = 0;
unsigned long buttonDebounceTime = 1000; // Debounce delay for button in ms
byte buttonPressCount = 0;
// Function Prototypes
void displayMainPage();
void displaySettingsMenu(int selectedOption);
void displayCalibrationPage();
void displayUnitsPage(int selectedOption);
void readEncoder();
void readButton();
void saveCalibration();
void loadCalibration();
void saveUnits();
void loadUnits();
float calculateAltitude(double pressure);
float convertToFeet(float meters);
void setupEncoder();
void setupButton();
void setup() {
Serial.begin(115200);
// Initialize MS5611 sensor
Serial.println("Initialize MS5611 Sensor");
while (!ms5611.begin()) {
Serial.println("Could not find a valid MS5611 sensor, check wiring!");
delay(500);
}
// Initialize OLED display
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Don't proceed, loop forever
}
display.clearDisplay();
display.display();
delay(500);
// Initialize Encoder
setupEncoder();
// Initialize Button
setupButton();
// Load calibration and units from EEPROM
loadCalibration();
loadUnits();
// Set initial reference pressure
currentReferencePressure = ms5611.readPressure();
Serial.print("Initial Reference Pressure: ");
Serial.println(currentReferencePressure);
Serial.print("Initial Units: ");
Serial.println(currentUnits == 0 ? "Meters" : "Feet");
displayMainPage();
}
void loop() {
readButton();
readEncoder(); // Call the encoder reading function in the loop
switch (currentMenuState) {
case MAIN_PAGE:
displayMainPage();
break;
case SETTINGS_MENU: {
static int selectedSetting = 0;
if (encoderCount != 0) {
selectedSetting -= encoderCount;
if (selectedSetting < 0) selectedSetting = 2;
if (selectedSetting > 2) selectedSetting = 0;
encoderCount = 0;
displaySettingsMenu(selectedSetting);
}
if (buttonPressCount == 1) {
buttonPressCount = 0; // Reset after action
if (selectedSetting == 0) {
currentMenuState = CALIBRATION_PAGE;
currentSetAltitude = calculateAltitude(ms5611.readPressure()); // Initialize with current calculated altitude
displayCalibrationPage();
} else if (selectedSetting == 1) {
currentMenuState = UNITS_PAGE;
displayUnitsPage(currentUnits);
} else if (selectedSetting == 2) {
currentMenuState = MAIN_PAGE;
displayMainPage();
}
}
break;
}
case CALIBRATION_PAGE:
if (encoderCount != 0) {
currentSetAltitude += encoderCount; // Adjust the 'Set Altitude' value
encoderCount = 0;
displayCalibrationPage();
}
if (buttonPressCount == 1) {
buttonPressCount = 0;
// Recalculate reference pressure based on the set altitude
long currentPressure = ms5611.readPressure();
currentReferencePressure = currentPressure / pow(1.0 - (currentSetAltitude / 44330.0), 5.255);
saveCalibration();
currentMenuState = SETTINGS_MENU;
displaySettingsMenu(0);
}
break;
case UNITS_PAGE: {
static int selectedUnit = currentUnits;
if (encoderCount != 0) {
selectedUnit -= encoderCount;
if (selectedUnit < 0) selectedUnit = 1;
if (selectedUnit > 1) selectedUnit = 0;
encoderCount = 0;
displayUnitsPage(selectedUnit);
}
if (buttonPressCount == 1) {
buttonPressCount = 0;
currentUnits = selectedUnit;
saveUnits();
currentMenuState = SETTINGS_MENU;
displaySettingsMenu(1);
}
break;
}
}
delay(1); // Small delay for overall loop
}
void displayMainPage() {
double realTemperature = ms5611.readTemperature();
long realPressure = ms5611.readPressure();
float altitudeMeters = calculateAltitude(realPressure);
float altitudeDisplay = (currentUnits == 1) ? convertToFeet(altitudeMeters) : altitudeMeters;
String unitString = (currentUnits == 1) ? "ft" : "m";
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(3, 50);
display.print("Tmp");
display.setTextSize(2);
display.setCursor(24, 46);
display.print(realTemperature, 1);
display.print("c");
display.setTextSize(1);
display.setCursor(3, 28);
display.print("Pre");
display.setTextSize(2);
display.setTextSize(2);
display.setCursor(24, 24);
display.print((int)(realPressure / 100));
display.print("hPa");
display.setTextSize(1);
display.setCursor(3, 7);
display.print("Alt");
display.setTextSize(2);
display.setCursor(24, 3);
display.print(altitudeDisplay, 0);
display.print(unitString);
display.display();
}
void displaySettingsMenu(int selectedOption) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
String options[] = {"Calibration", "Units", "Back"};
for (int i = 0; i < 3; i++) {
display.setCursor(0, i * 16);
if (i == selectedOption) {
display.print("> ");
} else {
display.print(" ");
}
display.println(options[i]);
}
display.display();
}
void displayCalibrationPage() {
long realPressure = ms5611.readPressure();
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(4, 4);
display.print("Altitude Calibration");
display.setCursor(6, 50);
display.print("Pre");
display.setTextSize(2);
display.setCursor(30, 46);
display.print((int)(realPressure / 100));
display.print("hPa");
display.setTextSize(1);
display.setCursor(6, 27);
display.print("Alt");;
display.setTextSize(2);
display.setCursor(32, 23);
display.print(currentSetAltitude, 0);
display.print("m");
display.display();
}
void displayUnitsPage(int selectedUnit) {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.print("Select Units:");
display.setCursor(0, 16);
if (selectedUnit == 0) {
display.print("> Meters");
} else {
display.print(" Meters");
}
display.setCursor(0, 32);
if (selectedUnit == 1) {
display.print("> Feet");
} else {
display.print(" Feet");
}
display.display();
}
void readEncoder() {
// Rotary Encoder Inputs
int currentStateCLK = digitalRead(ENCODER_PIN_A);
// If last and current state of CLK are different, then pulse occurred
// React to only 1 state change to avoid double count
if (currentStateCLK != lastStateCLK && currentStateCLK == 1) {
// If the DT state is different than the CLK state then
// the encoder is rotating CCW so decrement
if (digitalRead(ENCODER_PIN_B) != currentStateCLK) {
encoderCount--;
currentDir = "CCW";
} else {
// Encoder is rotating CW so increment
encoderCount++;
currentDir = "CW";
}
Serial.print("Direction: ");
Serial.print(currentDir);
Serial.print(" | Counter: ");
Serial.println(encoderCount);
}
// Remember last CLK state
lastStateCLK = currentStateCLK;
}
void readButton() {
unsigned long currentTime = millis();
int buttonState = digitalRead(ENCODER_BUTTON_PIN);
if (buttonState == LOW) {
if (currentTime - lastButtonPress > buttonDebounceTime) {
buttonPressCount++;
lastButtonPress = currentTime;
}
}
if (currentMenuState == MAIN_PAGE && buttonPressCount >= 2) {
currentMenuState = SETTINGS_MENU;
displaySettingsMenu(0);
buttonPressCount = 0; // Reset the count after entering the menu
} else if (currentMenuState != MAIN_PAGE && buttonPressCount >= 1) {
// For other menus, a single press acts as "select" or "save"
// The action is handled within the respective menu's state logic
buttonPressCount = 1; // Ensure it's treated as a single action
} else if (buttonState == HIGH) {
// Reset the count if the button is released for a while
if (currentTime - lastButtonPress > 200) { // Adjust this delay as needed
buttonPressCount = 0;
}
}
}
void saveCalibration() {
EEPROM.put(EEPROM_REFERENCE_PRESSURE, currentReferencePressure);
Serial.println("Calibration saved to EEPROM");
}
void loadCalibration() {
if (EEPROM.read(EEPROM_REFERENCE_PRESSURE) != 0xFF) { // Check if EEPROM has been written before
EEPROM.get(EEPROM_REFERENCE_PRESSURE, currentReferencePressure);
Serial.print("Calibration loaded from EEPROM: ");
Serial.println(currentReferencePressure);
} else {
Serial.println("No calibration data in EEPROM, using default.");
}
}
void saveUnits() {
EEPROM.write(EEPROM_UNITS, currentUnits);
Serial.print("Units saved to EEPROM: ");
Serial.println(currentUnits == 0 ? "Meters" : "Feet");
}
void loadUnits() {
currentUnits = EEPROM.read(EEPROM_UNITS);
if (currentUnits != 0 && currentUnits != 1) {
currentUnits = 0; // Default to meters if invalid value
Serial.println("Invalid units in EEPROM, using default (Meters).");
} else {
Serial.print("Units loaded from EEPROM: ");
Serial.println(currentUnits == 0 ? "Meters" : "Feet");
}
}
float calculateAltitude(double pressure) {
// Simplified altitude calculation based on pressure and reference pressure
// Assumes standard atmospheric conditions
return 44330.0 * (1.0 - pow(pressure / currentReferencePressure, 0.1903));
}
float convertToFeet(float meters) {
return meters * 3.28084;
}
void setupEncoder() {
// Set encoder pins as inputs with pull-up resistors
pinMode(ENCODER_PIN_A, INPUT_PULLUP);
pinMode(ENCODER_PIN_B, INPUT_PULLUP);
// Initialize the last state of CLK for the encoder reading logic
lastStateCLK = digitalRead(ENCODER_PIN_A);
}
void setupButton() {
// Set the button pin as an input with a pull-up resistor
pinMode(ENCODER_BUTTON_PIN, INPUT_PULLUP);
}

Wiring

C.jpg

We can start the assembly process by preparing the components for the final assembly

I soldered a 4 cm small-gauge wire in every pin of the module

Optionally, you can solder a 10k pull-down resistor to the A and B pins of the encoder.

At last, wire up everything to the XIAO

Final Assembly

Let's place our wire harness and components into the enclosure.

First, we can start with the encoder. Place the encoder in the 3d printed slot and use 2 nuts to secure the encoder to the 3d print

Put some glue on top of the XIAO and put it on the main body

Apply glue to the 3d-printed slot for the pressure sensor module and insert the module into the slot.

Connect the power switch to the battery

Glue the battery and place it onto the main body

Place the power switch in the 3D printed slot on the side wall

To finish the wiring, solder the battery wires and power switch wires to the battery input of the XIAO

Attach the display module to the front cap and melt the plastic to secure everything.

Place the font cap on the main body

To finish our build, connect the 3D printed knob to the encoder.


Operation

s.jpg

The user can power on the device using the slide switch. It is important to ensure that the device is powered on during the battery charging process.

Upon booting up, the main screen will display the temperature, pressure, and altitude. If the user presses the encoder button for 2 seconds, they will be taken to the settings menu, which includes three options: Calibration, Units, and Back to Main Page.

If the user selects the Calibration option, they will be directed to the calibration page. Here, the user can adjust the altitude using the rotating encoder. They will be able to view the current pressure and set the altitude accordingly. After making the adjustments, the user can press the encoder button once to save the values to the EEPROM of the ESP32 and return to the settings page.

If the user selects the Units option, they will have the ability to change the measurement units for altitude between feet and meters.