گجت ارتفاعسنج جیبی و هوشمند با برد ESP32
وقتی به دل طبیعت میزنی، کوهنوردی میکنی یا حتی سوار هواپیما میشی، دونستن ارتفاع دقیق از سطح دریا یه چالش هیجانانگیزه. داشتن یه گجت کوچیک که بتونه با دقت بالا فشار هوا و ارتفاع رو اندازه بگیره، هم جنبه آموزشی داره و هم تو سفرهات حسابی به کار میاد. ما تو تیم آیسیمدار تصمیم گرفتیم یه ارتفاعسنج جیبی و هوشمند طراحی کنیم که تو کوچیکترین ابعاد ممکن، بیشترین کارایی رو بهت بده.
قلب تپنده این پروژه برد قدرتمند و نقلی XIAO ESP32-C3 هست که در کنار سنسور فشارسنج دقیق MS5611، میتونه فشار هوا، دما و ارتفاع رو به صورت لحظهای محاسبه کنه. برای اینکه رابط کاربری جذابی داشته باشیم، از یه نمایشگر OLED و یه روتاری انکودر (ولوم چرخشی) استفاده کردیم تا بتونی خیلی راحت وارد منوهای تنظیمات بشی، دستگاه رو کالیبره کنی یا واحد اندازهگیری رو تغییر بدی.
همه این قطعات داخل یه قاب پرینت سهبعدی جمعوجور قرار میگیرن تا در نهایت یه گجت حرفهای و قابل حمل داشته باشی. پس قطعاتت رو روی میز بچین، هویه رو گرم کن و آماده باش تا با هم این ارتفاعسنج جذاب رو بسازیم.
قطعات موردنیاز
- برد توسعه Seeed Studio XIAO ESP32-C3
- ماژول سنسور فشار هوا و ارتفاعسنج MS5611
- ماژول نمایشگر OLED سایز 0.96 اینچ (SSD1306, 128x64)
- ماژول روتاری انکودر (ولوم چرخشی دکمهدار)
- باتری لیتیوم پلیمر 300 میلیآمپر ساعت
- کلید کشویی مینیاتوری برای قطع و وصل تغذیه
- مقاومتهای پولآپ 10 کیلواهم (اختیاری، برای رفع نویز احتمالی انکودر)
طراحی و پرینت سهبعدی قاب دستگاه
برای اینکه گجتمون ظاهر حرفهای و جمعوجوری داشته باشه، قاب دستگاه رو تو نرمافزار Fusion 360 طراحی کردیم. از اونجایی که فضا خیلی محدوده، باید قطعات رو طوری میچیدیم که همهچیز کاملا فیت بشه و جای کافی برای سیمکشی هم بمونه. برای این کار، مدلهای سهبعدی تمام قطعات رو وارد نرمافزار کردیم و چیدمانهای مختلف رو تست کردیم.
وقتی به بهترین چیدمان رسیدیم، بدنه نهایی رو بر اساس اون طراحی کردیم. تو هم میتونی با توجه به ابعاد قطعاتت، قاب رو با پرینتر سهبعدی چاپ کنی.
تو این پروژه ما بدنه اصلی رو با فیلامنت PLA نارنجی و پنل جلویی و سرولوم رو با رنگ مشکی پرینت گرفتیم تا یه تضاد رنگی جذاب ایجاد بشه.
برنامهنویسی و کدهای ارتفاعسنج
قبل از اینکه بریم سراغ مونتاژ، بهتره کدهای پروژه رو بررسی کنیم. این برنامه دو تا بخش اصلی داره:
- صفحه اصلی:
- نمایش لحظهای دما، فشار هوا و ارتفاع محاسبهشده.
- منوی تنظیمات (که با نگه داشتن دکمه انکودر باز میشه):
- کالیبراسیون: تنظیم ارتفاع نمایشدادهشده بر اساس ارتفاع واقعی محیط.
- واحدها: تغییر واحد ارتفاع بین متر و فوت.
- بازگشت: برگشتن به صفحه اصلی.
برای راهاندازی سنسور فشار به کتابخونه مخصوص MS5611 نیاز داری. حتما قبل از کامپایل کردن کدها، این کتابخونه رو تو محیط نرمافزار آردوینو نصب کن و بعد برنامه رو روی برد XIAO فلش بزن.
#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);
}
سیمکشی و آمادهسازی قطعات
حالا وقتشه هویه رو برداری و قطعات رو برای مونتاژ نهایی آماده کنی. برای شروع، به پایههای هر ماژول سیمهای نازک 4 سانتیمتری لحیم کن. اگه دیدی روتاری انکودر موقع چرخوندن پرش داره یا نویز میگیره، میتونی دو تا مقاومت 10 کیلواهم رو به عنوان پولدان (Pull-down) به پایههای A و B انکودر اضافه کنی.
در نهایت، تمام ماژولها رو طبق شماتیک به پایههای برد XIAO وصل کن.
مونتاژ نهایی داخل قاب
بریم سراغ جا زدن قطعات و سیمکشیها داخل قاب. اول از روتاری انکودر شروع کن. اون رو تو شیار مخصوصش روی بدنه قرار بده و با دو تا مهره سر جاش محکم ببند.
یه کم چسب پشت برد XIAO بزن و اون رو کف بدنه اصلی فیکس کن. برای سنسور فشار هوا هم یه مقدار چسب تو محفظه طراحیشده بزن و ماژول رو داخلش قرار بده. حالا کلید کشویی رو سر راه یکی از سیمهای باتری لحیم کن.
باتری رو هم چسب بزن و تو فضای خالی بدنه جا بده تا تکون نخوره. کلید تغذیه رو تو شکافی که روی دیواره کناری قاب در نظر گرفته شده قرار بده. برای تکمیل بخش تغذیه، سیمهای باتری و کلید رو به پدهای ورودی باتری روی برد XIAO لحیم کن.
نمایشگر OLED رو روی پنل جلویی قرار بده. برای محکم کردنش میتونی پایههای پلاستیکی رو با نوک هویه کمی ذوب کنی تا نمایشگر کاملا قفل بشه. حالا پنل جلویی رو روی بدنه اصلی ببند و چفت کن.
تو قدم آخر، سرولوم پرینتشده رو روی محور روتاری انکودر فشار بده تا ظاهر دستگاهت تکمیل بشه.
راهاندازی و جمعبندی پروژه
با استفاده از کلید کشویی کناری، دستگاه رو روشن کن. حواست باشه وقتی میخوای باتری رو شارژ کنی هم باید دستگاه روشن باشه تا مدار شارژ فعال بشه.
به محض روشن شدن، صفحه اصلی دما، فشار و ارتفاع رو نشون میده. اگه دکمه انکودر رو حدود 2 ثانیه نگه داری، وارد منوی تنظیمات میشی که سه تا گزینه کالیبراسیون، واحدها و بازگشت به صفحه اصلی رو داره.
اگه گزینه کالیبراسیون رو انتخاب کنی، میتونی با چرخوندن انکودر، ارتفاع رو بر اساس یه مرجع دقیق تنظیم کنی. فشار فعلی هم روی صفحه نمایش داده میشه تا کار کالیبره کردن برات راحتتر بشه.
بعد از انجام تنظیمات، کافیه دکمه انکودر رو یک بار فشار بدی تا مقادیر داخل حافظه دائم (EEPROM) برد ESP32 ذخیره بشن و برگردی به صفحه تنظیمات. تو بخش واحدها (Units) هم این امکان رو داری که واحد اندازهگیری ارتفاع رو خیلی سریع بین متر و فوت تغییر بدی.
این گجت کوچیک و کاربردی، یه نمونه فوقالعاده از ترکیب طراحی سهبعدی و برنامهنویسی میکروکنترلرهاست. ما تو تیم آیسیمدار همیشه دنبال معرفی پروژههایی هستیم که هم مهارتهای الکترونیکی تو رو بالا ببرن و هم تو دنیای واقعی کاربرد داشته باشن. امیدواریم تو هم این ارتفاعسنج هوشمند رو بسازی و تو سفرهات ازش لذت ببری.
اگه موقع ساختش سوالی برات پیش اومد، حتما تو بخش نظرات برامون بنویس تا با هم چالشها رو برطرف کنیم!
تو میتونی اولین سازنده باشی!
اگه پروژه رو ساختی به اشتراک بزار و اعتبار هدیه بگیر
این آموزش با استانداردهای اختصاصی آیسیمدار بازنویسی و بهینهسازی شده و تمامی حقوق انتشار آن متعلق به این مجموعه است و در صورت کپی یا بازنشر پیگرد قانونی خواهد داشت.








































گفتگوهای کاربران
هنوز نظری ثبت نشده؛ تو شروع کن!
هنوز نظری ثبت نشده؛ تو شروع کن!