ESP32 Aneroid Barometer using Squareline Studio and LVGL on CrowPanel Round display
How modern electronics can completely replace and improve the classic mechanical instrument, offering digital precision in a beautiful retro package.
Devices & Components
1
Gravity: I2C BME280 Environmental Sensor
1
Elecrow CrowPanel 2.1inch-HMI ESP32 Rotary Display 480*480
Hardware & Tools
1
Soldering Station
Software & Tools
1
LVGL Image Converter
1
Squareline Studio
Arduino IDE
Project description
Code
Code Baro
cpp
...
1#include <Arduino.h> 2#include <lvgl.h> 3#include <Arduino_GFX_Library.h> 4#include <Wire.h> 5#include <Adafruit_BME280.h> 6 7extern "C" { 8 #include "ui.h" 9} 10 11// ---------- BME280 ---------- 12Adafruit_BME280 bme; 13 14// Пинови за енкодерот од Radio.txt [cite: 244] 15#define ENCODER_CLK 4 16#define ENCODER_DT 42 17#define ENCODER_SW 41 18 19// Променливи за жолтата стрелка 20extern lv_obj_t * ui_Image5; // Дефинирано во ui_Screen1.h 21static float manual_pressure_set = 1013.0f; // Почетна вредност 22volatile long encoderValue = 0; 23long lastEncoderValue = 0; 24volatile int lastEncoded = 0; 25 26static const float ALTITUDE_M = 700.0f; // твоја надморска висина 27static const float P_MIN_HPA = 950.0f; 28static const float P_MAX_HPA = 1050.0f; 29 30// update rate + smoothing 31static uint32_t lastReadMs = 0; 32static const uint32_t READ_PERIOD_MS = 1000; 33 34// “smoothed” pressure to avoid jitter 35static float p0_hpa_smoothed = 1000.0f; 36 37// ---------- NEEDLE LVGL OBJECT ---------- 38// IMPORTANT: Ова мора да е точниот UI pointer за црната стрелка од SquareLine. 39// Отвори ui_Screen1.h и најди како се вика (пример: ui_ImageBlack или ui_needleBlack). 40// ТУКА стави го истиот. 41extern lv_obj_t * ui_Image2; // <-- смени ако кај тебе е друго име 42 43// Ако 0° во код не ти е 950 позицијата, тука стави offset во степени. 44// Пример ако ти треба да ја ротираш “стартно” за -130°: NEEDLE_OFFSET_DEG = -130.0f; 45static const float NEEDLE_OFFSET_DEG = -131.0f; 46 47 48// -------------------- Panel constants -------------------- 49static const uint16_t screenWidth = 480; 50static const uint16_t screenHeight = 480; 51 52#define SCREEN_BACKLIGHT_PIN 6 53static const int pwmFreq = 5000; 54static const int pwmChannel = 0; 55static const int pwmResolution = 8; 56 57// -------------------- LVGL draw buffer -------------------- 58static lv_disp_draw_buf_t draw_buf; 59static lv_color_t *buf1 = nullptr; // LVGL color buffer (in PSRAM if possible) 60 61// -------------------- Display objects (same as factory) -------------------- 62// NOTE: These pin mappings are copied from your factory .ino snippet. 63Arduino_ESP32RGBPanel *bus = new Arduino_ESP32RGBPanel( 64 16 /* CS */, 2 /* SCK */, 1 /* SDA */, 65 40 /* DE */, 7 /* VSYNC */, 15 /* HSYNC */, 41 /* PCLK */, 66 46 /* R0 */, 3 /* R1 */, 8 /* R2 */, 18 /* R3 */, 17 /* R4 */, 67 14 /* G0 */, 13 /* G1 */, 12 /* G2 */, 11 /* G3 */, 10 /* G4 */, 9 /* G5 */, 68 5 /* B0 */, 45 /* B1 */, 48 /* B2 */, 47 /* B3 */, 21 /* B4 */ 69); 70 71// In factory they use Arduino_ST7701_RGBPanel + st7701_type5_init_operations 72// (these init operation arrays are provided by Arduino_GFX_Library for ST7701 panels) 73Arduino_ST7701_RGBPanel *gfx = new Arduino_ST7701_RGBPanel( 74 bus, GFX_NOT_DEFINED /* RST */, 0 /* rotation */, 75 false /* IPS */, screenWidth, screenHeight, 76 st7701_type5_init_operations, sizeof(st7701_type5_init_operations), 77 true /* BGR */, 78 10 /* hsync_front_porch */, 4 /* hsync_pulse_width */, 20 /* hsync_back_porch */, 79 10 /* vsync_front_porch */, 4 /* vsync_pulse_width */, 20 /* vsync_back_porch */ 80); 81 82// -------------------- LVGL flush -------------------- 83void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) 84{ 85 uint32_t w = (area->x2 - area->x1 + 1); 86 uint32_t h = (area->y2 - area->y1 + 1); 87 88#if (LV_COLOR_16_SWAP != 0) 89 gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h); 90#else 91 gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h); 92#endif 93 94 lv_disp_flush_ready(disp); 95} 96 97// -------------------- Backlight helper -------------------- 98static void backlight_on(uint8_t level_0_255) 99{ 100 ledcSetup(pwmChannel, pwmFreq, pwmResolution); 101 ledcAttachPin(SCREEN_BACKLIGHT_PIN, pwmChannel); 102 ledcWrite(pwmChannel, level_0_255); 103} 104 105static float seaLevelFromAltitude(float pressure_hpa, float altitude_m) 106{ 107 // pressure_hpa = measured absolute pressure at altitude 108 // returns sea-level equivalent pressure 109 return pressure_hpa / powf(1.0f - (altitude_m / 44330.0f), 5.255f); 110} 111 112static float clampf(float x, float a, float b) 113{ 114 if (x < a) return a; 115 if (x > b) return b; 116 return x; 117} 118 119static float pressureToAngleDeg(float p0_hpa) 120{ 121 float p = clampf(p0_hpa, P_MIN_HPA, P_MAX_HPA); 122 float t = (p - P_MIN_HPA) / (P_MAX_HPA - P_MIN_HPA); // 0..1 123 return (t * 260.0f) + NEEDLE_OFFSET_DEG; 124} 125 126static void setNeedleAngleDeg(lv_obj_t *img, float angle_deg) 127{ 128 // LVGL uses 0.1 degree units 129 int32_t a10 = (int32_t)lroundf(angle_deg * 10.0f); 130 131 // Ако не си го сетирал pivot во SquareLine: 132 // lv_img_set_pivot(img, 240, 240); // <-- стави точни pivot координати за твојата стрелка 133 134 lv_img_set_angle(img, a10); 135} 136 137void IRAM_ATTR updateEncoder() { 138 int MSB = digitalRead(ENCODER_CLK); 139 int LSB = digitalRead(ENCODER_DT); 140 int encoded = (MSB << 1) | LSB; 141 int sum = (lastEncoded << 2) | encoded; 142 143 if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue++; 144 if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue--; 145 146 lastEncoded = encoded; 147} 148 149void setup() 150{ 151 Serial.begin(115200); 152 153 // 1. I2C кон CrowPanel со зголемена брзина (400kHz) 154 Wire.begin(38, 39); 155 Wire.setClock(400000); 156 157 bool ok = bme.begin(0x76); 158 159 float initial_p = bme.readPressure() / 100.0f; 160p0_hpa_smoothed = seaLevelFromAltitude(initial_p, ALTITUDE_M); 161 162 if (!ok) ok = bme.begin(0x77); 163 if (!ok) { 164 Serial.println("ERROR: BME280 not found!"); 165 while (1) delay(1000); 166 } 167 168 delay(200); 169 Serial.println("Boot..."); 170 171 // 2. Вклучување на осветлувањето 172 backlight_on(220); 173 174 // 3. Иницијализација на дисплејот 175 Serial.println("gfx begin..."); 176 gfx->begin(12000000); // Намалено на 12MHz за елиминирање на трепкањето 177 gfx->fillScreen(BLACK); 178 179 Serial.println("lv_init..."); 180 lv_init(); 181 182 // 4. Оптимизација на баферот во внатрешната RAM (SRAM) 183 // Ова го решава хоризонталното трепкање 184 size_t buffer_size = screenWidth * 40 * sizeof(lv_color_t); 185 lv_color_t *internal_buf = (lv_color_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); 186 187 if (!internal_buf) { 188 // Ако нема доволно Internal RAM, користи PSRAM 189 internal_buf = (lv_color_t *)ps_malloc(buffer_size); 190 Serial.println("Warning: Using PSRAM for buffer"); 191 } 192 193 lv_disp_draw_buf_init(&draw_buf, internal_buf, NULL, screenWidth * 40); 194 195 // 5. Конфигурација на дисплеј драјверот 196 static lv_disp_drv_t disp_drv; 197 lv_disp_drv_init(&disp_drv); 198 disp_drv.hor_res = screenWidth; 199 disp_drv.ver_res = screenHeight; 200 disp_drv.flush_cb = my_disp_flush; 201 disp_drv.draw_buf = &draw_buf; 202 203 // Исклучено full_refresh за побрзо цртање само на стрелката 204 disp_drv.full_refresh = 0; 205 206 lv_disp_drv_register(&disp_drv); 207 208 Serial.println("ui_init..."); 209 ui_init(); 210 211 // Иницијализација на енкодерот 212 pinMode(ENCODER_CLK, INPUT_PULLUP); 213 pinMode(ENCODER_DT, INPUT_PULLUP); 214 attachInterrupt(digitalPinToInterrupt(ENCODER_CLK), updateEncoder, CHANGE); 215 attachInterrupt(digitalPinToInterrupt(ENCODER_DT), updateEncoder, CHANGE); 216 217 if (ui_Image5) { 218 lv_img_set_pivot(ui_Image5, 18, 150); // Жолта стрелка (исти димензии 50x240) 219 // Постави ја жолтата стрелка на почетна позиција (1013 hPa) 220 float startAng = pressureToAngleDeg(manual_pressure_set); 221 lv_img_set_angle(ui_Image5, (int32_t)(startAng * 10.0f)); 222 } 223 224 // 6. Подесување на Pivot (Центар на ротација на стрелката) 225 if (ui_Image2) { 226 // x=25 (средина), y=220 (близу дното на стрелката) 227 lv_img_set_pivot(ui_Image2, 18, 150); 228 Serial.println("Pivot adjusted."); 229 } 230 231 Serial.println("Done."); 232} 233 234// Помошна функција која LVGL ја повикува за да го менува аголот за време на анимацијата 235void set_needle_angle_anim(void * img, int32_t v) 236{ 237 lv_img_set_angle((lv_obj_t *)img, v); 238} 239 240// Главна функција за активирање на анимацијата 241void update_needle_with_anim(lv_obj_t * obj, int32_t target_angle10) 242{ 243 int32_t current_angle = lv_img_get_angle(obj); 244 245 // ЛОГИКА ЗА НАЈКРАТОК ПАТ: 246 // Го пресметуваме растојанието помеѓу моменталниот и новиот агол 247 int32_t diff = target_angle10 - current_angle; 248 249 // Ако разликата е поголема од 180 степени (1800 единици), 250 // значи анимацијата тргнала по подолгиот пат околу кругот. 251 // Со додавање/одземање на 3600 ја присилуваме да оди по краткиот пат. 252 if (diff > 1800) target_angle10 -= 3600; 253 else if (diff < -1800) target_angle10 += 3600; 254 255 lv_anim_t a; 256 lv_anim_init(&a); 257 lv_anim_set_var(&a, obj); 258 lv_anim_set_values(&a, current_angle, target_angle10); 259 lv_anim_set_time(&a, 400); 260 lv_anim_set_exec_cb(&a, set_needle_angle_anim); 261 lv_anim_set_path_cb(&a, lv_anim_path_ease_out); 262 lv_anim_start(&a); 263} 264 265void loop() 266{ 267 static uint32_t last_tick = millis(); 268 uint32_t current_tick = millis(); 269 lv_tick_inc(current_tick - last_tick); 270 last_tick = current_tick; 271 272 // Справување со енкодерот за жолтата стрелка 273 if (encoderValue != lastEncoderValue) { 274 // Секој чекор на енкодерот менува 1 hPa (можете да смените на 0.1 како кај радиото) 275 float diff = (encoderValue - lastEncoderValue) * 1.0f; 276 manual_pressure_set += diff; 277 278 // Ограничување во опсегот 950-1050 hPa [cite: 223, 331] 279 if (manual_pressure_set < 950.0f) manual_pressure_set = 950.0f; 280 if (manual_pressure_set > 1050.0f) manual_pressure_set = 1050.0f; 281 282 // Пресметка на агол и движење на жолтата стрелка [cite: 224, 225] 283 float yellowAng = pressureToAngleDeg(manual_pressure_set); 284 if (ui_Image5) { 285 lv_img_set_angle(ui_Image5, (int32_t)(yellowAng * 10.0f)); 286 } 287 288 Serial.print("Manual Marker Set: "); 289 Serial.println(manual_pressure_set); 290 291 lastEncoderValue = encoderValue; 292 } 293 294 lv_timer_handler(); 295 296 static uint32_t lastReadMs = 0; 297 static int32_t lastAngle10 = INT32_MIN; 298 299 if (millis() - lastReadMs >= 1000) { 300 lastReadMs = millis(); 301 302 float p_hpa = bme.readPressure() / 100.0f; 303 float p0_hpa = seaLevelFromAltitude(p_hpa, ALTITUDE_M); 304 305 // Филтерот може да остане 0.3f, анимацијата ќе го заврши останатото 306 const float alpha = 0.3f; 307 p0_hpa_smoothed = (1.0f - alpha) * p0_hpa_smoothed + alpha * p0_hpa; 308 309 float ang = pressureToAngleDeg(p0_hpa_smoothed); 310 int32_t ang10 = (int32_t)lroundf(ang * 10.0f); 311 312 // Ажурирање со анимација 313 if (ui_Image2 && (lastAngle10 == INT32_MIN || abs(ang10 - lastAngle10) >= 3)) { 314 lastAngle10 = ang10; 315 316 // ПОВИКУВАЊЕ НА АНИМАЦИЈАТА 317 update_needle_with_anim(ui_Image2, ang10); 318 } 319 320 // Serial испис 321 Serial.print("P0: "); 322 Serial.print(p0_hpa_smoothed, 2); 323 Serial.print(" hPa | Angle: "); 324 Serial.println(ang, 1); 325 } 326 327 delay(5); 328}
Downloadable files
Code full
....
Code Final.zip
Comments
Only logged in users can leave comments