ESP32 CYD с LVGL: отображение температуры на линейном графике (BME280)

В этом руководстве вы узнаете, как отображать температуру с датчика BME280 на ESP32 Cheap Yellow Display (CYD) с использованием LVGL (Light Versatile Graphics Library). Вы научитесь рисовать линейный график для отображения данных с датчика. ESP32 будет программироваться с помощью Arduino IDE.

ESP32 CYD с LVGL: отображение температуры на линейном графике (BME280)

Впервые работаете с ESP32 Cheap Yellow Display? Начните здесь: Getting Started with ESP32 Cheap Yellow Display Board – CYD (ESP32-2432S028R).

Обзор проекта

В этом проекте мы нарисуем линейный график с температурой от датчика BME280 на экране ESP32 CYD. Вот основные функции:

  • График отображает максимум 20 точек данных;

  • При добавлении новой точки на экран самая старая точка данных удаляется;

  • Диапазон вертикальной оси автоматически подстраивается в зависимости от текущих значений, отображаемых на графике;

  • Вы можете коснуться рядом с точками данных, чтобы проверить точное значение точки – рядом с ней отобразится метка с её значением.

Обзор проекта ESP32 CYD - линейный график BME280

Предварительные требования

Перед тем как продолжить, убедитесь, что вы выполнили следующие предварительные требования. Вы должны выполнить все шаги, иначе ваш проект не будет работать.

1) Необходимые компоненты и подключение схемы

Для этого проекта вам понадобятся следующие компоненты:

Мы будем использовать протокол связи I2C для получения данных с датчика BME280. На плате ESP32 CYD есть разъём расширенного ввода-вывода, который позволяет подключать внешние периферийные устройства. Мы подключим датчик к разъёму CN1.

ESP32 CYD Cheap Yellow Display - подключение датчика BME280 через JST разъём

В комплекте с вашей платой должен был идти небольшой JST-разъём для доступа к этим GPIO.

Мы подключим SDA к GPIO 27, а SCL к GPIO 22.

ESP32 CYD Cheap Yellow Display - JST разъём

Если вы не знакомы с датчиком BME280, рекомендуем прочитать наше руководство для начинающих: ESP32 with BME280 Sensor using Arduino IDE (Pressure, Temperature, Humidity).

На плате CYD должен быть встроенный LDR (фоторезистор) рядом с экраном, подключённый к GPIO 34.

ESP32 Cheap Yellow Display LDR (фоторезистор)

2) Установка плат ESP32 в Arduino IDE

Логотип Arduino IDE 2

Мы будем программировать ESP32 с помощью Arduino IDE. Убедитесь, что у вас установлены платы ESP32. Следуйте следующему руководству:

3) Знакомство с ESP32 Cheap Yellow Display

Плата разработки ESP32-2432S028R стала известна в сообществе мейкеров как «Cheap Yellow Display» или сокращённо CYD. Эта плата разработки, основным чипом которой является модуль ESP32-WROOM-32, оснащена 2.8-дюймовым TFT сенсорным ЖК-дисплеем, интерфейсом карты microSD, RGB-светодиодом и всеми необходимыми схемами для программирования и питания платы.

ESP32 Cheap Yellow Display CYD Board ESP32-2432S028R - вид спереди

Если вы впервые используете ESP32 Cheap Yellow Display, обязательно следуйте нашему руководству для начинающих:

4) Установка библиотек TFT и LVGL

LVGL (Light and Versatile Graphics Library) – это бесплатная графическая библиотека с открытым исходным кодом, которая предоставляет широкий спектр простых в использовании графических элементов для ваших проектов на микроконтроллерах, требующих графического пользовательского интерфейса (GUI).

Логотип LVGL

Следуйте следующему руководству, чтобы установить и настроить необходимые библиотеки для использования LVGL с ESP32 Cheap Yellow Display в Arduino IDE.

5) Установка библиотек BME280

Для этого проекта мы будем использовать библиотеку Adafruit BME280 для получения данных с BME280. В Arduino IDE перейдите в Sketch > Include Library > Manage Libraries. Найдите Adafruit BME280 Library в поле поиска и установите библиотеку. Также установите все зависимости, которые в данный момент не установлены (обычно это библиотеки Adafruit Bus IO и Adafruit Unified Sensor).

Установка библиотеки Adafruit BME280 в Arduino IDE

Узнайте больше о распиновке CYD: ESP32 Cheap Yellow Display (CYD) Pinout (ESP32-2432S028R).

Отображение линейного графика температуры на ESP32 CYD с LVGL – код Arduino

Следующий код создаст график со значениями с датчика BME280.

/*  Rui Santos & Sara Santos - Random Nerd Tutorials - https://RandomNerdTutorials.com/esp32-cyd-lvgl-line-chart/  |  https://RandomNerdTutorials.com/esp32-tft-lvgl-line-chart/
    THIS EXAMPLE WAS TESTED WITH THE FOLLOWING HARDWARE:
    1) ESP32-2432S028R 2.8 inch 240×320 also known as the Cheap Yellow Display (CYD): https://makeradvisor.com/tools/cyd-cheap-yellow-display-esp32-2432s028r/
      SET UP INSTRUCTIONS: https://RandomNerdTutorials.com/cyd-lvgl/
    2) REGULAR ESP32 Dev Board + 2.8 inch 240x320 TFT Display: https://makeradvisor.com/tools/2-8-inch-ili9341-tft-240x320/ and https://makeradvisor.com/tools/esp32-dev-board-wi-fi-bluetooth/
      SET UP INSTRUCTIONS: https://RandomNerdTutorials.com/esp32-tft-lvgl/
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
    The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/

/*  Install the "lvgl" library version 9.X by kisvegabor to interface with the TFT Display - https://lvgl.io/
    *** IMPORTANT: lv_conf.h available on the internet will probably NOT work with the examples available at Random Nerd Tutorials ***
    *** YOU MUST USE THE lv_conf.h FILE PROVIDED IN THE LINK BELOW IN ORDER TO USE THE EXAMPLES FROM RANDOM NERD TUTORIALS ***
    FULL INSTRUCTIONS AVAILABLE ON HOW CONFIGURE THE LIBRARY: https://RandomNerdTutorials.com/cyd-lvgl/ or https://RandomNerdTutorials.com/esp32-tft-lvgl/   */
#include <lvgl.h>

/*  Install the "TFT_eSPI" library by Bodmer to interface with the TFT Display - https://github.com/Bodmer/TFT_eSPI
    *** IMPORTANT: User_Setup.h available on the internet will probably NOT work with the examples available at Random Nerd Tutorials ***
    *** YOU MUST USE THE User_Setup.h FILE PROVIDED IN THE LINK BELOW IN ORDER TO USE THE EXAMPLES FROM RANDOM NERD TUTORIALS ***
    FULL INSTRUCTIONS AVAILABLE ON HOW CONFIGURE THE LIBRARY: https://RandomNerdTutorials.com/cyd-lvgl/ or https://RandomNerdTutorials.com/esp32-tft-lvgl/   */
#include <TFT_eSPI.h>

// Install the "XPT2046_Touchscreen" library by Paul Stoffregen to use the Touchscreen - https://github.com/PaulStoffregen/XPT2046_Touchscreen - Note: this library doesn't require further configuration
#include <XPT2046_Touchscreen.h>

// Install Adafruit Unified Sensor and Adafruit BME280 Library
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define I2C_SDA 27
#define I2C_SCL 22
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

// SET VARIABLE TO 0 FOR TEMPERATURE IN FAHRENHEIT DEGREES
#define TEMP_CELSIUS 1
#define BME_NUM_READINGS 20
float bme_last_readings[BME_NUM_READINGS] = {-20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0};
float scale_min_temp;
float scale_max_temp;

// Touchscreen pins
#define XPT2046_IRQ 36   // T_IRQ
#define XPT2046_MOSI 32  // T_DIN
#define XPT2046_MISO 39  // T_OUT
#define XPT2046_CLK 25   // T_CLK
#define XPT2046_CS 33    // T_CS

SPIClass touchscreenSPI = SPIClass(VSPI);
XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);

#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 320

// Touchscreen coordinates: (x, y) and pressure (z)
int x, y, z;

#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

// Generally, you should use "unsigned long" for variables that hold time
unsigned long previousMillis = 0; // will store last time the chart was updated
// Interval at which the chart will be updated (milliseconds) 10000 milliseconds = 10 seconds
const long interval = 10000;

// If logging is enabled, it will inform the user about what is happening in the library
void log_print(lv_log_level_t level, const char * buf) {
  LV_UNUSED(level);
  Serial.println(buf);
  Serial.flush();
}

// Get the Touchscreen data
void touchscreen_read(lv_indev_t * indev, lv_indev_data_t * data) {
  // Checks if Touchscreen was touched, and prints X, Y and Pressure (Z)
  if(touchscreen.tirqTouched() && touchscreen.touched()) {
    // Get Touchscreen points
    TS_Point p = touchscreen.getPoint();

    // Advanced Touchscreen calibration, LEARN MORE » https://RandomNerdTutorials.com/touchscreen-calibration/
    float alpha_x, beta_x, alpha_y, beta_y, delta_x, delta_y;

    // REPLACE WITH YOUR OWN CALIBRATION VALUES » https://RandomNerdTutorials.com/touchscreen-calibration/
    alpha_x = -0.000;
    beta_x = 0.090;
    delta_x = -33.771;
    alpha_y = 0.066;
    beta_y = 0.000;
    delta_y = -14.632;

    x = alpha_y * p.x + beta_y * p.y + delta_y;
    // clamp x between 0 and SCREEN_WIDTH - 1
    x = max(0, x);
    x = min(SCREEN_WIDTH - 1, x);

    y = alpha_x * p.x + beta_x * p.y + delta_x;
    // clamp y between 0 and SCREEN_HEIGHT - 1
    y = max(0, y);
    y = min(SCREEN_HEIGHT - 1, y);

    // Basic Touchscreen calibration points with map function to the correct width and height
    //x = map(p.x, 200, 3700, 1, SCREEN_WIDTH);
    //y = map(p.y, 240, 3800, 1, SCREEN_HEIGHT);

    z = p.z;

    data->state = LV_INDEV_STATE_PRESSED;

    // Set the coordinates
    data->point.x = x;
    data->point.y = y;

    // Print Touchscreen info about X, Y and Pressure (Z) on the Serial Monitor
    Serial.print("X = ");
    Serial.print(x);
    Serial.print(" | Y = ");
    Serial.print(y);
    Serial.print(" | Pressure = ");
    Serial.print(z);
    Serial.println();
  }
  else {
    data->state = LV_INDEV_STATE_RELEASED;
  }
}

// Draw a label on that chart with the value of the pressed point
static void chart_draw_label_cb(lv_event_t * e) {
  lv_event_code_t code = lv_event_get_code(e);
  lv_obj_t * chart = (lv_obj_t*) lv_event_get_target(e);

  if(code == LV_EVENT_VALUE_CHANGED) {
    lv_obj_invalidate(chart);
  }
  if(code == LV_EVENT_REFR_EXT_DRAW_SIZE) {
    int32_t * s = (int32_t*)lv_event_get_param(e);
    *s = LV_MAX(*s, 20);
  }
  // Draw the label on the chart based on the pressed point
  else if(code == LV_EVENT_DRAW_POST_END) {
    int32_t id = lv_chart_get_pressed_point(chart);
    if(id == LV_CHART_POINT_NONE) return;

    LV_LOG_USER("Selected point %d", (int)id);

    lv_chart_series_t * ser = lv_chart_get_series_next(chart, NULL);
    while(ser) {
      lv_point_t p;
      lv_chart_get_point_pos_by_id(chart, ser, id, &p);

      int32_t * y_array = lv_chart_get_y_array(chart, ser);
      int32_t value = y_array[id];
      char buf[16];
      #if TEMP_CELSIUS
        const char degree_symbol[] = "\u00B0C";
      #else
        const char degree_symbol[] = "\u00B0F";
      #endif

      // Preparing the label text for the selected data point
      lv_snprintf(buf, sizeof(buf), LV_SYMBOL_DUMMY " %3.2f %s ", float(bme_last_readings[id]), degree_symbol);

      // Draw the rectangular label that will display the temperature value
      lv_draw_rect_dsc_t draw_rect_dsc;
      lv_draw_rect_dsc_init(&draw_rect_dsc);
      draw_rect_dsc.bg_color = lv_color_black();
      draw_rect_dsc.bg_opa = LV_OPA_60;
      draw_rect_dsc.radius = 2;
      draw_rect_dsc.bg_image_src = buf;
      draw_rect_dsc.bg_image_recolor = lv_color_white();
      // Rectangular label size
      lv_area_t a;
      a.x1 = chart->coords.x1 + p.x - 35;
      a.x2 = chart->coords.x1 + p.x + 35;
      a.y1 = chart->coords.y1 + p.y - 30;
      a.y2 = chart->coords.y1 + p.y - 10;
      lv_layer_t * layer = lv_event_get_layer(e);
      lv_draw_rect(layer, &draw_rect_dsc, &a);
      ser = lv_chart_get_series_next(chart, ser);
    }
  }
  else if(code == LV_EVENT_RELEASED) {
    lv_obj_invalidate(chart);
  }
}

// Draw chart
void lv_draw_chart(void) {
  // Clear screen
  lv_obj_clean(lv_scr_act());

  // Create a a text label aligned on top
  lv_obj_t * label = lv_label_create(lv_screen_active());
  lv_label_set_text(label, "BME280 Temperature Readings");
  lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 10);

  // Create a container to display the chart and scale
  lv_obj_t * container_row = lv_obj_create(lv_screen_active());
  lv_obj_set_size(container_row, SCREEN_HEIGHT-20,  SCREEN_WIDTH-40);
  lv_obj_align(container_row, LV_ALIGN_BOTTOM_MID, 0, -10);
  // Set the container in a flexbox row layout aligned center
  lv_obj_set_flex_flow(container_row, LV_FLEX_FLOW_ROW);
  lv_obj_set_flex_align(container_row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

  // Create a chart
  lv_obj_t * chart = lv_chart_create(container_row);
  lv_obj_set_size(chart, SCREEN_HEIGHT-90, SCREEN_WIDTH-70);
  lv_chart_set_point_count(chart, BME_NUM_READINGS);
  lv_obj_add_event_cb(chart, chart_draw_label_cb, LV_EVENT_ALL, NULL);
  lv_obj_refresh_ext_draw_size(chart);

  // Add a data series
  lv_chart_series_t * chart_series = lv_chart_add_series(chart, lv_palette_main(LV_PALETTE_GREEN), LV_CHART_AXIS_PRIMARY_Y);

  for(int i = 0; i < BME_NUM_READINGS; i++) {
    if(float(bme_last_readings[i]) != -20.0) { // Ignores default array values
      // Set points in the chart and scale them with an *100 multiplier to remove the 2 floating-point numbers
      chart_series->y_points[i] = float(bme_last_readings[i]) * 100;
    }
  }
  // Set the chart range and also scale it with an *100 multiplier to remove the 2 floating-point numbers
  lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, int(scale_min_temp-1)*100, int(scale_max_temp+1)*100);
  lv_chart_refresh(chart); // Required to update the chart with the new values

  // Create a scale (y axis for the temperature) aligned vertically on the right
  lv_obj_t * scale = lv_scale_create(container_row);
  lv_obj_set_size(scale, 15, SCREEN_WIDTH-90);
  lv_scale_set_mode(scale, LV_SCALE_MODE_VERTICAL_RIGHT);
  lv_scale_set_label_show(scale, true);
  // Set the scale ticks count
  lv_scale_set_total_tick_count(scale, int(scale_max_temp+2) - int(scale_min_temp-1));
  if((int(scale_max_temp+2) - int(scale_min_temp-1)) < 10) {
    lv_scale_set_major_tick_every(scale, 1); // set y axis to have 1 tick every 1 degree
  }
  else {
    lv_scale_set_major_tick_every(scale, 10); // set y axis to have 1 tick every 10 degrees
  }
  // Set the scale style and range
  lv_obj_set_style_length(scale, 5, LV_PART_ITEMS);
  lv_obj_set_style_length(scale, 10, LV_PART_INDICATOR);
  lv_scale_set_range(scale, int(scale_min_temp-1), int(scale_max_temp+1));
}

// Get the latest BME readings
void get_bme_readings(void) {
  #if TEMP_CELSIUS
    float bme_temp = bme.readTemperature();
  #else
    float bme_temp = 1.8 * bme.readTemperature() + 32;
  #endif

  // Reset scale range (chart y axis) variables
  scale_min_temp = 120.0;
  scale_max_temp = -20.0;

  // Shift values to the left of the array and inserts the latest reading at the end
  for (int i = 0; i < BME_NUM_READINGS; i++) {
    if(i == (BME_NUM_READINGS-1) && float(bme_temp) < 120.0) {
      bme_last_readings[i] = float(bme_temp);  // Inserts the new reading at the end
    }
    else {
      bme_last_readings[i] = float(bme_last_readings[i + 1]);  // Shift values to the left of the array
    }
    // Get the min/max value in the array to set the scale range (chart y axis)
    if((float(bme_last_readings[i]) < scale_min_temp) && (float(bme_last_readings[i]) != -20.0 )) {
      scale_min_temp = bme_last_readings[i];
    }
    if((float(bme_last_readings[i]) > scale_max_temp) && (float(bme_last_readings[i]) != -20.0 )) {
      scale_max_temp = bme_last_readings[i];
    }
  }
  Serial.print("Min temp: ");
  Serial.println(float(scale_min_temp));
  Serial.print("Max temp: ");
  Serial.println(float(scale_max_temp));
  Serial.print("BME last reading: ");
  Serial.println(float(bme_last_readings[BME_NUM_READINGS-1]));
  lv_draw_chart();
}

void setup() {
  String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
  Serial.begin(115200);
  Serial.println(LVGL_Arduino);

  I2CBME.begin(I2C_SDA, I2C_SCL, 100000);
  bool status;
  // Passing a &Wire2 to set custom I2C ports
  status = bme.begin(0x76, &I2CBME);
  if (!status) {
    Serial.println("Could not find a valid BME280 sensor, check wiring!");
    while (1);
  }

  // Start LVGL
  lv_init();
  // Register print function for debugging
  lv_log_register_print_cb(log_print);

  // Start the SPI for the touchscreen and init the touchscreen
  touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
  touchscreen.begin(touchscreenSPI);
  // Set the Touchscreen rotation in landscape mode
  // Note: in some displays, the touchscreen might be upside down, so you might need to set the rotation to 0: touchscreen.setRotation(0);
  touchscreen.setRotation(2);

  // Create a display object
  lv_display_t * disp;
  // Initialize the TFT display using the TFT_eSPI library
  disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf));
  lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270);

  // Initialize an LVGL input device object (Touchscreen)
  lv_indev_t * indev = lv_indev_create();
  lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
  // Set the callback function to read Touchscreen input
  lv_indev_set_read_cb(indev, touchscreen_read);

  get_bme_readings();
}

void loop() {
  lv_task_handler();  // let the GUI do its work
  lv_tick_inc(5);     // tell LVGL how much time has passed
  delay(5);           // let this time pass

  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {
    // save the last time that chart was updated
    previousMillis = currentMillis;
    get_bme_readings();
  }
}

Просмотреть исходный код

Как работает код?

Давайте рассмотрим, как получить температуру с датчика BME280 и добавить текущее показание на линейный график. Также вы можете перейти к разделу Демонстрация.

Подключение библиотек

Во всех ваших скетчах необходимо подключать библиотеки lvgl.h и TFT_eSPI.h для отображения на экране.

#include <lvgl.h>
#include <TFT_eSPI.h>

Для взаимодействия с датчиком BME280 необходимо подключить следующие библиотеки.

#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Пины I2C для BME280

Установите пины I2C для использования датчика BME280, создайте новый экземпляр шины I2C для этих пинов и создайте объект Adafruit_BME280 с именем bme для обращения к датчику.

#define I2C_SDA 27
#define I2C_SCL 22
TwoWire I2CBME = TwoWire(0);
Adafruit_BME280 bme;

Цельсий или Фаренгейт

Наш код готов для отображения температуры в градусах Цельсия или Фаренгейта. Чтобы выбрать нужную единицу измерения, вы можете установить значение переменной TEMP_CELSIUS. По умолчанию она установлена в 1 для отображения температуры в градусах Цельсия.

#define TEMP_CELSIUS 1

Если вы хотите отображать температуру в градусах Фаренгейта, установите значение 0.

#define TEMP_CELSIUS 0

Количество показаний и переменные масштаба

Данные для отображения на графике необходимо поместить в массив. Мы будем отображать максимум 20 показаний. Они сохраняются в массиве bme_last_readings.

#define BME_NUM_READINGS 20
float bme_last_readings[BME_NUM_READINGS] = {-20.0, -20.0, -20.0, -20.0, -20.0,
-20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0,
-20.0, -20.0, -20.0, -20.0};

Мы также создаём две глобальные переменные scale_min_temp и scale_max_temp, которые будут использоваться для настройки диапазона масштаба.

float scale_min_temp;
float scale_max_temp;

Определение ширины и высоты дисплея

Во всех ваших скетчах, использующих LVGL, необходимо определить ширину и высоту дисплея. Если вы используете рекомендуемый дисплей, размер составляет 240x320.

#define SCREEN_WIDTH 241
#define SCREEN_HEIGHT 320

Создание буфера отрисовки

Необходимо создать буфер для отрисовки на дисплее следующим образом:

#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

Это также следует включать во все примеры LVGL.

Функция отладки

Для отладки с библиотекой LVGL следует использовать функцию log_print(). Она определена ниже. Включайте её во все ваши скетчи перед setup().

// If logging is enabled, it will inform the user about what is happening in the library
void log_print(lv_log_level_t level, const char * buf) {
  LV_UNUSED(level);
  Serial.println(buf);
  Serial.flush();
}

setup()

В setup() включите следующие строки для отладки. Они выведут версию LVGL, которую вы используете. Вы должны использовать версию 9.

String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
Serial.begin(115200);
Serial.println(LVGL_Arduino);

Инициализация датчика BME280

В setup() начните с инициализации датчика BME280.

I2CBME.begin(I2C_SDA, I2C_SCL, 100000);
bool status;
// Passing a &Wire2 to set custom I2C ports
status = bme.begin(0x76, &I2CBME);
if (!status) {
  Serial.println("Could not find a valid BME280 sensor, check wiring!");
  while (1);
}

Инициализация библиотеки LVGL

Инициализируйте библиотеку LVGL, вызвав функцию lv_init() в setup().

// Start LVGL
lv_init();

Регистрация функции отладки

Зарегистрируйте вашу функцию log_print(), объявленную ранее, как функцию, связанную с отладкой LVGL.

// Register print function for debugging
lv_log_register_print_cb(log_print);

Получение показаний BME280

В setup() мы вызываем функцию get_bme_readings(), которая получает новые показания датчика и отображает их на графике.

get_bme_readings();

Эта функция также вызывается каждые 10 секунд (переменная interval) в loop() для добавления новой точки данных на график.

unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
  // save the last time that chart was updated
  previousMillis = currentMillis;
  get_bme_readings();
}

В get_bme_readings() мы начинаем с получения нового показания температуры и сохраняем его в переменной bme_temp (в градусах Цельсия или Фаренгейта).

#if TEMP_CELSIUS
  float bme_temp = bme.readTemperature();
#else
  float bme_temp = 1.8 * bme.readTemperature() + 32;
#endif

Каждый раз при получении новой точки данных мы сбрасываем диапазон масштаба (ось Y).

// Reset scale range (chart y axis) variables
scale_min_temp = 120.0;
scale_max_temp = -20.0;

Мы добавляем новое значение температуры в конец массива и сдвигаем все точки данных влево.

// Shift values to the left of the array and inserts the latest reading at the end
for (int i = 0; i < BME_NUM_READINGS; i++) {
  if(i == (BME_NUM_READINGS-1) && float(bme_temp) < 120.0) {
    bme_last_readings[i] = float(bme_temp); //Inserts the new reading at the end
  }
  else {
    bme_last_readings[i] = float(bme_last_readings[i+1]);
    // Shift values to the left of the array
  }

Массив bme_last_readings теперь содержит данные для отображения на графике.

Мы проходим по массиву и получаем максимальное и минимальное показания температуры, сохраняя их в переменных scale_max_temp и scale_min_temp, чтобы позднее настроить ось Y.

// Get the min/max value in the array to set the scale range (chart y axis)
if((float(bme_last_readings[i]) < scale_min_temp) && (float(bme_last_readings[i]) != -20.0 )) {
  scale_min_temp = bme_last_readings[i];
}
if((float(bme_last_readings[i]) > scale_max_temp) && (float(bme_last_readings[i]) != -20.0 )) {
  scale_max_temp = bme_last_readings[i];
}

Теперь, когда наш массив готов и у нас есть максимальное и минимальное значения температуры, мы можем начать рисовать график. Мы создали функцию lv_draw_chart(), в которую добавили все инструкции для рисования графика.

lv_draw_chart();

Рисование графика

В lv_draw_chart() мы рисуем график и добавляем все остальные виджеты на экран.

Мы добавляем текстовую метку вверху с текстом «BME280 Temperature Readings».

// Create a a text label aligned on top
lv_obj_t * label = lv_label_create(lv_screen_active());
lv_label_set_text(label, "BME280 Temperature Readings");
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 10);

Далее мы создаём контейнер (container_row), в который добавим график и соответствующую шкалу (ось Y), и устанавливаем его выравнивание.

// Create a container to display the chart and scale
lv_obj_t * container_row = lv_obj_create(lv_screen_active());
lv_obj_set_size(container_row, SCREEN_HEIGHT-20, SCREEN_WIDTH-40);
lv_obj_align(container_row, LV_ALIGN_BOTTOM_MID, 0, -10);
// Set the container in a flexbox row layout aligned center
lv_obj_set_flex_flow(container_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(container_row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);

Мы создаём объект графика LVGL с помощью функции lv_chart_create(). В качестве аргумента передаём место, где хотим разместить график. В данном случае мы хотим разместить его внутри container_row.

lv_obj_t * chart = lv_chart_create(container_row);

Устанавливаем размер графика.

lv_obj_set_size(chart, SCREEN_HEIGHT-90, SCREEN_WIDTH-70);

Устанавливаем количество точек, которые вы хотите отображать одновременно на графике.

lv_chart_set_point_count(chart, BME_NUM_READINGS);

Добавляем событие к графику, чтобы он отображал значение нажатой точки – это будет обработано внутри функции обратного вызова chart_draw_label_cb().

lv_obj_add_event_cb(chart, chart_draw_label_cb, LV_EVENT_ALL, NULL);

После создания графика нам нужно создать серию данных для графика с помощью lv_chart_add_series(). Передаём в качестве аргумента график, к которому обращаемся, цвет серии и ось Y.

lv_chart_series_t * chart_series = lv_chart_add_series(chart,
lv_palette_main(LV_PALETTE_GREEN), LV_CHART_AXIS_PRIMARY_Y);

После этого добавляем все наши точки из массива в серию данных графика.

for(int i = 0; i < BME_NUM_READINGS; i++) {
  if(float(bme_last_readings[i]) != -20.0) { // Ignores default array values
    // Set points in the chart and scale them with an
    // *100 multiplier to remove the 2 floating-point numbers
   chart_series->y_points[i] = float(bme_last_readings[i]) * 100;
  }
}

Устанавливаем диапазон графика с учётом значений scale_max_temp и scale_min_temp, полученных ранее.

// Set the chart range and also scale it with an
// *100 multiplier to remove the 2 floating-point numbers
lv_chart_set_range(chart, LV_CHART_AXIS_PRIMARY_Y, int(scale_min_temp-1)*100,
int(scale_max_temp+1)*100);

Вызываем функцию lv_chart_refresh() для обновления всех значений на графике.

lv_chart_refresh(chart); // Required to update the chart with the new values

Создаём вертикальную шкалу для графика с помощью функции lv_scale_create() и размещаем её внутри container_row с вертикальным выравниванием справа.

// Create a scale (y axis for the temperature) aligned vertically on the right
lv_obj_t * scale = lv_scale_create(container_row);
lv_obj_set_size(scale, 15, SCREEN_WIDTH-90);
lv_scale_set_mode(scale, LV_SCALE_MODE_VERTICAL_RIGHT);
lv_scale_set_label_show(scale, true);

Устанавливаем малые деления шкалы.

// Set the scale ticks count
lv_scale_set_total_tick_count(scale,int(scale_max_temp+2)-int(scale_min_temp-1));
if((int(scale_max_temp+2) - int(scale_min_temp-1)) < 10) {
  lv_scale_set_major_tick_every(scale, 1); // set y axis to have 1 tick every 1 degree
}
else {
  lv_scale_set_major_tick_every(scale, 10);//set y axis to have 1 tick every 10 degrees
}

Устанавливаем стиль и диапазон шкалы.

// Set the scale style and range
lv_obj_set_style_length(scale, 5, LV_PART_ITEMS);
lv_obj_set_style_length(scale, 10, LV_PART_INDICATOR);
lv_scale_set_range(scale, int(scale_min_temp-1), int(scale_max_temp+1));

Отображение метки при нажатии на график

Когда вы нажимаете на график, отображается метка со значениями для нажатой точки данных.

lv_obj_add_event_cb(chart, chart_draw_label_cb, LV_EVENT_ALL, NULL);

Всё это обрабатывается в функции chart_draw_label_cb().

// Draw a label on that chart with the value of the pressed point
static void chart_draw_label_cb(lv_event_t * e) {
  lv_event_code_t code = lv_event_get_code(e);
  lv_obj_t * chart = (lv_obj_t*) lv_event_get_target(e);
  if(code == LV_EVENT_VALUE_CHANGED) {
    lv_obj_invalidate(chart);
  }
  if(code == LV_EVENT_REFR_EXT_DRAW_SIZE) {
    int32_t * s = (int32_t*)lv_event_get_param(e);
    *s = LV_MAX(*s, 20);
  }
  // Draw the label on the chart based on the pressed point
  else if(code == LV_EVENT_DRAW_POST_END) {
    int32_t id = lv_chart_get_pressed_point(chart);
    if(id == LV_CHART_POINT_NONE) return;
      LV_LOG_USER("Selected point %d", (int)id);
      lv_chart_series_t * ser = lv_chart_get_series_next(chart, NULL);
      while(ser) {
        lv_point_t p;
        lv_chart_get_point_pos_by_id(chart, ser, id, &p);
        int32_t * y_array = lv_chart_get_y_array(chart, ser);
        int32_t value = y_array[id];
        char buf[16];
        #if TEMP_CELSIUS
          const char degree_symbol[] = "\u00B0C";
        #else
          const char degree_symbol[] = "\u00B0F";
        #endif
        // Preparing the label text for the selected data point
        lv_snprintf(buf, sizeof(buf), LV_SYMBOL_DUMMY " %3.2f %s ", float(bme_last_readings[id]), degree_symbol);
        // Draw the rectangular label that will display the temperature value
        lv_draw_rect_dsc_t draw_rect_dsc;
        lv_draw_rect_dsc_init(&draw_rect_dsc);
        draw_rect_dsc.bg_color = lv_color_black();
        draw_rect_dsc.bg_opa = LV_OPA_60;
        draw_rect_dsc.radius = 2;
        draw_rect_dsc.bg_image_src = buf;
        draw_rect_dsc.bg_image_recolor = lv_color_white();
        // Rectangular label size
        lv_area_t a;
        a.x1 = chart->coords.x1 + p.x - 35;
        a.x2 = chart->coords.x1 + p.x + 35;
        a.y1 = chart->coords.y1 + p.y - 30;
        a.y2 = chart->coords.y1 + p.y - 10;
        lv_layer_t * layer = lv_event_get_layer(e);
        lv_draw_rect(layer, &draw_rect_dsc, &a);
        ser = lv_chart_get_series_next(chart, ser);
      }
    }
    else if(code == LV_EVENT_RELEASED) {
      lv_obj_invalidate(chart);
    }
}

Демонстрация

Загрузите код на вашу плату. Перейдите в Tools > Board и выберите ESP32 > ESP32 Dev Module. Затем выберите правильный COM-порт в Tools > Port. Наконец, нажмите кнопку загрузки.

Кнопка загрузки Arduino IDE 2

После загрузки кода на дисплее появится график с одной точкой. Подождите 10 секунд для получения второй точки, ещё 10 секунд для третьей точки и так далее.

ESP32 CYD LVGL - отображение температуры на линейном графике BME280

Подождите, пока график заполнится 20 точками данных. Новое значение добавляется каждые 10 секунд – график обновляется автоматически.

ESP32 CYD LVGL - демонстрация линейного графика температуры BME280

Вы можете нажать на конкретную точку данных, чтобы проверить её точное значение.

Монитор последовательного порта отображает последнее показание температуры, а также минимальное и максимальное значения из массива.

ESP32 CYD BME280 - демонстрация монитора последовательного порта с температурой

Заключение

В этом руководстве вы научились отображать данные датчика на линейном графике с помощью экрана Cheap Yellow Display, используя библиотеку LVGL.

Мы надеемся, что это руководство было полезным. Мы готовим ещё больше руководств по этой плате, так что следите за обновлениями. Если вы хотите узнать больше о создании графических пользовательских интерфейсов с помощью библиотеки LVGL с ESP32, ознакомьтесь с нашей последней электронной книгой:

Другие руководства, которые могут вас заинтересовать:

Для более глубокого изучения ESP32 обязательно ознакомьтесь с нашими ресурсами:


Источник: ESP32 CYD with LVGL: Display Temperature on Line Chart (BME280)