전자 종이 디스플레이의 온도 플로터
이 튜토리얼에서는 ESP32와 BME280 센서를 사용하여 4.2인치 전자 종이 디스플레이에 온도 플로터를 구축합니다.
온도 데이터를 표시하는 일반적인 방법은 데카르트 좌표계의 선 플롯으로, 여기서 시간은 x축에 있고 온도 또는 기타 환경 데이터는 y축에 있습니다. 아래의 예시 그림을 참조하세요. 여기서 온도(빨간색 선)는 15°~25° 사이이고 시간은 1시~12시 사이입니다.
데카르트 좌표로 온도를 플로팅하면 온도 변화를 쉽게 볼 수 있지만 시간이 순환적이라는 사실(예: 하루 24시간 반복)을 무시합니다. 이 경우 극좌표계가 더 나은 선택인 경우가 많습니다. 시계 시간이 표시된 원이나 고리 주위에 온도를 표시하는 아래의 예시 플롯을 참조하세요.
시간 축이 시계 시간을 따르기 때문에 특정 시간의 온도를 보는 것이 조금 더 쉽습니다. 그러나 더 중요한 것은 12시와 11시 59분이 나란히 있기 때문에 플롯이 연속적이라는 것입니다. 예를 들어 플롯을 스크롤할 필요 없이 며칠 동안 플롯할 수 있습니다.
이 튜토리얼에서는 온도 데이터를 위한 이러한 극좌표 플로터를 만드는 방법을 알아봅니다. 플로터는 시간과 날짜 외에도 현재 온도, 습도 및 기압을 표시합니다. 아래 화면 샷은 완성된 플로터의 모습을 보여줍니다.
플로팅된 온도 곡선을 빨간색으로 강조 표시했습니다. 회색조 전자 종이 디스플레이를 사용하므로 색상이 없습니다. 이제 필요한 부분으로 넘어갑니다.
개요
필수 부품
4.2인치 전자종이 디스플레이
BME280 센서
ESP32 lite
USB 데이터 케이블
Dupont 와이어 세트
브레드보드
BME280 센서
전자종이 디스플레이
전자종이 연결 및 테스트
GxEPD2 라이브러리 설치
Adafruit_BME280 라이브러리 설치
전자종이 디스플레이 테스트
온도 센서 연결 및 테스트
BME280 센서 테스트
전자종이의 온도 플로터 코드
라이브러리
상수
객체
initDisplay 함수
setTimezone 함수
syncTime 함수
initSensor 함수
polar2cart 함수
printAt 및 printfAt 함수
drawDottedCircle 함수
curSeconds 함수
sec2angle 함수
drawTimeArc 함수
drawClockLabels 함수
plotTemp 함수
printBMEData 함수
printTimeDate 함수
drawAll 함수
drawCanvas 기능
partialRefresh 기능
fullRefresh 기능
setup 기능
loop 기능
결론
부품 목록
저는 ES32 lite를 마이크로프로세서로 사용하고 있습니다. 저렴하고 배터리 충전 인터페이스가 있어서 LiPo 배터리로 플로터를 실행할 수 있습니다. 충분한 메모리가 있는 다른 ESP32나 ESP8266도 작동하지만 배터리 충전 인터페이스가 있는 것을 사용하는 것이 좋습니다.
4.2인치 전자종이 디스플레이
BME280 센서
ESP32 lite Lolin32
Makerguides.com은 Amazon.com에서 제품을 광고하고 링크하여 사이트에서 광고 수수료를 벌 수 있는 수단을 제공하도록 설계된 제휴 광고 프로그램인 Amazon Services LLC Associates Program에 참여하고 있습니다. Amazon Associate로서 우리는 적격 구매로 수익을 얻습니다.
BME280 센서
우리는 온도 플로터의 온도 데이터를 측정하기 위해 BME280 센서를 사용할 것입니다. 그러나 BME280을 사용하면 습도와 기압도 측정할 수 있으며, 이를 플로팅할 수도 있습니다. BME280의 또 다른 특징은 저전력 절전 모드가 있어서 0.1µA(일반 모드에서는 3.6µA)만 소모한다는 것입니다. 전자종이와 함께 사용하면 배터리 전원으로 작동할 수 있는 저전력 설정이 가능합니다. BME280 센서 자체는 매우 작으며 일반적으로 I2C 인터페이스가 있는 브레이크아웃 보드에 제공됩니다. 아래 그림을 참조하세요. I2C 인터페이스 덕분에 연결하고 사용하기가 매우 쉽습니다.
센서는 300hPa~1100hPa의 압력, -40°C~+85°C의 온도, 0%~100%의 습도를 측정할 수 있습니다. BME280과 그 응용 분야에 대한 자세한 내용은 Arduino와 함께 BME280 압력 센서를 사용하는 방법 및 전자종이 디스플레이의 기상 관측소 튜토리얼을 참조하세요.
전자종이 디스플레이
이 프로젝트에 사용된 전자종이 디스플레이는 4.2인치 디스플레이 모듈로, 400×300픽셀 해상도, 4개 회색 레벨, 0.4초의 부분적 새로 고침 시간, SPI 인터페이스가 있는 임베디드 컨트롤러를 갖추고 있습니다.
모듈 뒷면에는 4선 SPI에서 3선 SPI로 전환하기 위한 작은 점퍼 패드/스위치가 있습니다. 여기서는 기본 4선 SPI를 사용할 것입니다. 따라서 아무것도 변경할 필요가 없습니다.
디스플레이 모듈은 3.3V 또는 5V에서 작동하고, 0.01µA의 매우 낮은 수면 전류를 가지며, 새로 고침하는 동안 약 26.4mW만 소모합니다. 전자종이 디스플레이에 대한 자세한 내용은 일반적으로 다음 두 가지 자습서를 참조하세요. Arduino를 전자잉크 디스플레이에 인터페이싱하고 전자종이 디스플레이를 부분적으로 새로 고침합니다.
전자종이 연결 및 테스트
먼저 전자종이의 기능을 연결하고 테스트해 보겠습니다. 다음 그림은 ESP32와 디스플레이 간의 전원 및 SPI 인터페이스 간의 전체 배선을 보여줍니다.
편의를 위해 모든 연결부를 표로 정리했습니다. 3.3V 또는 5V로 디스플레이에 전원을 공급할 수 있지만 ESP32-lite는 3.3V 출력만 있고 SPI 데이터 라인은 3.3V여야 합니다!
e-Paper display | ESP32 lite |
CS/SS | 5 |
SCL/SCK | 18 |
SDA/DIN/MOSI | 23 |
BUSY | 15 |
RES/RST | 2 |
DC | 0 |
VCC | 3.3V |
GND | G |
GxEPD2 라이브러리 설치
e-Paper 디스플레이에 그림을 그리거나 쓰기 전에 두 개의 라이브러리를 설치해야 합니다. 공통적인 그래픽 기본 요소(텍스트, 점, 선, 원 등)를 제공하는 Adafruit_GFX 그래픽 라이브러리와 그래픽 드라이버 소프트웨어를 제공하는 GxEPD2 라이브러리 전자신문 디스플레이.
이 라이브러리를 평소와 같은 방식으로 설치하기만 하면 됩니다. 설치 후 라이브러리 관리자에 다음과 같이 표시되어야 합니다.
Adafruit_BME280 라이브러리 설치
BME280 센서를 사용하여 온도, 습도 및 기압을 측정할 것이므로 Adafruit_BME280 라이브러리도 설치해야 합니다. 평소와 같이 설치하면 라이브러리 관리자에 다음과 같이 표시되어야 합니다.
전자신문 디스플레이 테스트
라이브러리를 설치한 후 다음 테스트 코드를 실행하여 디스플레이가 작동하는지 확인하는 것이 좋습니다.
#include "GxEPD2_BW.h"
//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
void setup() {
epd.init(115200, true, 50, false);
epd.setRotation(1);
epd.setTextColor(GxEPD_BLACK);
epd.setTextSize(2);
epd.setFullWindow();
epd.fillScreen(GxEPD_WHITE);
epd.setCursor(90, 190);
epd.print("Makerguides");
epd.display();
epd.hibernate();
}
void loop() {}
텍스트 코드는 "Makerguides"라는 텍스트를 인쇄하고 디스플레이가 약간 깜빡인 후(전체 새로 고침) 아래와 같이 디스플레이에 표시되어야 합니다.
그렇지 않다면 문제가 있는 것입니다. 디스플레이가 올바르게 배선되지 않았거나 잘못된 디스플레이 드라이버가 선택되었을 가능성이 큽니다. 중요한 코드 줄은 4.2인치 디스플레이 드라이버를 지정하는 다음 줄입니다.
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
GxEPD2 라이브러리의 Readme에는 지원되는 모든 디스플레이가 나열되어 있으며 자세한 내용은 다음에서 찾을 수 있습니다. 헤더 파일, 예: GxEPD2.h. 디스플레이에 맞는 디스플레이 드라이버를 찾으세요. 시행착오가 필요할 수 있습니다.
온도 센서 연결 및 테스트
I2C 인터페이스 덕분에 BME280 센서를 ESP32에 연결하는 것은 매우 간단합니다. 센서의 SCL을 ESP32의 핀 25에 연결하고 SDA를 핀 33에 연결하기만 하면 됩니다. 그런 다음 ESP32의 GND 핀과 3.3V 핀을 BME280 센서의 VIN에 접지합니다. 아래의 전자신문과 BME280 연결이 있는 전체 배선도를 참조하세요.
다음으로 전자신문 디스플레이가 연결된 상태에서 BME280 센서가 작동하는지 확인해 보겠습니다.
BME280 센서 테스트
다음 테스트 코드는 BME280 센서를 사용하여 온도, 기압 및 상대 습도를 측정하고 측정된 값을 직렬 모니터에 인쇄합니다.
#include "Adafruit_BME280.h"
Adafruit_BME280 bme;
void setup() {
Serial.begin(115200);
Wire.begin(33, 25);
bme.begin(0x76, &Wire);
}
void loop() {
Serial.print("Temperature in degC = ");
Serial.println(bme.readTemperature());
Serial.print("Pressure in hPa = ");
Serial.println(bme.readPressure() / 100.0F);
Serial.print("Humidity in %RH = ");
Serial.println(bme.readHumidity());
Serial.println();
delay(5000);
}
소프트웨어 I2C를 사용하고 있으며 Wire.begin(33, 25)를 호출하여 SDA 및 SCL 데이터 라인이 연결되는 핀을 지정해야 합니다. BME280은 일반적으로 I2C 주소 0x76에 있습니다. 데이터를 읽을 수 없다면 BME280의 주소가 다를 수 있으며 이에 따라 bme.begin(0x76, &Wire)를 변경해야 합니다.
모든 것이 잘 작동하면 직렬 모니터에 다음과 유사한 데이터가 인쇄되는 것을 볼 수 있습니다. 통신 속도를 115200으로 설정해야 합니다.
이렇게 하면 이제 온도 플로터에 대한 코드를 작성할 수 있습니다.
전자 종이에 표시된 온도 플로터에 대한 코드
이 섹션에서는 플로터에 대한 코드를 구현합니다. 플로터 디스플레이의 다음 화면 샷은 플로터가 가질 요소와 기능을 보여줍니다.
바깥쪽 링에는 시간에 따른 온도(빨간색 선)를 표시합니다. 바깥쪽 링에는 시간 레이블과 온도 범위도 표시됩니다. 바깥쪽 링(시간 호)을 따라 있는 작은 검은색 점은 현재 시간을 나타냅니다. 위 그림에서는 11시에서 약 15분 후입니다.
링 중앙에는 현재 온도(18.3°), 현재 상대 습도(47%) 및 기압(1005mb)을 인쇄합니다. 그 아래에는 현재 시간(Mo 11:14)과 날짜(09/09/24)도 인쇄합니다.
아래에서 온도 플로터의 전체 코드를 찾을 수 있습니다. 전체적인 아이디어를 얻기 위해 잠깐 살펴본 다음 세부 정보를 논의합니다.
#include "WiFi.h"
#include "esp_sntp.h"
#include "Adafruit_BME280.h"
#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const int H = GxEPD2_420_GDEY042T81::WIDTH;
const int W = GxEPD2_420_GDEY042T81::HEIGHT;
const int CW = W / 2;
const int CH = H / 2;
const int R = min(W, H) / 2;
const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
const float TMAX = 25.0;
const float TMID = 20.0;
const float TMIN = 15.0;
const int RMAX = R - 5;
const int RMID = R - 25;
const int RMIN = R - 45;
//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
Adafruit_BME280 bme;
GFXcanvas1 canvas(W, H);
void initDisplay() {
epd.init(115200, true, 50, false);
epd.setRotation(1);
canvas.setTextColor(BLACK);
canvas.setTextSize(1);
canvas.setFont();
canvas.fillScreen(WHITE);
drawDottedCircle(CW, CH, RMAX, 2);
drawDottedCircle(CW, CH, RMID, 8);
drawDottedCircle(CW, CH, RMIN, 2);
printfAt(CW, CH - (RMIN + 5), "%.0fc", TMIN);
printfAt(CW, CH - (RMID + 5), "%.0fc", TMID);
printfAt(CW, CH - (RMAX + 5), "%.0fc", TMAX);
}
void setTimezone() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
void syncTime() {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
}
void initSensor() {
Wire.begin(33, 25); // sda, scl, Software I2C
bme.begin(0x76, &Wire);
}
void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
cx = int(x + r * sin(alpha));
cy = int(y - r * cos(alpha));
}
void printAt(int16_t x, int16_t y, const char* text) {
int16_t x1, y1;
uint16_t w, h;
canvas.getTextBounds(text, x, y, &x1, &y1, &w, &h);
canvas.setCursor(x - w / 2, y - h / 2);
canvas.print(text);
}
void printfAt(int16_t x, int16_t y, const char* format, ...) {
static char buff[64];
va_list args;
va_start(args, format);
vsnprintf(buff, 64, format, args);
printAt(x, y, buff);
}
void drawDottedCircle(float x, float y, float r, float s) {
int n = int(TWO_PI / (s / r));
for (int i = 0; i < n; i++) {
float alpha = TWO_PI * i / n;
int cx, cy;
polar2cart(x, y, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
}
int curSeconds() {
static struct tm t;
getLocalTime(&t);
return (t.tm_hour % 12) * 60 * 60 + t.tm_min * 60 + t.tm_sec;
}
float sec2angle(int sec) {
return TWO_PI * sec / (12 * 60 * 60);
}
void drawTimeArc() {
int cx, cy;
float secs = curSeconds();
float alpha = sec2angle(secs);
int n = round(6 * 12 * alpha / TWO_PI);
for (int i = 0; i < n; i++) {
float a = alpha * i / n;
polar2cart(CW, CH, RMIN - 7, a, cx, cy);
canvas.fillCircle(cx, cy, 2, BLACK);
}
}
void drawClockLabels() {
int cx, cy;
canvas.setFont();
for (int h = 1; h <= 12; h++) {
float alpha = sec2angle(h * 60 * 60);
polar2cart(CW, CH, RMIN - 20, alpha, cx, cy);
printfAt(cx, cy, "%d", h);
}
}
void plotTemp() {
float temp = bme.readTemperature();
temp = constrain(temp, TMIN, TMAX);
float c = (RMAX - RMIN) / (TMAX - TMIN);
float r = RMIN + (temp - TMIN) * c;
int cx, cy;
float alpha = sec2angle(curSeconds());
polar2cart(CW, CH, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
void printBMEData() {
float temp = bme.readTemperature();
canvas.setFont(&FreeSansBold24pt7b);
printfAt(CW, CH + 5, "%.1f", temp);
float hum = bme.readHumidity();
float pres = bme.readPressure() / 100;
canvas.setFont();
printfAt(CW, CH + 2, "%.0f%% %.0fmb", hum, pres);
}
void printTimeDate() {
static struct tm t;
getLocalTime(&t);
canvas.setFont(&FreeSansBold9pt7b);
printfAt(CW, CH + 35, "%s %2d:%02d",
DAYSTR[t.tm_wday], t.tm_hour, t.tm_min);
printfAt(CW, CH + 55, "%02d/%02d/%02d",
t.tm_mday, t.tm_mon + 1, t.tm_year - 100);
}
void drawAll() {
canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
drawClockLabels();
drawTimeArc();
printBMEData();
printTimeDate();
plotTemp();
}
void drawCanvas() {
epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}
void partialRefresh(const void* pv) {
drawAll();
epd.setPartialWindow(0, 0, W, H);
drawCanvas();
}
void fullRefresh(const void* pv) {
epd.setFullWindow();
drawCanvas();
}
void setup() {
initSensor();
initDisplay();
setTimezone();
}
void loop() {
static uint16_t iter = 0;
if (iter % 50 == 0)
syncTime();
if (iter % 120 == 0)
epd.drawPaged(fullRefresh, 0);
iter = (iter + 1) % 1000;
epd.drawPaged(partialRefresh, 0);
epd.hibernate();
delay(30 * 1000);
}
라이브러리
Wi-Fi를 통해 플로터 시계를 SNTP 인터넷 시간 서버와 동기화하는 데 필요한 WiFi.h 및 esp_snt.h 라이브러리를 포함하는 것으로 시작합니다.
#include "WiFi.h"
#include "esp_sntp.h"
이렇게 하면 온도를 측정한 시간에 정확히 보고할 수 있습니다. 자세한 내용은 ESP32 시계를 SNTP 서버와 동기화하는 방법 튜토리얼을 참조하세요.
다음으로 BME280 센서에서 온도, 습도 및 압력 데이터를 읽는 데 필요한 Adafruit_BME280 라이브러리를 포함합니다.
#include "Adafruit_BME280.h"
마지막으로 흑백(BW) 전자 종이 디스플레이를 위한 GxEPD2_BW.h 헤더 파일을 포함합니다. 3색 디스플레이가 있는 경우 대신 GxEPD2_3C.h 또는 4색 디스플레이의 경우 GxEPD2_4C.h를 포함하고 7색 디스플레이의 경우 GxEPD2_7C.h를 포함합니다.
#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"
또한 플로터 레이블, 온도, 날짜 및 기타 정보를 표시하는 데 사용하는 세 개의 글꼴 파일을 포함합니다. AdaFruit GFX 글꼴에 대한 개요는 여기에서 확인할 수 있습니다.
상수
다음으로 몇 가지 상수를 정의합니다. 가장 중요한 것은 WiFi의 SSID와 PASSWORD, 그리고 거주하는 시간대를 정의해야 한다는 것입니다.
const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
위의 시간대 사양 "AEST-10AEDT,M10.1.0,M4.1.0/3"은 호주를 위한 것으로, 일광 절약 시간 조정이 적용된 호주 동부 표준시(AEST)에 해당합니다.
이 시간대 정의의 부분은 다음과 같습니다.
- AEST: 호주 동부 표준시
- -10: 협정 세계시(UTC)보다 10시간 앞선 UTC 오프셋
- AEDT: 호주 동부 일광 절약 시간
- M10.1.0: 일광 절약 시간으로의 전환은 10월 1일 일요일에 발생합니다.
- M4.1.0/3: 표준 시간으로의 전환은 4월 1일 일요일에 발생하며, UTC와 3시간 차이가 납니다.
시간대 정의를 찾으려면 Posix 시간대 데이터베이스를 살펴보세요.
그런 다음 e-Paper 디스플레이의 크기(W, H), 중심점(CW, CH) 및 디스플레이의 원의 최대 반지름 R에 대한 상수가 있습니다. initDisplay 함수에서 디스플레이를 회전(setRotation(1))하기 때문에 너비와 높이가 바뀐다는 점에 유의하세요.
const int H = GxEPD2_420_GDEY042T81::WIDTH;
const int W = GxEPD2_420_GDEY042T81::HEIGHT;
const int CW = W / 2;
const int CH = H / 2;
const int R = min(W, H) / 2;
편의상 흑백 색상과 요일 이름에 대한 두 개의 상수도 정의합니다.
const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };
마지막으로 온도(TMAX, TMID, TMIN)의 최대, 중간, 최소값과 극축의 해당 반경(RMAX, RMID, RMIN)을 정의합니다.
const float TMAX = 25.0;
const float TMID = 20.0;
const float TMIN = 15.0;
const int RMAX = R - 5;
const int RMID = R - 25;
const int RMIN = R - 45;
객체
다음으로 e-Paper 디스플레이, BME280 센서, 캔버스에 대한 객체를 만듭니다.
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
Adafruit_BME280 bme;
GFXcanvas1 canvas(W, H);
epd 객체는 디스플레이(e-paper 디스플레이)를 나타냅니다. 이 객체 정의는 사용하는 e-Paper 디스플레이 유형과 일치해야 합니다. 여기에는 4.2인치(= _420) e-디스플레이가 있습니다. GxEPD2 라이브러리의 Readme와 지원되는 디스플레이에 대한 GxEPD2.h 파일을 참조하세요.
캔버스는 백그라운드에서 그릴 수 있는 버퍼입니다. 온도 곡선과 부분 새로 고침을 그리는 데 필요합니다. 자세한 내용은 e-Paper 디스플레이의 부분 새로 고침 튜토리얼을 참조하세요.
initDisplay 함수
initDisplay() 함수는 e-Paper 디스플레이를 초기화하고, 캔버스의 텍스트 색상, 크기 및 글꼴을 설정하고, 캔버스를 흰색으로 채웁니다. 그런 다음 먼저 최소, 중간 및 최대 온도에 대한 극좌표 축을 그린 다음, 축에 온도 라벨을 인쇄합니다.
void initDisplay() {
epd.init(115200, true, 50, false);
epd.setRotation(1);
canvas.setTextColor(BLACK);
canvas.setTextSize(1);
canvas.setFont();
canvas.fillScreen(WHITE);
drawDottedCircle(CW, CH, RMAX, 2);
drawDottedCircle(CW, CH, RMID, 8);
drawDottedCircle(CW, CH, RMIN, 2);
printfAt(CW, CH - (RMIN + 5), "%.0fc", TMIN);
printfAt(CW, CH - (RMID + 5), "%.0fc", TMID);
printfAt(CW, CH - (RMAX + 5), "%.0fc", TMAX);
}
다음 스크린샷은 initDisplay()가 호출된 후의 모습을 보여줍니다.
setTimezone 함수
setTimezone() 함수는 ESP32의 내부 클록에 대한 TIMEZONE을 설정합니다. 앞서 언급했듯이 Posix Timezones Database에서 다른 시간대 정의를 찾을 수 있습니다.
void setTimezone() {
setenv("TZ", TIMEZONE, 1);
tzset();
}
syncTime 함수
syncTime 함수는 WiFi 연결을 생성한 다음 configTzTime()을 호출하여 ESP32의 내부 클록을 SNTP 서버 "pool.ntp.org"와 동기화합니다. 연결할 다른 SNTP 서버 또는 여러 SNTP 서버를 지정할 수 있습니다. 자세한 내용은 ESP32 클록을 SNTP 서버와 동기화하는 방법 튜토리얼을 참조하세요.
void syncTime() {
WiFi.begin(SSID, PWD);
while (WiFi.status() != WL_CONNECTED)
;
configTzTime(TIMEZONE, "pool.ntp.org");
}
initSensor 함수
initSensor() 함수는 BME280 센서를 초기화합니다. SDA와 SCL이 지정된 대로 ESP32의 핀 33과 25에 연결되어 있는지 확인하세요. 또한 I2C 주소 0x76이 센서에 맞는지 확인하세요.
void initSensor() {
Wire.begin(33, 25); // SDA, SCL
bme.begin(0x76, &Wire);
}
polar2cart 함수
polar2cart() 함수는 중심점 x, y, 반지름 r, 각도 alpha로 주어진 극좌표를 cx와 cy에 반환되는 데카르트 좌표로 변환합니다.
void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy)
{
cx = int(x + r * sin(alpha));
cy = int(y - r * cos(alpha));
}
다음 이미지는 polar2cart() 함수가 극좌표를 데카르트 좌표로 변환하는 방법을 보여줍니다.
예를 들어, 원의 시계 시간을 e-Paper 디스플레이가 사용하는 데카르트 x, y 좌표로 변환하려면 이 함수가 필요합니다.
printAt 및 printfAt 함수
printAt() 및 printfAt() 함수는 e-Paper 디스플레이의 좌표 x, y에 텍스트를 인쇄합니다.
void printAt(int16_t x, int16_t y, const char* text) {
int16_t x1, y1;
uint16_t w, h;
canvas.getTextBounds(text, x, y, &x1, &y1, &w, &h);
canvas.setCursor(x - w / 2, y - h / 2);
canvas.print(text);
}
void printfAt(int16_t x, int16_t y, const char* format, ...) {
static char buff[64];
va_list args;
va_start(args, format);
vsnprintf(buff, 64, format, args);
printAt(x, y, buff);
}
printAt() 함수는 일반 텍스트만 인쇄할 수 있는 반면, printfAt()는 printf처럼 작동하며 텍스트와 데이터를 인쇄하기 위한 형식 지정자를 제공할 수 있습니다(예: printfAt(x, y, "%02d-%02d-%02d", m, h, y);
drawDottedCircle 함수
drawDottedCircle() 함수는 중심점 x, y, 반지름 r, 점 사이의 공간 s를 갖는 점선 원을 그립니다. 이 함수는 온도가 표시되는 극축을 그리는 데 사용됩니다.
void drawDottedCircle(float x, float y, float r, float s) {
int n = int(TWO_PI / (s / r));
for (int i = 0; i < n; i++) {
float alpha = TWO_PI * i / n;
int cx, cy;
polar2cart(x, y, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
}
curSeconds 함수
curSeconds() 함수는 현재 시간을 가져와 초로 변환합니다. 예를 들어, 8:50:10은 8 * 60 * 60 +50 * 60 +10 = 31810초로 변환됩니다.
int curSeconds()
{
static struct tm t;
getLocalTime(&t);
return (t.tm_hour % 12) * 60 * 60 + t.tm_min * 60 + t.tm_sec;
}
sec2angle 함수
sec2angle() 함수는 초 단위로 제공된 시간을 시계의 각도로 변환합니다.
float sec2angle(int sec) { return TWO_PI * sec / (12 * 60 * 60); }
drawTimeArc 함수
drawTimeArc() 함수는 링에 현재 시간(시간 아크)을 나타내는 검은색 점을 그립니다. 보시다시피 curSeconds() 및 sec2angle() 함수를 사용하여 현재 시간을 초와 시간 아크의 끝을 결정하는 각도로 변환합니다.
void drawTimeArc() {
int cx, cy;
float secs = curSeconds();
float alpha = sec2angle(secs);
int n = round(6 * 12 * alpha / TWO_PI);
for (int i = 0; i < n; i++) {
float a = alpha * i / n;
polar2cart(CW, CH, RMIN - 7, a, cx, cy);
canvas.fillCircle(cx, cy, 2, BLACK);
}
}
다음 그림은 시간 아크의 끝을 보여줍니다.
drawClockLabels 함수
drawClockLabels() 함수는 단순히 링에 시계 시간 라벨을 그립니다.
void drawClockLabels() {
int cx, cy;
canvas.setFont();
for (int h = 1; h <= 12; h++) {
float alpha = sec2angle(h * 60 * 60);
polar2cart(CW, CH, RMIN - 20, alpha, cx, cy);
printfAt(cx, cy, "%d", h);
}
}
다음 그림은 시계 시간 레이블을 보여주는 디스플레이 섹션입니다.
plotTemp 함수
plotTemp() 함수는 극좌표에서 시간에 따른 온도를 표시합니다. 먼저 현재 온도를 읽고 TMIN에서 TMAX 범위로 제한한 다음 반경 r로 크기를 조정합니다. 그런 다음 현재 시간을 각도 알파로 변환한 다음 온도를 극좌표 알파, r에 단일 픽셀로 표시합니다.
void plotTemp() {
float temp = bme.readTemperature();
temp = constrain(temp, TMIN, TMAX);
float c = (RMAX - RMIN) / (TMAX - TMIN);
float r = RMIN + (temp - TMIN) * c;
int cx, cy;
float alpha = sec2angle(curSeconds());
polar2cart(CW, CH, r, alpha, cx, cy);
canvas.drawPixel(cx, cy, BLACK);
}
모든 그림은 캔버스에서 그려지므로 즉시 볼 수 없습니다. 다음 이미지는 표시될 때 함수의 출력이 어떻게 보이는지 보여줍니다.
온도 플롯이 다시 빨간색으로 강조 표시되지만 실제로는 검은색 선으로 나타납니다.
printBMEData 함수
printBMEData() 함수는 플롯 중앙에 현재 온도, 상대 습도 및 기압을 인쇄합니다.
void printBMEData() {
float temp = bme.readTemperature();
canvas.setFont(&FreeSansBold24pt7b);
printfAt(CW, CH + 5, "%.1f", temp);
float hum = bme.readHumidity();
float pres = bme.readPressure() / 100;
canvas.setFont();
printfAt(CW, CH + 2, "%.0f%% %.0fmb", hum, pres);
}
다음은 출력의 예시 그림입니다.
printTimeDate 함수
printTimeDate() 함수는 플롯 중앙에 현재 요일 이름, 시간 및 날짜를 인쇄합니다.
void printTimeDate() {
static struct tm t;
getLocalTime(&t);
canvas.setFont(&FreeSansBold9pt7b);
printfAt(CW, CH + 35, "%s %2d:%02d",
DAYSTR[t.tm_wday], t.tm_hour, t.tm_min);
printfAt(CW, CH + 55, "%02d/%02d/%02d",
t.tm_mday, t.tm_mon + 1, t.tm_year - 100);
}
출력은 다음과 같습니다.
e-Paper 디스플레이의 시간/날짜
drawAll 함수
drawAll() 함수는 전체 플로터 디스플레이를 그리는 데 필요한 모든 함수를 호출합니다. 여기에는 시계 레이블, 시간 아크, 센서 데이터, 시간 및 온도 플로팅이 포함됩니다.
void drawAll() {
canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
drawClockLabels();
drawTimeArc();
printBMEData();
printTimeDate();
plotTemp();
}
다음과 같은 출력을 생성합니다.
drawCanvas 함수
drawCanvas() 함수는 캔버스를 비트맵으로 해석하여 e-Paper에 표시합니다. 이때 캔버스에 그려진 것이 실제로 디스플레이에 표시됩니다.
void drawCanvas()
{
epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}
partialRefresh 함수
partialRefresh() 함수는 drawAll()을 호출하여 캔버스의 모든 것을 다시 그린 다음 e-Paper의 부분적 새로 고침을 수행합니다. 이는 전체 새로 고침보다 훨씬 빠르지만(0.5초 미만) 잔상이 남습니다.
oid partialRefresh(const void* pv)
{
drawAll();
epd.setPartialWindow(0, 0, W, H);
drawCanvas();
}
자세한 내용은 e-Paper 디스플레이의 부분적 새로 고침 튜토리얼을 참조하세요.
fullRefresh 함수
fullRefresh() 함수는 전체 새로 고침을 수행하는데, 이는 부분적 새로 고침보다 훨씬 느리고 e-Paper 디스플레이가 깜빡거리지만 모든 잔상은 제거합니다.
void fullRefresh(const void* pv)
{
epd.setFullWindow();
drawCanvas();
}
setup 함수
setup() 함수는 BME280 센서와 디스플레이를 초기화하고 시간대를 설정합니다.
void setup()
{
initSensor();
initDisplay();
setTimezone(); }
loop 함수
loop() 함수는 기본적으로 30초마다 디스플레이를 업데이트합니다. 업데이트 횟수(iter)에 따라 부분적 또는 전체적 새로 고침이 수행됩니다. 구체적으로, 전체 새로 고침은 120번째 반복마다 수행되며, 이는 120*30/60 = 60분이 됩니다.
마찬가지로 ESP32의 내부 클록은 약 50*30/60 = 25분마다 SNTP 서버와 동기화됩니다.
void loop() {
static uint16_t iter = 0;
if (iter % 50 == 0)
syncTime();
if (iter % 120 == 0)
epd.drawPaged(fullRefresh, 0);
iter = (iter + 1) % 1000;
epd.drawPaged(partialRefresh, 0);
epd.hibernate();
delay(30 * 1000);
}
코드 설명은 이것으로 끝입니다.
결론
이 튜토리얼에서는 ESP32와 BME280 센서를 사용하여 4.2인치 전자종이 디스플레이에 온도(및 기타 데이터)를 표시하는 Polar Plotter를 만드는 방법을 알아보았습니다.
이 플로터를 수정하거나 확장할 수 있는 방법은 여러 가지가 있습니다. 첫째, 현재 구현의 온도 분해능은 온도가 표시된 링이 상당히 얇기 때문에 매우 낮습니다. 중앙의 텍스트를 줄이고 링을 더 넓게 만들어 더 나은 분해능을 얻을 수 있습니다.
온도 외에도 시간 경과에 따른 기압과 습도도 모니터링하면 좋을 것입니다. 이는 세 개의 캔버스(온도, 기압 및 습도)를 사용하여 주기적으로 또는 회로에 버튼을 추가하여 캔버스 사이를 전환하여 달성할 수 있습니다.
ESP32는 어차피 인터넷에 연결되어 있으므로 시간 경과에 따른 Wi-Fi 속도를 모니터링하고 플로팅할 수도 있습니다. 일반적으로 하루 동안 측정하려는 매개변수(예: 광도, 공기 질, 먼지 등) 또는 기타 주기적 시간 간격은 Polar Plotter에 적합한 선택입니다. 디스플레이 모서리도 날씨 예보 데이터를 표시하는 데 좋은 공간이 될 것입니다.
마지막으로 딥슬립에 대한 말씀드리겠습니다. 디스플레이 업데이트 사이에 ESP32를 딥슬립 상태로 전환하면 좋겠지만, 캔버스에 그린 그림을 포함한 모든 메모리가 딥슬립 상태에서 사라지고 캔버스가 RTC 메모리에 비해 너무 크기 때문에 간단하지 않습니다. 대신 RTC 메모리에 맞는 작은 배열에 온도 시계열을 저장해야 합니다. 불가능하지는 않지만 조금 더 제한적입니다.
가지고 놀 수 있는 아이디어가 많습니다. 즐거운 땜질 되세요 : )
때론 지겹거나 하기 싫고, 늦고, 집중하지 않아도 절대 멈추지 않는다. 너무 기니 버벅댄다. 소스 링크
'개발자 > 부품' 카테고리의 다른 글
전자 종이 e-paper 디스플레이의 기상 관측소 (6) | 2024.11.15 |
---|---|
전자 종이 e-paper 디스플레이의 디지털 시계 (2) | 2024.11.14 |
S8550 트랜지스터 데이터시트, 핀아웃, 회로 및 용도 (0) | 2024.11.13 |
9mm 초소형 시그널 피에조 부저(904RP/A) (0) | 2024.11.12 |
AHT-10 상대 습도 온도 센서 (1) | 2024.11.11 |
전자 PCB용 터치 감지 버튼 스위치 압축 스프링 (6) | 2024.11.08 |
4-Channel Relay(12V) Module Shield (2) | 2024.11.07 |
1층과 2층, 실내와 실외 전등 연결도 (2) | 2024.11.04 |
더욱 좋은 정보를 제공하겠습니다.~ ^^