В современном быстро меняющемся мире многие из нас проводят долгие часы в помещении — работая из дома, учась или просто управляя своими повседневными делами. Естественные сигналы, такие как изменение неба, температура на улице или ощущение влажности, часто теряются за четырьмя стенами и закрытыми окнами. Это может мешать оставаться в курсе того, что происходит снаружи, особенно когда наше внимание привязано к цифровым устройствам. Наличие специального, всегда видимого дисплея погоды на вашем столе добавляет тонкую, но значимую связь с внешним миром. Это небольшой штрих, который может помочь спланировать свой день, узнать, когда следует взять куртку перед выходом на улицу, или просто насладиться мягким напоминанием о том, что время и погода продолжают меняться за пределами экрана.
Этот проект предлагает практичный способ построить чистую и минималистичную метеостанцию своими руками с использованием микроконтроллера ESP32 и дисплея E-Ink. Использование E-Ink гарантирует, что дисплей остается читаемым даже без постоянного питания, что делает его идеальным для настольного использования с питанием от батареи. После подключения к Wi-Fi станция получает данные о погоде в режиме реального времени, такие как температура, влажность и текущие условия, с помощью API OpenWeatherMap. Она также контролирует температуру и влажность в помещении с помощью встроенного датчика и дополняет данные, отображаемые на экране. Устройство автоматически обновляется через фиксированные интервалы и переходит в режим глубокого сна для экономии энергии. После сборки оно становится тихим и элегантным компаньоном на вашем столе, держа вас в курсе событий, без отвлекающих факторов смартфона или экрана компьютера.
Также на нашем сайте вы можете посмотреть и другие проекты метеостанций:
- метеостанция IoT на NodeMCU ESP8266 и OpenWeatherMap (максимально похожий проект на рассматриваемую в данной статье метеостанцию);
- метеостанция на ESP32/ESP8266 и BME680 с мониторингом качества воздуха по MQTT;
- метеостанция IoT Live на NodeMCU ESP8266;
- беспроводная метеостанция на Arduino;
- метеостанция на Raspberry Pi: измерение температуры, влажности и давления через сеть Интернет.
Необходимые компоненты
- ESP32-S3-WROOM-1-N16R8 (купить на AliExpress).
- 4,2-дюймовый дисплей EPD (купить на AliExpress).
- Датчик температуры и влажности HPP845E031R4.
- ADP124ACPZ 3,3 В LDO (купить на AliExpress).
- Микросхема зарядного устройства для аккумуляторов MAX1898 (купить на AliExpress).
- МОП-транзисторы, диоды и светодиоды.
- Другие пассивные компоненты.
- Переключатели и разъемы.
- Индивидуальная печатная плата.
- Детали, напечатанные на 3D-принтере.
- Другие инструменты и расходные материалы.
Реклама: ООО «АЛИБАБА.КОМ (РУ)» ИНН: 7703380158
Функции настольной метеостанции
- Многоцветный дисплей E-Ink: 4,2-дюймовый многоцветный дисплей E-Ink, сохраняющий контент видимым без постоянного потребления энергии.
- ESP32-S3 SoC: микроконтроллер ESP32-S3 управляет связью по Wi-Fi, сбором данных и обновлением дисплея.
- Поддержка API погоды: использует API OpenWeatherMap для получения данных о погоде, таких как температура наружного воздуха, состояние и местоположение.
- Мониторинг в помещении: специальный маломощный датчик для мониторинга температуры и влажности в помещении.
- Низкое энергопотребление: одной зарядки хватает на несколько дней.
- Автоматический спящий режим: переходит в режим глубокого сна после каждого обновления для экономии заряда батареи.
- Индивидуальная разработка печатной платы: все компоненты аккуратно размещены на индивидуальной печатной плате, разработанной с помощью KiCad.
Принципиальная схема настольной метеостанции
Полная принципиальная схема настольной метеостанции представлена ниже. Её также можно скачать в формате PDF из репозитория GitHub, ссылка на который приведена в конце статьи.
Мы разделили схемы на несколько разделов в зависимости от функциональности. Это значительно упростит понимание каждого раздела. Давайте рассмотрим каждый раздел отдельно.
В первой секции у нас есть вход USB вместе со схемой управления питанием. Вход USB типа C используется для зарядки внутренней батареи, а также для программирования и отладки встроенной ESP32-S3 SoC. Два подтягивающих резистора добавлены к входу CCx, так что плата будет работать без проблем с хостом типа C. Линии данных USB напрямую подключены к собственному выводу USB ESP32-S3. Далее у нас есть схема управления трактом питания, построенная на P-канальном MOSFET AO3401 и диоде Шоттки SS14. Эта секция гарантирует, что остальная часть платы может без проблем питаться как от батареи, так и от входа USB. Далее следует сверхмалошумящий стабилизатор напряжения с малым падением напряжения ADP124ACPZ от Analog Devices. Этот LDO отвечает за создание линии 3,3 В, необходимой остальной части схемы.
В следующем разделе мы видим, что выключатель питания подключен к выводу разрешения LDO-стабилизатора APD124. Для зарядки аккумулятора мы использовали микросхему зарядки одноэлементных литиевых аккумуляторов MAX1898 от Analog Devices. Она может заряжать встроенный аккумулятор максимальным током до 500 мА. В зависимости от используемого аккумулятора необходимо изменить значение резистора программирования тока заряда для установки оптимального тока. Как видите, для аккумулятора мы предусмотрели два варианта: пользователь может использовать LIR2450, который устанавливается во встроенный держатель, или подключить LiPo-аккумулятор через разъём JST. Для измерения напряжения аккумулятора используется делитель напряжения, который отображается на дисплее E-Ink/EPD.
Как уже упоминалось, мозг настольной метеостанции — это микросхема на кристалле ESP32-S3. Мы использовали модуль ESP32-S3-Wroom-1, чтобы упростить процесс сборки и не беспокоиться о радиочастотной составляющей, которая была бы проблематичной при использовании простого чипа ESP32-S3. Микросхема супервизора напряжения ADM803 используется для контроля питания и обеспечения корректного запуска ESP32-S3. Разъёмы USC подключены к выводам GPIO19 и GPIO20, которые являются собственными выводами USB ESP32-S3. На схеме есть две кнопки: одна для выбора загрузки, а другая для ручного сброса. Эти кнопки будут полезны при перепрограммировании ESP32-S3, если автоматическая прошивка через USB не работает.
Раздел дисплея E-Ink довольно прост и соответствует инструкциям производителя. Мы использовали дисплейный модуль E2417JS0D6 от Pervasive Displays — это 4,2-дюймовый трёхцветный дисплей E-Ink с разрешением 400 x 300 пикселей. Схема драйвера взята непосредственно из технического описания модуля дисплея, а некоторые компоненты заменены на аналогичные из-за отсутствия рекомендуемых компонентов.
И наконец, у нас есть HPP845E — высокоточный датчик относительной влажности и температуры от TE Connectivity. HPP845E относится к серии HTU21D и совместим с другими датчиками HTU21D. Он может измерять температуру в широком диапазоне от -40 до 125 °C и относительную влажность от 0 до 100%. Он подключен к ESP32-S3 через интерфейс I2C. Этот датчик отвечает за мониторинг окружающей среды в помещении в нашей настольной метеостанции.
Печатная плата настольной метеостанции
Для этого проекта мы решили изготовить индивидуальную многоцветную печатную плату. Это обеспечит максимальную компактность конечного продукта, а также простоту сборки и использования. Печатная плата разработана в KiCad. Все файлы проекта доступны для скачивания из репозитория GitHub, ссылка на который приведена в конце каждой статьи. Размер каждой грани печатной платы составляет приблизительно 105 x 90 мм.
Вот верхний и нижний слои печатной платы.
После того, как печатная плата готова и полностью проверена, мы отправляем её на производство. Вот готовые печатные платы от производителя.
Сборка печатной платы настольной метеостанции
Первым шагом при сборке печатных плат стала сортировка всех необходимых компонентов в соответствии с спецификацией. После этого мы разместили их на плате и припаяли один за другим. Для упрощения процесса можно использовать трафарет для SMD-монтажа, чтобы нанести паяльную пасту, а затем разместить на нём компоненты перед оплавлением платы в паяльной станции SMD или печи для оплавления. Ниже представлены изображения полностью собранной печатной платы метеостанции.
А вот полностью собранная печатная плата вместе с дисплейным модулем.
Детали метеостанции, напечатанные на 3D-принтере
Мы разработали корпус для метеостанции, напечатанный на 3D-принтере, чтобы он стал стильным гаджетом для вашего рабочего стола. Файлы всех деталей, напечатанных на 3D-принтере, можно скачать по ссылке на GitHub, приведённой в конце статьи, вместе с эскизом и растровым изображением для Arduino. Узнайте больше о 3D-печати и о том, как начать работать с ней, перейдя по ссылке. Вы можете скачать 3D-файлы из репозитория проекта на GitHub.
А вот обратная сторона, на которой видна подставка и монтажные отверстия.
А вот и распечатанные детали. Как видите, мы также использовали резьбовую вставку для винтов. Используемые винты — это винты М2,5 длиной 6 мм.
Сборка настольной метеостанции
Когда все детали готовы, можно приступать к сборке настольной метеостанции. Вот всё, что нужно сделать перед окончательной сборкой.
Чтобы собрать настольную метеостанцию, сначала вставьте дисплейный модуль в распечатанную на 3D-принтере рамку, а затем полностью собранную печатную плату. Аккуратно вставьте гибкий кабель дисплея в разъём дисплея и закрепите его. Затем подключите аккумулятор.
Затем закройте заднюю панель и закрепите её на передней раме винтами M2.5 длиной 6 мм. После этого можно прикрепить подставку к задней панели винтами M2.5. Вот конечный результат.
Код Arduino для настольной метеостанции
Теперь, когда мы разобрались, как устроена схема, давайте взглянем на код, который всё это работает. Несмотря на то, что в коде есть несколько файлов, мы обсудим только два самых важных: файл config.h, содержащий пользовательские конфигурации, и основной скетч Arduino, содержащий основные функции и переменные. Прошивка этого проекта основана на превосходной работе Дэвида Бёрда. Код был адаптирован из его оригинальной реализации, которую вы можете найти здесь: ESP32 Weather Paper Display . Прежде чем погрузиться в код, убедитесь, что ваша среда разработки Arduino IDE правильно настроена для проекта. Установите менеджер плат ESP32, если он ещё не установлен, и выберите ESP32-S3 в качестве платы. Вам также потребуется установить несколько библиотек, включая GxEPD2 для дисплея E-Ink, U8g2_for_Adafruit_GFX для дополнительной графической поддержки и ArduinoJson для обработки данных JSON. После установки платы и библиотек можно приступать к внесению изменений или компиляции кода.
Для начала рассмотрим файл config.h. Как следует из названия, этот файл содержит пользовательские настройки Wi-Fi и API карт OpenWeather.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Change to your WiFi credentials const char* ssid = "circuitdigest"; const char* password = "12345678"; // Use your own API key by signing up for a free developer account at https://openweathermap.org/ String apikey = "ad3exxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // openweathermap API const char server[] = "api.openweathermap.org"; //Set your location according to OWM locations String LAT = "11.0110382"; //Latitude String LON = "77.0130247"; //Longitude String City = "Coimbatore"; String Country = "IN"; String Language = "EN"; // Language String Hemisphere = "north"; // or "south" String Units = "M"; // Use 'M' for Metric or I for Imperial const char* Timezone = "IST-5:30"; //Time Zone const char* ntpServer = "pool.ntp.org"; //ntp server int gmtOffset_sec = 19800; // +5.30 int daylightOffset_sec = 0; |
В этом разделе кода вам необходимо ввести свои учётные данные Wi-Fi, заменив значения ssid и password на имя и пароль вашей сети Wi-Fi, чтобы ESP32 мог подключиться к Интернету. Проект получает данные о погоде в режиме реального времени из OpenWeatherMap, поэтому вам необходимо зарегистрироваться на сайте openweathermap.org, чтобы получить бесплатный ключ API, и заменить значение apikey на своё собственное. Домен сервера api.openweathermap.org предназначен для выполнения HTTP-запросов на получение данных о погоде. Кроме того, укажите своё точное географическое местоположение, обновив переменные LAT и LON своими координатами широты и долготы. Для наглядности отображения погоды укажите город и страну с помощью переменных City и Country, выберите предпочитаемый язык описания погоды в поле Language и укажите полушарие (северное или южное).
Переменная Units позволяет выбрать метрические (M) или имперские (I) единицы измерения для температуры и скорости ветра. Здесь также настраиваются параметры времени: установите часовой пояс с помощью строки Timezone (например, «IST-5:30» для индийского стандартного времени), укажите NTP-сервер (ntpServer) для синхронизации часов реального времени с интернет-временем и задайте значения gmtOffset_sec и daylightOffset_sec для корректировки времени в соответствии с вашим местным смещением по Гринвичу и настройками перехода на летнее время. Все эти параметры гарантируют успешное подключение ESP32, получение точных данных о погоде для вашего местоположения и их отображение в выбранном вами формате и на выбранном языке (в настоящее время код поддерживает только английский).
Поскольку вы знакомы с пользовательскими конфигурациями, давайте рассмотрим основной код.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
#include "config.h" // See 'config.h' file and enter your OWM API key and set the Wifi SSID and PASSWORD #include <ArduinoJson.h> // https://github.com/bblanchon/ArduinoJson #include <WiFi.h> // Built-in #include "time.h" // Built-in #include <SPI.h> // Built-in #define ENABLE_GxEPD2_display 0 #include <GxEPD2_BW.h> // GxEPD2 v1.6.3 #include <GxEPD2_3C.h> #include <U8g2_for_Adafruit_GFX.h> #include "epaper_fonts.h" #include "forecast_record.h" #include "lang.h" #include <Wire.h> #define HTU21D_ADDRESS 0x40 // I2C address of HTU21D #define TEMP_MEASURE_NO_HOLD 0xF3 #define HUMID_MEASURE_NO_HOLD 0xF5 #define SOFT_RESET 0xFE #define USER_REGISTER_READ 0xE7 #define USER_REGISTER_WRITE 0xE6 #define SCREEN_WIDTH 400.0 // Set for landscape mode, don't remove the decimal place! #define SCREEN_HEIGHT 300.0 enum alignment {LEFT, RIGHT, CENTER}; // Connections for the EPD Display static const uint8_t EPD_BUSY = 13; // to EPD BUSY static const uint8_t EPD_CS = 10; // to EPD CS static const uint8_t EPD_RST = 14; // to EPD RST static const uint8_t EPD_DC = 15; // to EPD DC static const uint8_t EPD_SCK = 12; // to EPD CLK static const uint8_t EPD_MISO = -1; // Master-In Slave-Out not used, as no data from display static const uint8_t EPD_MOSI = 11; // to EPD DIN GxEPD2_3C<GxEPD2_420c, GxEPD2_420c::HEIGHT> display(GxEPD2_420c(/*CS=5*/ EPD_CS, /*DC=*/ EPD_DC, /*RST=*/ EPD_RST, /*BUSY=*/ EPD_BUSY)); // U8G2_FOR_ADAFRUIT_GFX u8g2Fonts; // Select u8g2 font from here: https://github.com/olikraus/u8g2/wiki/fntlistall // Using fonts: // u8g2_font_helvB08_tf // u8g2_font_helvB10_tf // u8g2_font_helvB12_tf // u8g2_font_helvB14_tf // u8g2_font_helvB18_tf // u8g2_font_helvB24_tf boolean LargeIcon = true, SmallIcon = false; #define Large 11 // For icon drawing, needs to be odd number for best effect #define Small 5 // For icon drawing, needs to be odd number for best effect String time_str, date_str; // strings to hold time and received weather data int wifi_signal, CurrentHour = 0, CurrentMin = 0, CurrentSec = 0; long StartTime = 0; //################ PROGRAM VARIABLES and OBJECTS ################ #define max_readings 24 Forecast_record_type WxConditions[1]; Forecast_record_type WxForecast[max_readings]; #include <common.h> #define autoscale_on true #define autoscale_off false #define barchart_on true #define barchart_off false float pressure_readings[max_readings] = {0}; float temperature_readings[max_readings] = {0}; float humidity_readings[max_readings] = {0}; float rain_readings[max_readings] = {0}; float snow_readings[max_readings] = {0}; long SleepDuration = 15; // Sleep time in minutes, aligned to the nearest minute boundary, so if 30 will always update at 00 or 30 past the hour |
Код начинается с подключения всех необходимых библиотек, файлов конфигурации, настроек отображения, определений датчиков и глобальных переменных, необходимых для запуска проекта метеостанции. Он начинается с включения файла config.h, в котором пользователь должен ввести свой ключ API OpenWeatherMap, а также учётные данные Wi-Fi и другие параметры конфигурации. Для управления подключением к Интернету, синхронизации времени и обмена данными включены такие основные библиотеки, как ArduinoJson, WiFi, time и SPI. Библиотека GxEPD2 используется для управления дисплеем на электронной бумаге, а библиотека U8g2_for_Adafruit_GFX обеспечивает высококачественную визуализацию шрифтов. Для управления шрифтами, структурами данных о погоде и языковыми настройками включены несколько пользовательских заголовочных файлов, таких как epaper_fonts.h, forecast_record.h и lang.h.
Датчик температуры и влажности HPP845E031R4 (серия HTU21D) инициализируется по интерфейсу I2C стандартным адресом и командными константами. Размеры дисплея определены для альбомной ориентации, а контакты подключения сопоставлены для взаимодействия ESP32 с дисплеем на основе электронной бумаги. Инициализируется трёхцветный объект отображения на основе электронной бумаги, а также настраиваются объекты шрифтов для отображения текста. Для гибкости проектирования дисплея перечислено несколько вариантов шрифтов. Объявлены флаги и значения для управления размерами значков дисплея, временем удержания и строками данных о погоде, а также для управления временными переменными. Определены массивы для хранения показаний давления, температуры, влажности, дождя и снега, которые полезны для построения трендов и прогнозов. Массивы Forecast_record_type хранят текущие и прогнозируемые данные о погоде. Также определена переменная для установки продолжительности глубокого сна, что позволяет устройству просыпаться через регулярные интервалы (например, каждые 15 минут) для получения и отображения новых данных о погоде. Если вы хотите настроить частоту обновления дисплея, измените эту переменную соответствующим образом.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
void setup() { StartTime = millis(); pinMode(16, OUTPUT); digitalWrite(16, LOW); pinMode(42, OUTPUT); digitalWrite(42, HIGH); delay(100); Serial.begin(115200); Wire.begin(); // Initialize I2C softReset(); // Reset the sensor before starting disableHeater(); // Ensure the heater is turned off if (StartWiFi() == WL_CONNECTED && SetupTime() == true) { InitialiseDisplay(); // Give screen time to initialise by getting weather data! byte Attempts = 1; bool RxWeather = false, RxForecast = false; WiFiClient client; // wifi client object while ((RxWeather == false || RxForecast == false) && Attempts <= 2) { // Try up-to 2 time for Weather and Forecast data if (RxWeather == false) RxWeather = obtain_wx_data(client, "weather"); if (RxForecast == false) RxForecast = obtain_wx_data(client, "forecast"); Attempts++; } if (RxWeather && RxForecast) { // Only if received both Weather or Forecast proceed StopWiFi(); // Reduces power consumption DisplayWeather(); display.display(false); // Full screen update mode } } BeginSleep(); } void loop() { // this will never run! yield(); } |
В функции setup() ESP32 инициализирует основные компоненты и выполняет начальную выборку данных о погоде. GPIO 16 настроен как выход для управления питанием E-Ink дисплея и устанавливается в состояние LOW для включения питания. GPIO 42 подключен к светодиоду, который включается установкой в состояние HIGH. После этого запускается последовательная связь для отладки, и инициализируется шина I2C для датчика температуры и влажности. На датчике выполняется мягкий сброс, а его внутренний нагреватель отключается для обеспечения точности показаний. Затем код пытается подключиться к Wi-Fi и синхронизировать системное время. Если обе операции успешны, E-Ink дисплей инициализируется, и данные о погоде запрашиваются с помощью объекта клиента Wi-Fi. Он пытается получить до двух раз как текущие данные о погоде, так и прогнозируемые данные. В случае успеха Wi-Fi отключается для экономии энергии, отображаются данные о погоде и запускается полноэкранное обновление на дисплее. Наконец, система переходит в режим глубокого сна с помощью функции BeginSleep для экономии энергии между обновлениями. Функция цикла (loop) не содержит много кода, поскольку все операции выполняются внутри функции setup().
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void BeginSleep() { display.powerOff(); digitalWrite(16, HIGH); digitalWrite(42, LOW); long SleepTimer = SleepDuration * 60; // theoretical sleep duration long offset = (CurrentMin % SleepDuration) * 60 + CurrentSec; // number of seconds elapsed after last theoretical wake-up time point if (offset > SleepDuration/2 * 60){ // waking up too early will cause <offset> too large offset -= SleepDuration * 60; // then we should make it negative, so as to extend this coming sleep duration } esp_sleep_enable_timer_wakeup((SleepTimer - offset) * 1000000LL); // do compensation to cover ESP32 RTC timer source inaccuracies Serial.println("Entering " + String(SleepTimer) + "-secs of sleep time"); Serial.println("Awake for : " + String((millis() - StartTime) / 1000.0, 3) + "-secs"); Serial.println("Starting deep-sleep period..."); esp_deep_sleep_start(); // Sleep for e.g. 30 minutes } |
Функция BeginSleep отвечает за перевод ESP32 в режим глубокого сна для экономии энергии. Она отключает дисплей E-Ink, отключает GPIO, подключенные к дисплею и светодиоду, рассчитывает точную продолжительность сна в соответствии с предполагаемым интервалом пробуждения и инициирует глубокий сон с помощью функции esp_deep_sleep_start. Это помогает периодически обновлять прогноз погоды, поддерживая низкое энергопотребление.
|
1 2 3 4 5 6 7 8 9 |
void DisplayWeather() { // 4.2" e-paper display is 400x300 resolution DrawHeadingSection(); // Top line of the display DrawMainWeatherSection(172, 70); // Centre section of display for Location, temperature, Weather report, current Wx Symbol and wind direction DrawForecastSection(233, 15); // 3hr forecast boxes DisplayPrecipitationSection(233, 82); // Precipitation sectio if (WxConditions[0].Visibility > 0) Visibility(335, 100, String(WxConditions[0].Visibility) + "M"); if (WxConditions[0].Cloudcover > 0) CloudCover(350, 125, WxConditions[0].Cloudcover); DrawAstronomySection(233, 74); // Astronomy section Sun rise/set, Moon phase and Moon icon } |
Функция DisplayWeather управляет всей компоновкой E-Ink-дисплея, вызывая различные функции отрисовки. Она включает в себя заголовок, основной раздел погоды, прогноз погоды, осадки, видимость, облачность и астрономические данные, каждый из которых стратегически расположен на экране для наглядности.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void DrawHeadingSection() { u8g2Fonts.setFont(u8g2_font_helvB08_tf); display.setTextColor(GxEPD_RED); drawString(SCREEN_WIDTH / 2, 2, City, CENTER); display.setTextColor(GxEPD_BLACK); drawString(4, 2, date_str, LEFT); drawString(120, 2, time_str, LEFT); DrawBattery(SCREEN_WIDTH-70, 14); display.drawLine(0, 14, SCREEN_WIDTH, 14, GxEPD_BLACK); } void DrawMainWeatherSection(int x, int y) { Display_HPP845E_Data(x - 120, y + 58); DisplayDisplayWindSection(x - 115, y - 3, WxConditions[0].Winddir, WxConditions[0].Windspeed, 40); DisplayWXicon(x + 5, y - 5, WxConditions[0].Icon, LargeIcon); u8g2Fonts.setFont(u8g2_font_helvB10_tf); u8g2Fonts.setFont(u8g2_font_helvB12_tf); String Wx_Description = WxConditions[0].Forecast0; if (WxConditions[0].Forecast1 != "") Wx_Description += " & " + WxConditions[0].Forecast1; if (WxConditions[0].Forecast2 != "" && WxConditions[0].Forecast1 != WxConditions[0].Forecast2) Wx_Description += " & " + WxConditions[0].Forecast2; drawStringMaxWidth(x - 170, y + 83, 28, TitleCase(Wx_Description), LEFT); DrawMainWx(x, y + 60); display.drawRect(0, y + 68, 232, 48, GxEPD_BLACK); } |
Раздел DrawHeadingSection рисует верхнюю часть экрана, включая название города, текущую дату, время, уровень заряда батареи и горизонтальную разделительную линию. Раздел DrawMainWeatherSection создаёт центральную область, отображающую значок погоды, температуру, влажность и краткое описание прогноза. Он также рисует рамку вокруг этой информации.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
void DrawForecastSection(int x, int y) { u8g2Fonts.setFont(u8g2_font_helvB10_tf); DrawForecastWeather(x, y, 0); DrawForecastWeather(x + 56, y, 1); DrawForecastWeather(x + 112, y, 2); // (x,y,width,height,MinValue, MaxValue, Title, Data Array, AutoScale, ChartMode) for (int r = 0; r < max_readings; r++) { if (Units == "I") { pressure_readings[r] = WxForecast[r].Pressure * 0.02953; rain_readings[r] = WxForecast[r].Rainfall * 0.0393701; } else { pressure_readings[r] = WxForecast[r].Pressure; rain_readings[r] = WxForecast[r].Rainfall; } temperature_readings[r] = WxForecast[r].Temperature; } display.drawLine(0, y + 172, SCREEN_WIDTH, y + 172, GxEPD_BLACK); u8g2Fonts.setFont(u8g2_font_helvB12_tf); drawString(SCREEN_WIDTH / 2, y + 180, TXT_FORECAST_VALUES, CENTER); u8g2Fonts.setFont(u8g2_font_helvB10_tf); DrawGraph(SCREEN_WIDTH / 400 * 30, SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 900, 1050, Units == "M" ? TXT_PRESSURE_HPA : TXT_PRESSURE_IN, pressure_readings, max_readings, autoscale_on, barchart_off); DrawGraph(SCREEN_WIDTH / 400 * 158, SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 10, 30, Units == "M" ? TXT_TEMPERATURE_C : TXT_TEMPERATURE_F, temperature_readings, max_readings, autoscale_on, barchart_off); DrawGraph(SCREEN_WIDTH / 400 * 288, SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 0, 30, Units == "M" ? TXT_RAINFALL_MM : TXT_RAINFALL_IN, rain_readings, max_readings, autoscale_on, barchart_on); } void DrawForecastWeather(int x, int y, int index) { u8g2Fonts.setFont(u8g2_font_helvB08_tf); display.drawRect(x, y, 55, 65, GxEPD_BLACK); display.drawLine(x + 1, y + 13, x + 54, y + 13, GxEPD_BLACK); DisplayWXicon(x + 28, y + 35, WxForecast[index].Icon, SmallIcon); drawString(x + 31, y + 3, String(ConvertUnixTime(WxForecast[index].Dt + WxConditions[0].Timezone).substring(0,5)), CENTER); drawString(x + 41, y + 52, String(WxForecast[index].High, 0) + "° / " + String(WxForecast[index].Low, 0) + "°", CENTER); } |
Раздел DrawForecastSection отображает данные краткосрочного прогноза погоды в трёх отдельных блоках и соответствующих графиках давления, температуры и количества осадков.
Функция DrawForecastWeather используется для создания каждого из этих блоков прогноза с указанием времени, значка и диапазона температур.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
void DrawMainWx(int x, int y) { u8g2Fonts.setFont(u8g2_font_helvB14_tf); drawString(x - 25, y - 22, String(WxConditions[0].Temperature, 1) + "°" + (Units == "M" ? "C" : "F"), CENTER); // Show current Temperature u8g2Fonts.setFont(u8g2_font_helvB12_tf); drawString(x - 15, y - 3, String(WxConditions[0].High, 0) + "° | " + String(WxConditions[0].Low, 0) + "°", CENTER); // Show forecast high and Low drawString(x + 30, y - 22, String(WxConditions[0].Humidity, 0) + "%", CENTER); u8g2Fonts.setFont(u8g2_font_helvB10_tf); drawString(x + 32, y - 3, "RH", CENTER); } void DisplayDisplayWindSection(int x, int y, float angle, float windspeed, int Cradius) { arrow(x, y, Cradius - 7, angle, 12, 18); // Show wind direction on outer circle of width and length u8g2Fonts.setFont(u8g2_font_helvB08_tf); int dxo, dyo, dxi, dyi; display.drawLine(0, 15, 0, y + Cradius + 30, GxEPD_RED); display.drawCircle(x, y, Cradius, GxEPD_RED); // Draw compass circle display.drawCircle(x, y, Cradius + 1, GxEPD_RED); // Draw compass circle display.drawCircle(x, y, Cradius * 0.7, GxEPD_RED); // Draw compass inner circle for (float a = 0; a < 360; a = a + 22.5) { dxo = Cradius * cos((a - 90) * PI / 180); dyo = Cradius * sin((a - 90) * PI / 180); if (a == 45) drawString(dxo + x + 10, dyo + y - 10, TXT_NE, CENTER); if (a == 135) drawString(dxo + x + 7, dyo + y + 5, TXT_SE, CENTER); if (a == 225) drawString(dxo + x - 15, dyo + y, TXT_SW, CENTER); if (a == 315) drawString(dxo + x - 15, dyo + y - 10, TXT_NW, CENTER); dxi = dxo * 0.9; dyi = dyo * 0.9; display.drawLine(dxo + x, dyo + y, dxi + x, dyi + y, GxEPD_RED); dxo = dxo * 0.7; dyo = dyo * 0.7; dxi = dxo * 0.9; dyi = dyo * 0.9; display.drawLine(dxo + x, dyo + y, dxi + x, dyi + y, GxEPD_RED); } drawString(x, y - Cradius - 10, TXT_N, CENTER); drawString(x, y + Cradius + 5, TXT_S, CENTER); drawString(x - Cradius - 10, y - 3, TXT_W, CENTER); drawString(x + Cradius + 8, y - 3, TXT_E, CENTER); drawString(x - 2, y - 20, WindDegToDirection(angle), CENTER); drawString(x + 3, y + 12, String(angle, 0) + "°", CENTER); drawString(x + 3, y - 3, String(windspeed, 1) + (Units == "M" ? "m/s" : "mph"), CENTER); } String WindDegToDirection(float winddirection) { int dir = int((winddirection / 22.5) + 0.5); String Ord_direction[16] = {TXT_N, TXT_NNE, TXT_NE, TXT_ENE, TXT_E, TXT_ESE, TXT_SE, TXT_SSE, TXT_S, TXT_SSW, TXT_SW, TXT_WSW, TXT_W, TXT_WNW, TXT_NW, TXT_NNW}; return Ord_direction[(dir % 16)]; } |
Функция DrawMainWx выводит текущую температуру, влажность и максимальные/минимальные значения. DisplayDisplayWindSection отображает индикатор направления ветра, похожий на компас, а также скорость ветра и стороны света. Функция WindDegToDirection преобразует угол ветра в текстовое значение направления ветра.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void Display_HPP845E_Data(int x, int y) { display.fillRect(x-45, y-10 , 24, 18, GxEPD_RED); display.fillTriangle(x - 47, y - 10, x - 33, y - 20, x - 20, y - 10, GxEPD_RED); //display.fillRect(x - 30, y + 2, 6, 6, GxEPD_WHITE); display.drawRect(x - 39, y , 5, 5, GxEPD_WHITE); display.drawRect(x - 33, y , 5, 5, GxEPD_WHITE); display.drawRect(x - 39, y - 6, 5, 5, GxEPD_WHITE); display.drawRect(x - 33, y - 6, 5, 5, GxEPD_WHITE); float temperature = readTemperature(); float humidity = readHumidity(); drawString(x+30, y-2, String(temperature)+"° | "+String(humidity)+"%", CENTER); } void DisplayPrecipitationSection(int x, int y) { display.drawRect(x, y - 1, 167, 56, GxEPD_BLACK); // precipitation outline u8g2Fonts.setFont(u8g2_font_helvB10_tf); if (WxForecast[1].Rainfall > 0.005) { // Ignore small amounts drawString(x + 5, y + 15, String(WxForecast[1].Rainfall, 2) + (Units == "M" ? "mm" : "in"), LEFT); // Only display rainfall total today if > 0 addraindrop(x + 65 - (Units == "I" ? 10 : 0), y + 16, 7); } if (WxForecast[1].Snowfall > 0.005) // Ignore small amounts drawString(x + 5, y + 35, String(WxForecast[1].Snowfall, 2) + (Units == "M" ? "mm" : "in") + " * *", LEFT); // Only display snowfall total today if > 0 } |
Функция Display_HPP845E_Data используется для отображения температуры и влажности с датчика HTU21D на небольшой панели дисплея. DisplayPrecipitationSection отображает количество осадков и снега с помощью простых значков, если значения превышают пороговое значение.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
void DrawAstronomySection(int x, int y) { u8g2Fonts.setFont(u8g2_font_helvB08_tf); display.drawRect(x, y + 64, 167, 48, GxEPD_BLACK); drawString(x + 7, y + 70, ConvertUnixTime(WxConditions[0].Sunrise + WxConditions[0].Timezone).substring(0, (Units == "M" ? 5 : 7)) + " " + TXT_SUNRISE, LEFT); drawString(x + 7, y + 85, ConvertUnixTime(WxConditions[0].Sunset + WxConditions[0].Timezone).substring(0, (Units == "M" ? 5 : 7)) + " " + TXT_SUNSET, LEFT); time_t now = time(NULL); struct tm * now_utc = gmtime(&now); const int day_utc = now_utc->tm_mday; const int month_utc = now_utc->tm_mon + 1; const int year_utc = now_utc->tm_year + 1900; drawString(x + 7, y + 100, MoonPhase(day_utc, month_utc, year_utc), LEFT); DrawMoon(x + 105, y + 50, day_utc, month_utc, year_utc, Hemisphere); } void DrawMoon(int x, int y, int dd, int mm, int yy, String hemisphere) { const int diameter = 38; double Phase = NormalizedMoonPhase(dd, mm, yy); hemisphere.toLowerCase(); if (hemisphere == "south") Phase = 1 - Phase; // Draw dark part of moon display.fillCircle(x + diameter - 1, y + diameter, diameter / 2 + 1, GxEPD_RED); const int number_of_lines = 90; for (double Ypos = 0; Ypos <= 45; Ypos++) { double Xpos = sqrt(45 * 45 - Ypos * Ypos); // Determine the edges of the lighted part of the moon double Rpos = 2 * Xpos; double Xpos1, Xpos2; if (Phase < 0.5) { Xpos1 = - Xpos; Xpos2 = (Rpos - 2 * Phase * Rpos - Xpos); } else { Xpos1 = Xpos; Xpos2 = (Xpos - 2 * Phase * Rpos + Rpos); } // Draw light part of moon double pW1x = (Xpos1 + number_of_lines) / number_of_lines * diameter + x; double pW1y = (number_of_lines - Ypos) / number_of_lines * diameter + y; double pW2x = (Xpos2 + number_of_lines) / number_of_lines * diameter + x; double pW2y = (number_of_lines - Ypos) / number_of_lines * diameter + y; double pW3x = (Xpos1 + number_of_lines) / number_of_lines * diameter + x; double pW3y = (Ypos + number_of_lines) / number_of_lines * diameter + y; double pW4x = (Xpos2 + number_of_lines) / number_of_lines * diameter + x; double pW4y = (Ypos + number_of_lines) / number_of_lines * diameter + y; display.drawLine(pW1x, pW1y, pW2x, pW2y, GxEPD_WHITE); display.drawLine(pW3x, pW3y, pW4x, pW4y, GxEPD_WHITE); } display.drawCircle(x + diameter - 1, y + diameter, diameter / 2 + 1, GxEPD_RED); } String MoonPhase(int d, int m, int y) { int c, e; double jd; int b; if (m < 3) { y--; m += 12; } ++m; c = 365.25 * y; e = 30.6 * m; jd = c + e + d - 694039.09; /* jd is total days elapsed */ jd /= 29.53059; /* divide by the moon cycle (29.53 days) */ b = jd; /* int(jd) -> b, take integer part of jd */ jd -= b; /* subtract integer part to leave fractional part of original jd */ b = jd * 8 + 0.5; /* scale fraction from 0-8 and round by adding 0.5 */ b = b & 7; /* 0 and 8 are the same phase so modulo 8 for 0 */ Hemisphere.toLowerCase(); if (Hemisphere == "south") b = 7 - b; if (b == 0) return TXT_MOON_NEW; // New; 0% illuminated if (b == 1) return TXT_MOON_WAXING_CRESCENT; // Waxing crescent; 25% illuminated if (b == 2) return TXT_MOON_FIRST_QUARTER; // First quarter; 50% illuminated if (b == 3) return TXT_MOON_WAXING_GIBBOUS; // Waxing gibbous; 75% illuminated if (b == 4) return TXT_MOON_FULL; // Full; 100% illuminated if (b == 5) return TXT_MOON_WANING_GIBBOUS; // Waning gibbous; 75% illuminated if (b == 6) return TXT_MOON_THIRD_QUARTER; // Third quarter; 50% illuminated if (b == 7) return TXT_MOON_WANING_CRESCENT; // Waning crescent; 25% illuminated return ""; } |
Раздел DrawAstronomySection отображает текст о восходе, закате и фазах Луны и вызывает функцию DrawMoon для отображения её визуального представления. Фаза Луны рассчитывается с помощью функции MoonPhase , которая использует математические вычисления на основе даты для определения текущей фазы и возвращает соответствующую метку.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 |
void DisplayWXicon(int x, int y, String IconName, bool IconSize) { Serial.println(IconName); if (IconName == "01d" || IconName == "01n") Sunny(x, y, IconSize, IconName); else if (IconName == "02d" || IconName == "02n") MostlySunny(x, y, IconSize, IconName); else if (IconName == "03d" || IconName == "03n") Cloudy(x, y, IconSize, IconName); else if (IconName == "04d" || IconName == "04n") MostlyCloudy(x, y, IconSize, IconName); else if (IconName == "09d" || IconName == "09n") ChanceRain(x, y, IconSize, IconName); else if (IconName == "10d" || IconName == "10n") Rain(x, y, IconSize, IconName); else if (IconName == "11d" || IconName == "11n") Tstorms(x, y, IconSize, IconName); else if (IconName == "13d" || IconName == "13n") Snow(x, y, IconSize, IconName); else if (IconName == "50d") Haze(x, y, IconSize, IconName); else if (IconName == "50n") Fog(x, y, IconSize, IconName); else Nodata(x, y, IconSize, IconName); } void addcloud(int x, int y, int scale, int linesize) { //Draw cloud outer display.fillCircle(x - scale * 3, y, scale, GxEPD_RED); // Left most circle display.fillCircle(x + scale * 3, y, scale, GxEPD_RED); // Right most circle display.fillCircle(x - scale, y - scale, scale * 1.4, GxEPD_RED); // left middle upper circle display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75, GxEPD_RED); // Right middle upper circle display.fillRect(x - scale * 3 - 1, y - scale, scale * 6, scale * 2 + 1, GxEPD_RED); // Upper and lower lines //Clear cloud inner display.fillCircle(x - scale * 3, y, scale - linesize, GxEPD_WHITE); // Clear left most circle display.fillCircle(x + scale * 3, y, scale - linesize, GxEPD_WHITE); // Clear right most circle display.fillCircle(x - scale, y - scale, scale * 1.4 - linesize, GxEPD_WHITE); // left middle upper circle display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75 - linesize, GxEPD_WHITE); // Right middle upper circle display.fillRect(x - scale * 3 + 2, y - scale + linesize - 1, scale * 5.9, scale * 2 - linesize * 2 + 2, GxEPD_WHITE); // Upper and lower lines } void addraindrop(int x, int y, int scale) { display.fillCircle(x, y, scale / 2, GxEPD_RED); display.fillTriangle(x - scale / 2, y, x, y - scale * 1.2, x + scale / 2, y , GxEPD_RED); x = x + scale * 1.6; y = y + scale / 3; display.fillCircle(x, y, scale / 2, GxEPD_RED); display.fillTriangle(x - scale / 2, y, x, y - scale * 1.2, x + scale / 2, y , GxEPD_RED); } void addrain(int x, int y, int scale, bool IconSize) { if (IconSize == SmallIcon) scale *= 1.34; for (int d = 0; d < 4; d++) { addraindrop(x + scale * (7.8 - d * 1.95) - scale * 5.2, y + scale * 2.1 - scale / 6, scale / 1.6); } } void addsnow(int x, int y, int scale, bool IconSize) { int dxo, dyo, dxi, dyi; for (int flakes = 0; flakes < 5; flakes++) { for (int i = 0; i < 360; i = i + 45) { dxo = 0.5 * scale * cos((i - 90) * 3.14 / 180); dxi = dxo * 0.1; dyo = 0.5 * scale * sin((i - 90) * 3.14 / 180); dyi = dyo * 0.1; display.drawLine(dxo + x + flakes * 1.5 * scale - scale * 3, dyo + y + scale * 2, dxi + x + 0 + flakes * 1.5 * scale - scale * 3, dyi + y + scale * 2, GxEPD_RED); } } } void addtstorm(int x, int y, int scale) { y = y + scale / 2; for (int i = 0; i < 5; i++) { display.drawLine(x - scale * 4 + scale * i * 1.5 + 0, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 0, y + scale, GxEPD_RED); if (scale != Small) { display.drawLine(x - scale * 4 + scale * i * 1.5 + 1, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 1, y + scale, GxEPD_RED); display.drawLine(x - scale * 4 + scale * i * 1.5 + 2, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 2, y + scale, GxEPD_RED); } display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 0, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 0, GxEPD_RED); if (scale != Small) { display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 1, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 1, GxEPD_RED); display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 2, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 2, GxEPD_RED); } display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 0, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5, GxEPD_RED); if (scale != Small) { display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 1, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 1, y + scale * 1.5, GxEPD_RED); display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 2, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 2, y + scale * 1.5, GxEPD_RED); } } } void addsun(int x, int y, int scale, bool IconSize) { int linesize = 3; if (IconSize == SmallIcon) linesize = 1; display.fillRect(x - scale * 2, y, scale * 4, linesize, GxEPD_BLACK); display.fillRect(x, y - scale * 2, linesize, scale * 4, GxEPD_BLACK); display.drawLine(x - scale * 1.3, y - scale * 1.3, x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK); display.drawLine(x - scale * 1.3, y + scale * 1.3, x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK); if (IconSize == LargeIcon) { display.drawLine(1 + x - scale * 1.3, y - scale * 1.3, 1 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK); display.drawLine(2 + x - scale * 1.3, y - scale * 1.3, 2 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK); display.drawLine(3 + x - scale * 1.3, y - scale * 1.3, 3 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK); display.drawLine(1 + x - scale * 1.3, y + scale * 1.3, 1 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK); display.drawLine(2 + x - scale * 1.3, y + scale * 1.3, 2 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK); display.drawLine(3 + x - scale * 1.3, y + scale * 1.3, 3 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK); } display.fillCircle(x, y, scale * 1.3, GxEPD_WHITE); display.fillCircle(x, y, scale, GxEPD_BLACK); display.fillCircle(x, y, scale - linesize, GxEPD_WHITE); } void addfog(int x, int y, int scale, int linesize, bool IconSize) { if (IconSize == SmallIcon) { y -= 10; linesize = 1; } for (int i = 0; i < 6; i++) { display.fillRect(x - scale * 3, y + scale * 1.5, scale * 6, linesize, GxEPD_RED); display.fillRect(x - scale * 3, y + scale * 2.0, scale * 6, linesize, GxEPD_RED); display.fillRect(x - scale * 3, y + scale * 2.5, scale * 6, linesize, GxEPD_RED); } } void Sunny(int x, int y, bool IconSize, String IconName) { int scale = Small, offset = 3; if (IconSize == LargeIcon) { scale = Large; y = y - 8; offset = 18; } else y = y - 3; // Shift up small sun icon if (IconName.endsWith("n")) addmoon(x, y + offset, scale, IconSize); scale = scale * 1.6; addsun(x, y, scale, IconSize); } void MostlySunny(int x, int y, bool IconSize, String IconName) { int scale = Small, linesize = 3, offset = 3; if (IconSize == LargeIcon) { scale = Large; offset = 10; } else linesize = 1; if (IconName.endsWith("n")) addmoon(x, y + offset, scale, IconSize); addcloud(x, y + offset, scale, linesize); addsun(x - scale * 1.8, y - scale * 1.8 + offset, scale, IconSize); } void MostlyCloudy(int x, int y, bool IconSize, String IconName) { int scale = Large, linesize = 3; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); addcloud(x, y, scale, linesize); addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize); addcloud(x, y, scale, linesize); } void Cloudy(int x, int y, bool IconSize, String IconName) { int scale = Large, linesize = 3; if (IconSize == SmallIcon) { scale = Small; if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); linesize = 1; addcloud(x, y, scale, linesize); } else { y += 10; if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); addcloud(x + 30, y - 35, 5, linesize); // Cloud top right addcloud(x - 20, y - 25, 7, linesize); // Cloud top left addcloud(x, y, scale, linesize); // Main cloud } } void Rain(int x, int y, bool IconSize, String IconName) { int scale = Large, linesize = 3; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y + 10, scale, IconSize); addcloud(x, y, scale, linesize); addrain(x, y, scale, IconSize); } void ExpectRain(int x, int y, bool IconSize, String IconName) { int scale = Large, linesize = 3; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize); addcloud(x, y, scale, linesize); addrain(x, y, scale, IconSize); } void ChanceRain(int x, int y, bool IconSize, String IconName) { int scale = Large, linesize = 3; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize); addcloud(x, y, scale, linesize); addrain(x, y, scale, IconSize); } void Tstorms(int x, int y, bool IconSize, String IconName) { int scale = Large, linesize = 3; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); addcloud(x, y, scale, linesize); addtstorm(x, y, scale); } void Snow(int x, int y, bool IconSize, String IconName) { int scale = Large, linesize = 3; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y + 15, scale, IconSize); addcloud(x, y, scale, linesize); addsnow(x, y, scale, IconSize); } void Fog(int x, int y, bool IconSize, String IconName) { int linesize = 3, scale = Large; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); addcloud(x, y - 5, scale, linesize); addfog(x, y - 5, scale, linesize, IconSize); } void Haze(int x, int y, bool IconSize, String IconName) { int linesize = 3, scale = Large; if (IconSize == SmallIcon) { scale = Small; linesize = 1; } if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize); addsun(x, y - 5, scale * 1.4, IconSize); addfog(x, y - 5, scale * 1.4, linesize, IconSize); } void CloudCover(int x, int y, int CCover) { addcloud(x - 9, y - 3, Small * 0.5, 2); // Cloud top left addcloud(x + 3, y - 3, Small * 0.5, 2); // Cloud top right addcloud(x, y, Small * 0.5, 2); // Main cloud u8g2Fonts.setFont(u8g2_font_helvB08_tf); drawString(x + 15, y - 5, String(CCover) + "%", LEFT); } void Visibility(int x, int y, String Visi) { y = y - 3; // float start_angle = 0.52, end_angle = 2.61; int r = 10; for (float i = start_angle; i < end_angle; i = i + 0.05) { display.drawPixel(x + r * cos(i), y - r / 2 + r * sin(i), GxEPD_RED); display.drawPixel(x + r * cos(i), 1 + y - r / 2 + r * sin(i), GxEPD_RED); } start_angle = 3.61; end_angle = 5.78; for (float i = start_angle; i < end_angle; i = i + 0.05) { display.drawPixel(x + r * cos(i), y + r / 2 + r * sin(i), GxEPD_RED); display.drawPixel(x + r * cos(i), 1 + y + r / 2 + r * sin(i), GxEPD_RED); } display.fillCircle(x, y, r / 4, GxEPD_RED); u8g2Fonts.setFont(u8g2_font_helvB08_tf); drawString(x + 12, y - 3, Visi, LEFT); } void addmoon(int x, int y, int scale, bool IconSize) { if (IconSize == LargeIcon) { x = x + 12; y = y + 12; display.fillCircle(x - 50, y - 55, scale, GxEPD_RED); display.fillCircle(x - 35, y - 55, scale * 1.6, GxEPD_WHITE); } else { display.fillCircle(x - 20, y - 12, scale, GxEPD_RED); display.fillCircle(x - 15, y - 12, scale * 1.6, GxEPD_WHITE); } } void Nodata(int x, int y, bool IconSize, String IconName) { if (IconSize == LargeIcon) u8g2Fonts.setFont(u8g2_font_helvB24_tf); else u8g2Fonts.setFont(u8g2_font_helvB10_tf); drawString(x - 3, y - 8, "?", CENTER); u8g2Fonts.setFont(u8g2_font_helvB08_tf); } void DrawBattery(int x, int y) { uint8_t percentage = 100; float voltage = analogRead(3) / 4096.0 * 7.46; if (voltage > 1 ) { // Only display if there is a valid reading Serial.println("Voltage = " + String(voltage)); percentage = 2836.9625 * pow(voltage, 4) - 43987.4889 * pow(voltage, 3) + 255233.8134 * pow(voltage, 2) - 656689.7123 * voltage + 632041.7303; if (voltage >= 4.20) percentage = 100; if (voltage <= 3.50) percentage = 0; display.drawRect(x + 15, y - 12, 19, 10, GxEPD_BLACK); display.fillRect(x + 34, y - 10, 2, 5, GxEPD_BLACK); display.fillRect(x + 17, y - 10, 15 * percentage / 100.0, 6, GxEPD_RED); drawString(x + 65, y - 11, String(percentage) + "%", RIGHT); //drawString(x + 13, y + 5, String(voltage, 2) + "v", CENTER); } } void DrawGraph(int x_pos, int y_pos, int gwidth, int gheight, float Y1Min, float Y1Max, String title, float DataArray[], int readings, boolean auto_scale, boolean barchart_mode) { #define auto_scale_margin 0 // Sets the autoscale increment, so axis steps up in units of e.g. 3 #define y_minor_axis 5 // 5 y-axis division markers float maxYscale = -10000; float minYscale = 10000; int last_x, last_y; float x1, y1, x2, y2; if (auto_scale == true) { for (int i = 1; i < readings; i++ ) { if (DataArray[i] >= maxYscale) maxYscale = DataArray[i]; if (DataArray[i] <= minYscale) minYscale = DataArray[i]; } maxYscale = round(maxYscale + auto_scale_margin); // Auto scale the graph and round to the nearest value defined, default was Y1Max Y1Max = round(maxYscale + 0.5); if (minYscale != 0) minYscale = round(minYscale - auto_scale_margin); // Auto scale the graph and round to the nearest value defined, default was Y1Min Y1Min = round(minYscale); } // Draw the graph last_x = x_pos + 1; last_y = y_pos + (Y1Max - constrain(DataArray[1], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight; display.drawRect(x_pos, y_pos, gwidth + 3, gheight + 2, GxEPD_BLACK); u8g2Fonts.setFont(u8g2_font_helvB08_tf); drawString(x_pos + gwidth / 2, y_pos - 12, title, CENTER); // Draw the graph last_x = x_pos; last_y = y_pos + (Y1Max - constrain(DataArray[1], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight; display.drawRect(x_pos, y_pos, gwidth + 3, gheight + 2, GxEPD_BLACK); drawString(x_pos + gwidth / 2, y_pos - 13, title, CENTER); // Draw the data for (int gx = 0; gx < readings; gx++) { y2 = y_pos + (Y1Max - constrain(DataArray[gx], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight + 1; if (barchart_mode) { x2 = x_pos + gx * (gwidth / readings) + 2; display.fillRect(x2, y2, (gwidth / readings) - 2, y_pos + gheight - y2 + 2, GxEPD_RED); } else { x2 = x_pos + gx * gwidth / (readings - 1) + 1; // max_readings is the global variable that sets the maximum data that can be plotted display.drawLine(last_x, last_y, x2, y2, GxEPD_RED); } last_x = x2; last_y = y2; } //Draw the Y-axis scale #define number_of_dashes 15 for (int spacing = 0; spacing <= y_minor_axis; spacing++) { for (int j = 0; j < number_of_dashes; j++) { // Draw dashed graph grid lines if (spacing < y_minor_axis) display.drawFastHLine((x_pos + 3 + j * gwidth / number_of_dashes), y_pos + (gheight * spacing / y_minor_axis), gwidth / (2 * number_of_dashes), GxEPD_BLACK); } if ((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing) < 5 || title == TXT_PRESSURE_IN) { drawString(x_pos, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 1), RIGHT); } else { if (Y1Min < 1 && Y1Max < 10) drawString(x_pos - 3, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 1), RIGHT); else drawString(x_pos - 3, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 0), RIGHT); } } for (int i = 0; i <= 2; i++) { drawString(15 + x_pos + gwidth / 3 * i, y_pos + gheight + 3, String(i), LEFT); } drawString(x_pos + gwidth / 2, y_pos + gheight + 10, TXT_DAYS, CENTER); } |
Функция DisplayWXicon выбирает подходящую процедуру отрисовки на основе кода значка погоды из OpenWeatherMap. Она сопоставляет строки значка, например, «01d» или «09n», с соответствующими функциями отрисовки символов, такими как «Солнечно», «Дождь», «Туман» и другими, упомянутыми выше. Эти функции используются для отрисовки различных значков с использованием простых графических примитивов в зависимости от погодных данных. Функция drawBattery отображает состояние батареи, а функция drawGrapgh отвечает за графики прогноза погоды.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
void StopWiFi() { WiFi.disconnect(); WiFi.mode(WIFI_OFF); } //######################################################################################### boolean SetupTime() { configTime(gmtOffset_sec, daylightOffset_sec, ntpServer, "time.nist.gov"); //(gmtOffset_sec, daylightOffset_sec, ntpServer) setenv("TZ", Timezone, 1); //setenv()adds the "TZ" variable to the environment with a value TimeZone, only used if set to 1, 0 means no change tzset(); // Set the TZ environment variable delay(100); bool TimeStatus = UpdateLocalTime(); return TimeStatus; } boolean UpdateLocalTime() { struct tm timeinfo; char time_output[30], day_output[30], update_time[30]; while (!getLocalTime(&timeinfo, 10000)) { // Wait for 5-sec for time to synchronise Serial.println("Failed to obtain time"); return false; } CurrentHour = timeinfo.tm_hour; CurrentMin = timeinfo.tm_min; CurrentSec = timeinfo.tm_sec; 2017 14:05:49 if (Units == "M") { sprintf(day_output, "%s %02u-%s-%04u", weekday_D[timeinfo.tm_wday], timeinfo.tm_mday, month_M[timeinfo.tm_mon], (timeinfo.tm_year) + 1900); strftime(update_time, sizeof(update_time), "%H:%M", &timeinfo); // Creates: '@ 14:05:49' and change from 30 to 8 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< sprintf(time_output, "%s", update_time); } else { strftime(day_output, sizeof(day_output), "%a %b-%d-%Y", &timeinfo); // Creates 'Sat May-31-2019' strftime(update_time, sizeof(update_time), "%r", &timeinfo); // Creates: '@ 02:05:49pm' sprintf(time_output, "%s", update_time); } date_str = day_output; time_str = time_output; return true; } |
Функция StartWiFi подключает ESP32 к настроенной сети Wi-Fi и проверяет её состояние в течение 15-секундного тайм-аута. В случае успешного подключения она регистрирует уровень сигнала. Функция StopWiFi отключает и выключает Wi-Fi-модуль для экономии энергии. Функция SetupTime настраивает ESP32 на получение корректного местного времени с помощью NTP-серверов. Она устанавливает часовой пояс и вызывает функцию UpdateLocalTime для получения и форматирования текущего времени и даты, сохраняя их в глобальных переменных для отображения.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
void drawString(int x, int y, String text, alignment align) { int16_t x1, y1; //the bounds of x,y and w and h of the variable 'text' in pixels. uint16_t w, h; display.setTextWrap(false); display.getTextBounds(text, x, y, &x1, &y1, &w, &h); if (align == RIGHT) x = x - w; if (align == CENTER) x = x - w / 2; u8g2Fonts.setCursor(x, y + h); u8g2Fonts.print(text); } void drawStringMaxWidth(int x, int y, unsigned int text_width, String text, alignment align) { int16_t x1, y1; //the bounds of x,y and w and h of the variable 'text' in pixels. uint16_t w, h; display.getTextBounds(text, x, y, &x1, &y1, &w, &h); if (align == RIGHT) x = x - w; if (align == CENTER) x = x - w / 2; u8g2Fonts.setCursor(x, y); if (text.length() > text_width * 2) { u8g2Fonts.setFont(u8g2_font_helvB10_tf); text_width = 42; y = y - 3; } u8g2Fonts.println(text.substring(0, text_width)); if (text.length() > text_width) { u8g2Fonts.setCursor(x, y + h + 15); String secondLine = text.substring(text_width); secondLine.trim(); // Remove any leading spaces u8g2Fonts.println(secondLine); } } void InitialiseDisplay() { display.init(115200, true, 2, false); // display.init(); for older Waveshare HAT's SPI.end(); SPI.begin(EPD_SCK, EPD_MISO, EPD_MOSI, EPD_CS); u8g2Fonts.begin(display); // connect u8g2 procedures to Adafruit GFX u8g2Fonts.setFontMode(1); // use u8g2 transparent mode (this is default) u8g2Fonts.setFontDirection(0); // left to right (this is default) u8g2Fonts.setForegroundColor(GxEPD_BLACK); // apply Adafruit GFX color u8g2Fonts.setBackgroundColor(GxEPD_WHITE); // apply Adafruit GFX color u8g2Fonts.setFont(u8g2_font_helvB10_tf); // select u8g2 font from here: https://github.com/olikraus/u8g2/wiki/fntlistall display.fillScreen(GxEPD_WHITE); display.setFullWindow(); } |
InitialiseDisplay настраивает E-Ink-дисплей с необходимой конфигурацией SPI и инициализирует механизм рендеринга шрифтов. Он очищает экран и переводит дисплей в полнооконный режим для обновления. Функции drawString и drawStringMaxWidth обрабатывают различные текстовые элементы в пользовательском интерфейсе.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
void softReset() { Wire.beginTransmission(HTU21D_ADDRESS); Wire.write(SOFT_RESET); Wire.endTransmission(); delay(15); // Wait for reset to complete } void disableHeater() { // Read the user register Wire.beginTransmission(HTU21D_ADDRESS); Wire.write(USER_REGISTER_READ); Wire.endTransmission(); Wire.requestFrom(HTU21D_ADDRESS, 1); if (Wire.available()) { uint8_t userReg = Wire.read(); // Read the user register value // Clear bit 2 to disable the heater userReg &= ~(1 << 2); // Write the updated value back to the user register Wire.beginTransmission(HTU21D_ADDRESS); Wire.write(USER_REGISTER_WRITE); Wire.write(userReg); Wire.endTransmission(); } } // Function to read temperature float readTemperature() { Wire.beginTransmission(HTU21D_ADDRESS); Wire.write(TEMP_MEASURE_NO_HOLD); // Send temperature measurement command Wire.endTransmission(); delay(50); // Wait for conversion (50ms for temperature) Wire.requestFrom(HTU21D_ADDRESS, 3); // Read 3 bytes (2 data + 1 CRC) if (Wire.available() == 3) { uint16_t rawTemp = (Wire.read() << 8) | Wire.read(); Wire.read(); // Read and discard CRC rawTemp &= 0xFFFC; // Clear status bits Serial.print("T: "); Serial.print(rawTemp); Serial.println(" °C"); return -46.85 + (175.72 * rawTemp / 65536.0); // Convert to Celsius } return -999.0; // Error value } // Function to read humidity float readHumidity() { Wire.beginTransmission(HTU21D_ADDRESS); Wire.write(HUMID_MEASURE_NO_HOLD); // Send humidity measurement command Wire.endTransmission(); delay(16); // Wait for conversion (16ms for humidity) Wire.requestFrom(HTU21D_ADDRESS, 3); // Read 3 bytes (2 data + 1 CRC) if (Wire.available() == 3) { uint16_t rawHumidity = (Wire.read() << 8) | Wire.read(); Wire.read(); // Read and discard CRC rawHumidity &= 0xFFFC; // Clear status bits Serial.print("H: "); Serial.print(rawHumidity); Serial.println(" °C"); return -6.0 + (125.0 * rawHumidity / 65536.0); // Convert to %RH } return -999.0; // Error value } |
Остальные функции отвечают за управление датчиками температуры и влажности в помещении. Функция softReset отправляет команду мягкого сброса датчику HTU21D, чтобы убедиться в его готовности к использованию. Функция disableHeater считывает и изменяет регистр конфигурации датчика, чтобы убедиться, что внутренний нагреватель выключен, что повышает точность измерений. Функции readTemperature и readHumidity взаимодействуют с датчиком HTU21D по интерфейсу I2C для получения необработанных значений температуры и влажности, преобразования их в понятный человеку формат и отображения.
