아두이노 우노 R4 스마트 기상 관측소

이 가이드에서는 Arduino UNO R4 WiFi와 1.28인치 원형 TFT LCD 디스플레이로 스마트 기상 관측소를 만드는 방법을 배웁니다. 이 관측소는 인터넷에서 실시간 날씨를 가져옵니다. 온도 센서나 습도 센서는 전혀 필요하지 않습니다. 자세히 말하면 다음 내용을 배웁니다:

원형 tft lcd 디스플레이를 사용한 아두이노 우노 R4 스마트 기상 관측소

준비물

1×아두이노 우노 R4 와이파이 쿠팡 | 아마존
1×(또는) DIYables STEM V4 IoT 쿠팡 | 아마존
1×USB 케이블 타입-A to 타입-C (USB-A PC용) 쿠팡 | 아마존
1×USB 케이블 타입-C to 타입-C (USB-C PC용) 아마존
1×1.28 Inch Round Circular TFT LCD Display Module 쿠팡 | 아마존
1×Push Button 쿠팡 | 아마존
1×Button Module (또는) 아마존
1×브레드보드 쿠팡 | 아마존
1×점퍼케이블 쿠팡 | 아마존

You need only one of the two button parts. See the wiring section for both options.

1×(추천) 아두이노 우노 R4용 스크루 터미널 블록 쉴드 쿠팡 | 아마존
1×(추천) Sensors/Servo Expansion Shield for Arduino UNO R4 쿠팡 | 아마존
1×(추천) 아두이노 우노 R4용 브레드보드 쉴드 쿠팡 | 아마존
1×(추천) 아두이노 우노 R4용 케이스 쿠팡 | 아마존
1×(추천) 아두이노 우노 R4용 전원 분배기 쿠팡 | 아마존
1×(추천) 아두이노 우노용 프로토타이핑 베이스 플레이트 & 브레드보드 키트 아마존
공개: 이 포스팅 에 제공된 일부 링크는 아마존 제휴 링크입니다. 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
Arduino Uno R4 WiFi Compatible Boards

※ 주의:

이 프로젝트에는 Arduino UNO R4 WiFi가 필요합니다. Arduino UNO R4 Minima에는 WiFi가 없으므로 날씨 API에 접속할 수 없습니다.

스마트 기상 관측소에 대하여

일반적인 기상 관측소는 센서를 사용하여 주변 공기를 측정합니다. 이 관측소는 다른 방식으로 동작합니다. Arduino UNO R4가 인터넷의 날씨 서비스에 요청하면, 서비스가 여러분 도시의 날씨를 보내줍니다. 그래서 이 관측소는 앞으로 몇 시간 동안의 강수 확률이나 앞으로 며칠간의 예보처럼 작은 센서로는 측정할 수 없는 정보도 보여줄 수 있습니다.

관측소에는 8개의 페이지가 있습니다. 푸시 버튼 하나로 페이지 사이를 이동합니다:

  • HOME - 날씨 아이콘, 온도, 하늘 상태 설명, 그리고 시계.
  • TEMP - 온도와 "체감" 온도.
  • HUMIDITY - 링 형태와 숫자로 표시되는 습도.
  • WIND - 풍속과 나침반 형태로 표시되는 풍향.
  • RAIN - 작은 비 애니메이션과 함께 표시되는 강수 확률.
  • NEXT HOURS - 앞으로 몇 시간 동안의 온도와 강수 확률.
  • FORECAST - 앞으로 3일간의 최고 및 최저 온도.
  • SYSTEM - WiFi 상태, 신호 세기, API 상태.

Arduino UNO R4는 10분마다 새로운 날씨를 요청합니다. 두 번의 업데이트 사이에도 홈 페이지의 시계는 계속 흘러갑니다.

Open-Meteo 날씨 API에 대하여

Open-Meteo는 무료 날씨 서비스입니다. Arduino UNO R4 같은 작은 보드에 잘 맞습니다:

  • API 키 불필요: 가입하거나 코드에 키를 붙여넣을 필요가 없습니다.
  • 개인 용도로 무료: 이런 작은 프로젝트에는 비용이 들지 않습니다.
  • 단순한 JSON: 응답은 Arduino UNO R4가 읽을 수 있는 짧은 JSON 텍스트입니다.
  • 어떤 장소든 가능: 위도와 경도로 장소를 선택합니다.

Arduino UNO R4는 HTTPS 요청을 한 번 보내고 약 1.8 KB 크기의 응답 하나를 받습니다. 이 응답에는 현재 날씨, 앞으로 12시간, 그리고 앞으로 3일간의 날씨가 담겨 있습니다.

※ 주의:

응답에는 요청한 장소의 현지 시간도 함께 들어 있습니다. 코드는 여기서 시간을 가져오므로, 관측소는 NTP 클라이언트나 RTC 모듈 없이도 시계를 표시합니다.

배선도

1.28인치 원형 TFT LCD 디스플레이는 Arduino UNO R4의 하드웨어 SPI 핀을 사용합니다. 이 핀들은 고정되어 있습니다: D13이 클럭이고 D11이 데이터 라인입니다.

TFT LCD 핀 Arduino UNO R4 설명
VCC 5V 전원 공급
GND GND 접지
SCL D13 SPI 클럭 (고정 핀)
SDA D11 SPI MOSI (고정 핀)
DC D9 데이터/명령
CS D10 칩 선택
RST D8 리셋

버튼은 두 가지 부품 중 하나를 고를 수 있습니다. 이 프로젝트에서는 둘 다 같은 방식으로 동작합니다. 코드는 한 줄만 다릅니다.

푸시 버튼으로 배선하기

푸시 버튼은 핀이 두 개이고 저항이 없습니다. 버튼을 누르면 D2가 GND에 연결됩니다. Arduino UNO R4가 자체 내부 풀업 저항을 켜므로, 저항을 추가할 필요가 없습니다.

푸시 버튼을 사용한 아두이노 우노 R4 스마트 기상 관측소 배선도

이 이미지는 Fritzing을 사용하여 만들어졌습니다. 이미지를 확대하려면 클릭하세요.

버튼 핀 Arduino UNO R4 설명
핀 1 D2 내부 풀업을 사용하는 버튼 입력
핀 2 GND 접지

코드는 기본적으로 다음을 사용합니다:

ezButton button(PIN_BUTTON);

버튼 모듈로 배선하기

버튼 모듈은 핀이 세 개이고 보드에 자체 저항이 있습니다. 전원이 필요하며, OUT 핀은 D2에 연결합니다. 이 모듈은 누르지 않을 때 LOW를, 누를 때 HIGH를 출력합니다.

버튼 모듈을 사용한 아두이노 우노 R4 스마트 기상 관측소 배선도

이 이미지는 Fritzing을 사용하여 만들어졌습니다. 이미지를 확대하려면 클릭하세요.

Arduino Uno R4와 기타 부품에 전원을 공급하는 가장 효과적인 방법을 확인하시려면, 아래 링크를 참조하세요: 아두이노 우노 R4 전원 공급 방법.

버튼 모듈 핀 Arduino UNO R4 설명
VCC 5V 전원 공급
GND GND 접지
OUT D2 버튼 신호, 누르지 않으면 LOW, 누르면 HIGH

모듈에 이미 저항이 있으므로 내부 저항을 사용하면 안 됩니다. 코드에서 한 줄을 바꾸세요:

ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN);

스마트 기상 관측소 프로그래밍 방법

다섯 개의 라이브러리를 포함합니다.

#include <WiFiS3.h> #include <ArduinoHttpClient.h> #include <ArduinoJson.h> #include <DIYables_TFT_Round.h> #include <ezButton.h>

리셋, 데이터/명령, 칩 선택 핀으로 디스플레이 객체를 생성합니다.

DIYables_TFT_GC9A01_Round tft(8, 9, 10);

WiFi 이름, WiFi 비밀번호, 그리고 장소를 설정합니다.

#define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" #define LATITUDE 37.5665 #define LONGITUDE 126.9780

디스플레이와 WiFi를 시작합니다.

tft.begin(); tft.setRotation(2); WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

날씨 서버에 HTTPS 연결을 엽니다. Arduino UNO R4에서는 WiFiSSLClient가 HTTPS 부분을 처리합니다. 루트 인증서가 이미 WiFi 펌웨어 안에 들어 있으므로, 코드에 인증서를 붙여넣을 필요가 없습니다.

WiFiSSLClient sslClient; HttpClient http(sslClient, "api.open-meteo.com", 443);

요청을 보내고 응답을 읽습니다.

http.get(path); int statusCode = http.responseStatusCode(); String payload = http.responseBody();

JSON 응답을 읽습니다. Arduino UNO R4에는 RAM이 32 KB밖에 없으므로 필터를 사용합니다. 필터는 코드에 필요한 세 블록만 남기고 나머지는 버립니다.

JsonDocument filter; filter["current"] = true; filter["hourly"] = true; filter["daily"] = true; JsonDocument doc; deserializeJson(doc, payload, DeserializationOption::Filter(filter));

응답에서 값 하나를 꺼냅니다.

float temperature = doc["current"]["temperature_2m"] | 0.0;

원형 디스플레이에 그립니다.

tft.setTextSize(3); tft.setTextColor(DIYables_TFT::colorRGB(255, 255, 255)); tft.setCursor(80, 100); tft.print(temperature, 1);

아두이노 UNO R4 코드 - 스마트 기상 관측소

/* * 이 아두이노 우노 R4 코드는 newbiely.kr 에서 개발되었습니다 * 이 아두이노 우노 R4 코드는 어떠한 제한 없이 공개 사용을 위해 제공됩니다. * 상세한 지침 및 연결도에 대해서는 다음을 방문하세요: * https://newbiely.kr/tutorials/arduino-uno-r4/arduino-uno-r4-smart-weather-station */ /* ============================================================ SMART WEATHER STATION Arduino UNO R4 WiFi + GC9A01 Round TFT + Push Button Location: Seoul, South Korea (change it in USER CONFIGURATION) Weather API: Open-Meteo (free, no API key needed) ------------------------------------------------------------ GC9A01 Round TFT -> Arduino UNO R4 VCC -> 5V GND -> GND SCL -> D13 (hardware SPI clock, fixed) SDA -> D11 (hardware SPI MOSI, fixed) RST -> D8 DC -> D9 CS -> D10 Button D2 -> BUTTON -> GND ------------------------------------------------------------ BUTTON Short press -> Next page Long press -> Refresh weather Very long press -> Back to home page ------------------------------------------------------------ This sketch needs the Arduino UNO R4 WiFi. The UNO R4 Minima has no WiFi, so it cannot read the API. ============================================================ */ #include <WiFiS3.h> #include <ArduinoHttpClient.h> #include <ArduinoJson.h> #include <DIYables_TFT_Round.h> #include <ezButton.h> // ===== USER CONFIGURATION ===== #define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" #define LATITUDE 37.5665 #define LONGITUDE 126.9780 #define LOCATION_NAME "SEOUL" // Time zone name for the API, URL-encoded. // "/" must be written as "%2F". #define TIMEZONE "Asia%2FSeoul" // ===== API SERVER ===== const char API_HOST[] = "api.open-meteo.com"; const int API_PORT = 443; // ===== PIN CONFIGURATION ===== // SCK (D13) and MOSI (D11) are the hardware SPI pins of the // Arduino UNO R4. They are fixed and are not listed here. #define PIN_BUTTON 2 #define PIN_RST 8 #define PIN_DC 9 #define PIN_CS 10 // ===== TIMING ===== const unsigned long API_UPDATE_INTERVAL = 10UL * 60UL * 1000UL; const unsigned long WIFI_RETRY_INTERVAL = 10UL * 1000UL; const unsigned long WIFI_CONNECT_TIMEOUT = 15UL * 1000UL; const unsigned long BUTTON_DEBOUNCE = 40; const unsigned long LONG_PRESS_TIME = 600; const unsigned long VERY_LONG_PRESS_TIME = 2000; const unsigned long RAIN_ANIMATION_INTERVAL = 90; const unsigned long CLOCK_UPDATE_INTERVAL = 1000; // ===== TFT ===== DIYables_TFT_GC9A01_Round tft(PIN_RST, PIN_DC, PIN_CS); // ===== SCREEN ===== const int SCREEN_W = 240; const int SCREEN_H = 240; const int CENTER_X = 120; const int CENTER_Y = 120; // ===== COLORS ===== #define BLACK DIYables_TFT::colorRGB(0, 0, 0) #define WHITE DIYables_TFT::colorRGB(255, 255, 255) #define CYAN DIYables_TFT::colorRGB(0, 220, 255) #define BLUE DIYables_TFT::colorRGB(40, 120, 255) #define LIGHT_BLUE DIYables_TFT::colorRGB(90, 190, 255) #define GREEN DIYables_TFT::colorRGB(60, 230, 130) #define YELLOW DIYables_TFT::colorRGB(255, 210, 60) #define ORANGE DIYables_TFT::colorRGB(255, 150, 40) #define RED DIYables_TFT::colorRGB(255, 70, 70) #define PURPLE DIYables_TFT::colorRGB(180, 100, 255) #define GRAY DIYables_TFT::colorRGB(125, 125, 135) #define DARK_GRAY DIYables_TFT::colorRGB(42, 42, 52) #define DARK_BLUE DIYables_TFT::colorRGB(12, 18, 48) // ===== PAGE ===== enum Page { PAGE_HOME = 0, PAGE_TEMP, PAGE_HUMIDITY, PAGE_WIND, PAGE_RAIN, PAGE_HOURLY, PAGE_FORECAST, PAGE_SYSTEM, PAGE_COUNT }; Page currentPage = PAGE_HOME; // ===== WEATHER DATA ===== // The Arduino UNO R4 has only 32 KB of RAM, so short text is // kept in fixed char arrays instead of String objects. struct WeatherData { bool valid = false; float temperature = 0; float humidity = 0; float feelsLike = 0; float windSpeed = 0; float windDirection = 0; float pressure = 0; int weatherCode = 0; bool isDay = true; int currentRainChance = 0; float uvIndex = 0; // "HH:MM" char sunrise[6] = "--:--"; char sunset[6] = "--:--"; // NEXT HOURS float hourlyTemp[12]; int hourlyRainChance[12]; int hourlyWeatherCode[12]; // "HH:MM" char hourlyTime[12][6]; // DAILY // "YYYY-MM-DD" char dailyDate[3][11]; float dailyTempMax[3]; float dailyTempMin[3]; int dailyRainChance[3]; int dailyWeatherCode[3]; float dailyUV[3]; }; WeatherData weather; // ===== SOFTWARE CLOCK ===== // The Arduino UNO R4 has no NTP client in this sketch. // Instead, the local time that comes with the weather answer // is stored, then millis() moves the clock forward. // Seconds after midnight at the moment of the last answer. long clockBaseSecond = -1; unsigned long clockBaseMillis = 0; // ===== SYSTEM STATE ===== bool apiOnline = false; bool wifiOnline = false; bool screenDirty = true; bool isFetching = false; bool pendingRefresh = false; unsigned long lastApiUpdate = 0; unsigned long lastWifiRetry = 0; unsigned long wifiConnectStart = 0; unsigned long lastRainAnimation = 0; unsigned long lastClockUpdate = 0; bool wifiConnecting = false; // ===== BUTTON ===== // ezButton handles the debouncing, so the sketch only has to // measure how long the button was held down. // // The line below is for a BARE push button wired to GND. ezButton then // uses the internal pull-up resistor of the board, and the pin reads LOW // while the button is held down. ezButton button(PIN_BUTTON); // If you use a BUTTON MODULE instead, the module carries its own resistor // on the board, so the internal one must not be used. Comment the line // above and use this line instead: // ezButton button(PIN_BUTTON, EXTERNAL_PULLDOWN); unsigned long buttonPressStart = 0; // ===== RAIN ANIMATION ===== const int RAIN_DROP_COUNT = 16; int rainX[RAIN_DROP_COUNT]; int rainY[RAIN_DROP_COUNT]; bool rainInitialized = false; const int RAIN_AREA_X = 30; const int RAIN_AREA_Y = 125; const int RAIN_AREA_W = 180; const int RAIN_AREA_H = 78; // ===== WEATHER DESCRIPTION ===== String weatherDescription(int code) { switch (code) { case 0: return "CLEAR"; case 1: return "MAINLY CLEAR"; case 2: return "PARTLY CLOUDY"; case 3: return "OVERCAST"; case 45: case 48: return "FOG"; case 51: case 53: case 55: return "DRIZZLE"; case 56: case 57: return "FREEZING DRIZZLE"; case 61: case 63: case 65: return "RAIN"; case 66: case 67: return "FREEZING RAIN"; case 71: case 73: case 75: return "SNOW"; case 77: return "SNOW GRAINS"; case 80: case 81: case 82: return "SHOWERS"; case 85: case 86: return "SNOW SHOWERS"; case 95: return "THUNDERSTORM"; case 96: case 99: return "STORM"; default: return "UNKNOWN"; } } // ===== WEATHER COLOR ===== uint16_t weatherColor(int code) { if (code == 0) return YELLOW; if (code == 1 || code == 2) return CYAN; if (code == 3) return GRAY; if (code >= 51 && code <= 67) return BLUE; if (code >= 80 && code <= 82) return LIGHT_BLUE; if (code >= 95) return RED; return WHITE; } // ===== WIND DIRECTION ===== String windDirectionText(float degrees) { const char* directions[] = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; int index = (int)((degrees + 22.5) / 45.0); index %= 8; return directions[index]; } // ===== CENTER TEXT ===== void drawCenteredText(String text, int y, int size, uint16_t color) { tft.setTextSize(size); tft.setTextColor(color); int width = text.length() * 6 * size; int x = CENTER_X - width / 2; if (x < 0) x = 0; tft.setCursor(x, y); tft.print(text); } // ===== HEADER ===== void drawHeader(String title, uint16_t color) { // Safe radius from screen center (slightly less than the // physical 120px radius, to keep a small margin from the // round bezel). const float SAFE_RADIUS = 116.0; int size = 2; int width = title.length() * 6 * size; int half = width / 2; int y; if (half >= SAFE_RADIUS) { // Too wide even at the lowest safe position -> use a // smaller font instead of clipping. size = 1; width = title.length() * 6 * size; half = width / 2; y = 14; } else { // Compute the minimum y (top of text) so the top corners // of the text stay inside the round visible area. float insideSpan = sqrt((SAFE_RADIUS * SAFE_RADIUS) - ((float)half * (float)half)); int minY = (int)(120.0 - insideSpan); y = (minY > 14) ? minY : 14; } drawCenteredText(title, y, size, color); int lineY = y + (size == 2 ? 16 : 8) + 6; tft.drawLine(45, lineY, 195, lineY, DARK_GRAY); } // ===== FOOTER ===== void drawFooter() { tft.drawLine(55, 214, 185, 214, DARK_GRAY); drawCenteredText(String(currentPage + 1) + "/" + String(PAGE_COUNT), 220, 1, GRAY); } // ===== SUN ICON ===== void drawSun(int x, int y, int radius) { tft.fillCircle(x, y, radius, YELLOW); for (int i = 0; i < 8; i++) { float angle = i * PI / 4.0; int x1 = x + cos(angle) * (radius + 7); int y1 = y + sin(angle) * (radius + 7); int x2 = x + cos(angle) * (radius + 14); int y2 = y + sin(angle) * (radius + 14); tft.drawLine(x1, y1, x2, y2, YELLOW); } } // ===== CLOUD ICON ===== void drawCloud(int x, int y) { tft.fillCircle(x - 20, y, 15, LIGHT_BLUE); tft.fillCircle(x, y - 8, 20, LIGHT_BLUE); tft.fillCircle(x + 20, y, 15, LIGHT_BLUE); tft.fillRoundRect(x - 35, y, 70, 20, 10, LIGHT_BLUE); } // ===== STATIC RAIN ICON ===== void drawRainIcon(int x, int y) { drawCloud(x, y); for (int i = 0; i < 5; i++) { int dx = -24 + i * 12; int dy = 28; tft.drawLine(x + dx, y + dy, x + dx - 4, y + dy + 10, BLUE); } } // ===== WEATHER ICON ===== void drawWeatherIcon(int code, int x, int y) { if (code == 0) { drawSun(x, y, 18); } else if (code <= 2) { drawSun(x - 12, y - 7, 13); drawCloud(x + 10, y + 10); } else if (code == 3) { drawCloud(x, y); } else if ((code >= 51 && code <= 67) || (code >= 80 && code <= 82)) { drawRainIcon(x, y); } else if (code >= 95) { drawCloud(x, y); tft.drawLine(x - 5, y + 28, x - 12, y + 43, YELLOW); tft.drawLine(x + 10, y + 28, x + 3, y + 43, YELLOW); } else { drawCloud(x, y); } } void drawWifiIcon(int cx, int cy, int level) { const int radii[3] = { 14, 27, 40 }; for (int arc = 0; arc < 3; arc++) { uint16_t color = (arc < level) ? CYAN : DARK_GRAY; for (int deg = -150; deg <= -30; deg += 5) { float angle = deg * PI / 180.0; int x = cx + (int)(cos(angle) * radii[arc]); int y = cy + (int)(sin(angle) * radii[arc]); tft.fillCircle(x, y, 2, color); } } // Base dot (the "device") tft.fillCircle(cx, cy, 5, CYAN); } // ===== PAGE 1 - HOME ===== void drawHome() { uint16_t bg = weather.isDay ? BLACK : DARK_BLUE; tft.fillScreen(bg); if (!weather.valid) { drawCenteredText("NO DATA", 90, 3, RED); drawCenteredText("CHECK CONNECTION", 135, 1, WHITE); drawFooter(); return; } // Weather icon drawWeatherIcon(weather.weatherCode, CENTER_X, 65); // Temperature drawCenteredText(String(weather.temperature, 1) + " C", 112, 3, WHITE); // Location drawCenteredText(LOCATION_NAME, 153, 2, CYAN); // Weather condition drawCenteredText(weatherDescription(weather.weatherCode), 178, 1, weatherColor(weather.weatherCode)); // Clock area drawHomeClock(); drawFooter(); } // ===== HOME CLOCK ===== void drawHomeClock() { uint16_t bg = weather.isDay ? BLACK : DARK_BLUE; // Only clear a small region. // This prevents full-screen flicker. tft.fillRect(82, 196, 76, 12, bg); drawCenteredText(getTimeString(), 197, 1, GRAY); } // ===== PAGE 2 - TEMPERATURE ===== void drawTemperature() { tft.fillScreen(BLACK); drawHeader("TEMPERATURE", ORANGE); int cx = 120; int cy = 125; int radius = 68; int value = constrain((int)weather.temperature, 0, 40); int filled = map(value, 0, 40, 0, 240); // Temperature gauge for (int i = 0; i < 240; i += 4) { float angle = (-135.0 + i) * PI / 180.0; int x = cx + cos(angle) * radius; int y = cy + sin(angle) * radius; uint16_t color = (i < filled) ? ORANGE : DARK_GRAY; tft.fillCircle(x, y, 2, color); } // Main value drawCenteredText(String(weather.temperature, 1), 101, 3, WHITE); drawCenteredText("C", 138, 2, ORANGE); drawCenteredText("FEELS " + String(weather.feelsLike, 1) + " C", 168, 1, CYAN); drawFooter(); } // ===== PAGE 3 - HUMIDITY ===== void drawHumidity() { tft.fillScreen(BLACK); drawHeader("HUMIDITY", BLUE); int cx = 120; int cy = 125; int radius = 68; int progress = constrain((int)weather.humidity, 0, 100); int filled = map(progress, 0, 100, 0, 270); for (int i = 0; i < 270; i += 3) { float angle = (-135.0 + i) * PI / 180.0; int x = cx + cos(angle) * radius; int y = cy + sin(angle) * radius; uint16_t color = (i < filled) ? BLUE : DARK_GRAY; tft.fillCircle(x, y, 3, color); } drawCenteredText(String((int)weather.humidity) + "%", 105, 3, WHITE); String comfort; if (weather.humidity < 40) comfort = "DRY"; else if (weather.humidity < 70) comfort = "COMFORTABLE"; else if (weather.humidity < 85) comfort = "HUMID"; else comfort = "VERY HUMID"; drawCenteredText(comfort, 153, 1, CYAN); drawFooter(); } // ===== PAGE 4 - WIND ===== void drawWind() { tft.fillScreen(BLACK); drawHeader("WIND", GREEN); int cx = 120; int cy = 104; int radius = 49; // Compass tft.drawCircle(cx, cy, radius, DARK_GRAY); drawCenteredText("N", 43, 1, WHITE); drawCenteredText("S", 157, 1, WHITE); tft.setCursor(62, 101); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print("W"); tft.setCursor(172, 101); tft.print("E"); // Direction arrow float angle = weather.windDirection * PI / 180.0; int x2 = cx + sin(angle) * 38; int y2 = cy - cos(angle) * 38; tft.drawLine(cx, cy, x2, y2, GREEN); tft.fillCircle(cx, cy, 5, GREEN); // Wind speed drawCenteredText(String(weather.windSpeed, 1) + " km/h", 174, 2, WHITE); drawCenteredText(windDirectionText(weather.windDirection), 198, 1, CYAN); drawFooter(); } // ===== PAGE 5 - RAIN ===== void drawRain() { tft.fillScreen(BLACK); drawHeader("RAIN", LIGHT_BLUE); int rain = weather.currentRainChance; // Static weather icon if (rain >= 60) { drawRainIcon(CENTER_X, 68); } else { drawCloud(CENTER_X, 73); } // Rain percentage drawCenteredText(String(rain) + "%", 105, 3, WHITE); drawCenteredText("RAIN CHANCE", 140, 1, LIGHT_BLUE); // Status if (rain >= 60) { drawCenteredText("HIGH", 158, 1, RED); } else if (rain >= 30) { drawCenteredText("MODERATE", 158, 1, YELLOW); } else { drawCenteredText("LOW", 158, 1, GREEN); } // Animation area if (rain >= 20) { initializeRainAnimation(); } drawFooter(); } // ===== INITIALIZE RAIN ANIMATION ===== void initializeRainAnimation() { for (int i = 0; i < RAIN_DROP_COUNT; i++) { rainX[i] = RAIN_AREA_X + random(RAIN_AREA_W); rainY[i] = RAIN_AREA_Y + random(RAIN_AREA_H); } rainInitialized = true; } // ===== UPDATE RAIN ANIMATION ===== void updateRainAnimation() { if (currentPage != PAGE_RAIN) return; if (weather.currentRainChance < 20) return; if (millis() - lastRainAnimation < RAIN_ANIMATION_INTERVAL) { return; } lastRainAnimation = millis(); uint16_t bg = BLACK; // Erase old drops for (int i = 0; i < RAIN_DROP_COUNT; i++) { tft.drawLine(rainX[i], rainY[i], rainX[i] - 3, rainY[i] + 7, bg); } // Move drops for (int i = 0; i < RAIN_DROP_COUNT; i++) { rainY[i] += 5; if (rainY[i] > RAIN_AREA_Y + RAIN_AREA_H) { rainX[i] = RAIN_AREA_X + random(RAIN_AREA_W); rainY[i] = RAIN_AREA_Y; } tft.drawLine(rainX[i], rainY[i], rainX[i] - 3, rainY[i] + 7, BLUE); } } // ===== PAGE 6 - NEXT HOURS ===== void drawHourly() { tft.fillScreen(BLACK); drawHeader("NEXT HOURS", PURPLE); for (int i = 0; i < 5; i++) { int y = 52 + i * 31; // Row separator if (i > 0) { tft.drawLine(25, y - 8, 215, y - 8, DARK_GRAY); } // Time tft.setCursor(25, y); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print(weather.hourlyTime[i]); // Weather int code = weather.hourlyWeatherCode[i]; String condition; if (code == 0) condition = "SUN"; else if (code <= 3) condition = "CLOUD"; else if (code >= 95) condition = "STORM"; else condition = "RAIN"; tft.setCursor(75, y); tft.setTextColor(weatherColor(code)); tft.print(condition); // Temperature tft.setCursor(142, y); tft.setTextColor(WHITE); tft.print(weather.hourlyTemp[i], 0); tft.print("C"); // Rain tft.setCursor(180, y); tft.setTextColor(LIGHT_BLUE); tft.print(weather.hourlyRainChance[i]); tft.print("%"); } drawFooter(); } // ===== PAGE 7 - FORECAST ===== // UI FIX: // The card border (drawRoundRect) previously spanned x=15..225 // (width 210). For the TOP row, the corners of that rectangle // fell outside the visible round area of the screen and were // clipped. The card width below was reduced to 190px // (x=25..215) which keeps every corner, on every row, safely // inside the round bezel. Text start x was nudged in slightly // to match the new card padding. void drawForecast() { tft.fillScreen(BLACK); drawHeader("3-DAY FORECAST", CYAN); const char* labels[] = { "TODAY", "TOMORROW", "DAY 3" }; // Card geometry (UI fix: narrower so it never touches // the round bezel, even on the top row). const int CARD_X = 25; const int CARD_W = 190; for (int i = 0; i < 3; i++) { int y = 62 + i * 48; // Card tft.drawRoundRect(CARD_X, y - 5, CARD_W, 40, 8, DARK_GRAY); // Day tft.setCursor(CARD_X + 8, y + 4); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print(labels[i]); // Temperature tft.setCursor(CARD_X + 65, y + 4); tft.print(weather.dailyTempMin[i], 0); tft.print("/"); tft.print(weather.dailyTempMax[i], 0); tft.print("C"); // Rain tft.setCursor(CARD_X + 120, y + 4); tft.setTextColor(LIGHT_BLUE); tft.print(weather.dailyRainChance[i]); tft.print("%"); // Weather tft.setCursor(CARD_X + 155, y + 4); tft.setTextColor(weatherColor(weather.dailyWeatherCode[i])); int code = weather.dailyWeatherCode[i]; if (code == 0) tft.print("SUN"); else if (code <= 3) tft.print("CLD"); else if (code >= 95) tft.print("STM"); else tft.print("RAIN"); } drawFooter(); } // ===== PAGE 8 - SYSTEM ===== void drawSystem() { tft.fillScreen(BLACK); drawHeader("SYSTEM", CYAN); // WIFI tft.setCursor(25, 57); tft.setTextSize(1); tft.setTextColor(WHITE); tft.print("WIFI"); tft.setCursor(125, 57); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(GREEN); tft.print("ONLINE"); } else { tft.setTextColor(RED); tft.print("OFFLINE"); } // RSSI tft.setCursor(25, 82); tft.setTextColor(WHITE); tft.print("RSSI"); tft.setCursor(125, 82); if (WiFi.status() == WL_CONNECTED) { tft.setTextColor(CYAN); tft.print(WiFi.RSSI()); tft.print(" dBm"); } else { tft.print("--"); } // API tft.setCursor(25, 107); tft.setTextColor(WHITE); tft.print("API"); tft.setCursor(125, 107); if (apiOnline) { tft.setTextColor(GREEN); tft.print("ONLINE"); } else { tft.setTextColor(RED); tft.print("OFFLINE"); } // LOCATION tft.setCursor(25, 132); tft.setTextColor(WHITE); tft.print("LOCATION"); tft.setCursor(125, 132); tft.setTextColor(CYAN); tft.print(LOCATION_NAME); // TEMP tft.setCursor(25, 157); tft.setTextColor(WHITE); tft.print("TEMP"); tft.setCursor(125, 157); tft.print(weather.temperature, 1); tft.print(" C"); // STATUS tft.setCursor(25, 182); tft.setTextColor(WHITE); tft.print("STATUS"); tft.setCursor(125, 182); if (weather.valid) { tft.setTextColor(GREEN); tft.print("READY"); } else { tft.setTextColor(RED); tft.print("NO DATA"); } drawFooter(); } // ===== RENDER PAGE ===== void renderPage() { // Rain animation must be initialized // only when entering the page. rainInitialized = false; switch (currentPage) { case PAGE_HOME: drawHome(); break; case PAGE_TEMP: drawTemperature(); break; case PAGE_HUMIDITY: drawHumidity(); break; case PAGE_WIND: drawWind(); break; case PAGE_RAIN: drawRain(); break; case PAGE_HOURLY: drawHourly(); break; case PAGE_FORECAST: drawForecast(); break; case PAGE_SYSTEM: drawSystem(); break; } } // ===== WEATHER API PATH ===== // ArduinoHttpClient keeps the host name apart from the path, // so only the part after the host name is built here. String getWeatherPath() { String path = "/v1/forecast"; path += "?latitude=" + String(LATITUDE, 4); path += "&longitude=" + String(LONGITUDE, 4); path += "&current=" "temperature_2m," "relative_humidity_2m," "apparent_temperature," "weather_code," "wind_speed_10m," "wind_direction_10m," "pressure_msl," "is_day"; path += "&hourly=" "temperature_2m," "precipitation_probability," "weather_code"; path += "&daily=" "temperature_2m_max," "temperature_2m_min," "precipitation_probability_max," "weather_code," "sunrise," "sunset," "uv_index_max"; path += "&timezone=" TIMEZONE; path += "&forecast_days=3"; // Only the next 12 hours are asked for. // The answer then starts at the current hour, and it stays // small enough for the 32 KB RAM of the Arduino UNO R4. path += "&forecast_hours=12"; path += "&temperature_unit=celsius"; path += "&wind_speed_unit=kmh"; return path; } // ===== COPY "HH:MM" OUT OF AN ISO TIME TEXT ===== // Input : 2026-09-18T14:00 // Output: 14:00 void copyClockText(const char* isoTime, char* out) { if (isoTime != NULL && strlen(isoTime) >= 16) { out[0] = isoTime[11]; out[1] = isoTime[12]; out[2] = ':'; out[3] = isoTime[14]; out[4] = isoTime[15]; out[5] = '\0'; return; } strcpy(out, "--:--"); } // ===== SET THE SOFTWARE CLOCK FROM AN ISO TIME TEXT ===== void setClockFrom(const char* isoTime) { if (isoTime == NULL || strlen(isoTime) < 16) { return; } int hour = (isoTime[11] - '0') * 10 + (isoTime[12] - '0'); int minute = (isoTime[14] - '0') * 10 + (isoTime[15] - '0'); if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { return; } clockBaseSecond = (long)hour * 3600L + (long)minute * 60L; clockBaseMillis = millis(); } // ===== PARSE WEATHER ===== bool parseWeather(const String& payload) { // A filter keeps only the three blocks that are used. // The "..._units" blocks of the answer are dropped, so the // document stays small in the RAM of the Arduino UNO R4. JsonDocument filter; filter["current"] = true; filter["hourly"] = true; filter["daily"] = true; JsonDocument doc; DeserializationError error = deserializeJson(doc, payload, DeserializationOption::Filter(filter)); if (error) { Serial.print("JSON ERROR: "); Serial.println(error.c_str()); return false; } // CURRENT JsonObject current = doc["current"]; if (current.isNull()) { Serial.println("CURRENT DATA MISSING"); return false; } weather.temperature = current["temperature_2m"] | 0.0; weather.humidity = current["relative_humidity_2m"] | 0.0; weather.feelsLike = current["apparent_temperature"] | 0.0; weather.weatherCode = current["weather_code"] | 0; weather.windSpeed = current["wind_speed_10m"] | 0.0; weather.windDirection = current["wind_direction_10m"] | 0.0; weather.pressure = current["pressure_msl"] | 0.0; int isDay = current["is_day"] | 1; weather.isDay = (isDay == 1); // The answer carries the local time of the location. // It is used to start the software clock. const char* currentTime = current["time"] | ""; setClockFrom(currentTime); // HOURLY // "forecast_hours=12" makes the list start at the current // hour, so index 0 is now and index 11 is 11 hours later. JsonArray hourlyTime = doc["hourly"]["time"]; JsonArray hourlyTemp = doc["hourly"]["temperature_2m"]; JsonArray hourlyRain = doc["hourly"]["precipitation_probability"]; JsonArray hourlyCode = doc["hourly"]["weather_code"]; for (int i = 0; i < 12; i++) { // Safety check if (i >= (int)hourlyTemp.size()) { weather.hourlyTemp[i] = weather.temperature; weather.hourlyRainChance[i] = 0; weather.hourlyWeatherCode[i] = weather.weatherCode; strcpy(weather.hourlyTime[i], "--:--"); continue; } weather.hourlyTemp[i] = hourlyTemp[i] | weather.temperature; weather.hourlyRainChance[i] = hourlyRain[i] | 0; weather.hourlyWeatherCode[i] = hourlyCode[i] | weather.weatherCode; copyClockText(hourlyTime[i] | "", weather.hourlyTime[i]); } weather.currentRainChance = weather.hourlyRainChance[0]; // DAILY JsonArray dailyMax = doc["daily"]["temperature_2m_max"]; JsonArray dailyMin = doc["daily"]["temperature_2m_min"]; JsonArray dailyRain = doc["daily"]["precipitation_probability_max"]; JsonArray dailyCode = doc["daily"]["weather_code"]; JsonArray dailyDate = doc["daily"]["time"]; JsonArray dailySunrise = doc["daily"]["sunrise"]; JsonArray dailySunset = doc["daily"]["sunset"]; JsonArray dailyUV = doc["daily"]["uv_index_max"]; for (int i = 0; i < 3; i++) { weather.dailyTempMax[i] = dailyMax[i] | 0.0; weather.dailyTempMin[i] = dailyMin[i] | 0.0; weather.dailyRainChance[i] = dailyRain[i] | 0; weather.dailyWeatherCode[i] = dailyCode[i] | 0; weather.dailyUV[i] = dailyUV[i] | 0.0; const char* dateText = dailyDate[i] | ""; strncpy(weather.dailyDate[i], dateText, 10); weather.dailyDate[i][10] = '\0'; } if (dailySunrise.size() > 0) { copyClockText(dailySunrise[0] | "", weather.sunrise); } if (dailySunset.size() > 0) { copyClockText(dailySunset[0] | "", weather.sunset); } if (dailyUV.size() > 0) { weather.uvIndex = dailyUV[0] | 0.0; } weather.valid = true; return true; } // ===== FETCH WEATHER ===== bool fetchWeather() { if (WiFi.status() != WL_CONNECTED) { Serial.println("WIFI NOT CONNECTED"); apiOnline = false; screenDirty = true; return false; } Serial.println(); Serial.println("================================"); Serial.println("FETCHING WEATHER"); Serial.println("================================"); isFetching = true; // Loading screen is shown only once. drawLoadingScreen("FETCHING WEATHER..."); // WiFiSSLClient does the HTTPS part on the Arduino UNO R4. // The root certificates live in the WiFi firmware, so there // is no certificate to paste into this sketch. WiFiSSLClient sslClient; HttpClient http(sslClient, API_HOST, API_PORT); http.setTimeout(15000); http.setHttpResponseTimeout(15000); String path = getWeatherPath(); Serial.print("GET https://"); Serial.print(API_HOST); Serial.println(path); int error = http.get(path); if (error != 0) { Serial.print("HTTP REQUEST FAILED: "); Serial.println(error); http.stop(); isFetching = false; apiOnline = false; screenDirty = true; return false; } int statusCode = http.responseStatusCode(); Serial.print("HTTP CODE: "); Serial.println(statusCode); if (statusCode != 200) { Serial.println("API ERROR"); http.stop(); isFetching = false; apiOnline = false; screenDirty = true; return false; } // responseBody() also takes care of a chunked answer. String payload = http.responseBody(); http.stop(); Serial.print("PAYLOAD SIZE: "); Serial.println(payload.length()); bool result = parseWeather(payload); // The text is not needed any more, so the RAM is given back // before the screen is drawn. payload = ""; isFetching = false; if (result) { apiOnline = true; lastApiUpdate = millis(); Serial.println("WEATHER UPDATED"); Serial.print("Temperature: "); Serial.println(weather.temperature); Serial.print("Humidity: "); Serial.println(weather.humidity); Serial.print("Weather: "); Serial.println(weatherDescription(weather.weatherCode)); Serial.print("Wind: "); Serial.println(weather.windSpeed); Serial.print("Rain Chance: "); Serial.println(weather.currentRainChance); screenDirty = true; return true; } apiOnline = false; screenDirty = true; return false; } // ===== LOADING SCREEN ===== void drawLoadingScreen(String message) { tft.fillScreen(BLACK); drawCenteredText("SMART WEATHER", 48, 2, CYAN); drawCenteredText("STATION", 73, 2, WHITE); // Loading ring for (int i = 0; i < 12; i++) { float angle = i * 2.0 * PI / 12.0; int x = CENTER_X + cos(angle) * 42; int y = CENTER_Y + sin(angle) * 42; uint16_t color = (i == 0) ? CYAN : DARK_GRAY; tft.fillCircle(x, y, 4, color); } drawCenteredText(message, 182, 1, WHITE); } // ===== WIFI CONNECT ===== bool connectWiFi() { Serial.println(); Serial.println("CONNECTING TO WIFI..."); tft.fillScreen(BLACK); drawCenteredText("CONNECTING", 26, 2, CYAN); drawWifiIcon(CENTER_X, 95, 0); drawCenteredText("WI-FI", 145, 2, WHITE); drawCenteredText(WIFI_SSID, 178, 1, GRAY); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); unsigned long start = millis(); int iconFrame = 0; while (WiFi.status() != WL_CONNECTED && millis() - start < WIFI_CONNECT_TIMEOUT) { drawWifiIcon(CENTER_X, 95, iconFrame % 4); iconFrame++; delay(250); Serial.print("."); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { wifiOnline = true; wifiConnecting = false; drawWifiIcon(CENTER_X, 95, 3); Serial.println("WIFI CONNECTED"); Serial.print("IP: "); Serial.println(WiFi.localIP()); Serial.print("RSSI: "); Serial.print(WiFi.RSSI()); Serial.println(" dBm"); return true; } wifiOnline = false; Serial.println("WIFI CONNECTION FAILED"); return false; } // ===== START WIFI RECONNECT ===== void startWiFiReconnect() { Serial.println("START WIFI RECONNECT"); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); wifiConnecting = true; wifiConnectStart = millis(); } // ===== HANDLE WIFI ===== void handleWiFi() { int status = WiFi.status(); // Connected if (status == WL_CONNECTED) { if (!wifiOnline) { Serial.println("WIFI CONNECTED"); screenDirty = true; } wifiOnline = true; wifiConnecting = false; return; } wifiOnline = false; // Currently connecting if (wifiConnecting) { if (millis() - wifiConnectStart > WIFI_CONNECT_TIMEOUT) { Serial.println("WIFI CONNECT TIMEOUT"); WiFi.disconnect(); wifiConnecting = false; lastWifiRetry = millis(); } return; } // Start another attempt if (millis() - lastWifiRetry >= WIFI_RETRY_INTERVAL) { lastWifiRetry = millis(); startWiFiReconnect(); } } // ===== CHECK THE WIFI MODULE ===== bool checkWiFiModule() { if (WiFi.status() == WL_NO_MODULE) { Serial.println("WIFI MODULE NOT FOUND"); Serial.println("Use an Arduino UNO R4 WiFi board."); return false; } Serial.print("WIFI FIRMWARE: "); Serial.println(WiFi.firmwareVersion()); return true; } // ===== BUTTON HANDLER ===== void handleButton() { button.loop(); // MUST call the loop() function first // The moment the button goes down, remember when it happened. if (button.isPressed()) buttonPressStart = millis(); // The action is chosen when the button comes back up. if (button.isReleased()) { unsigned long duration = millis() - buttonPressStart; if (duration >= VERY_LONG_PRESS_TIME) { // VERY LONG PRESS -> back to the home page currentPage = PAGE_HOME; screenDirty = true; Serial.println("VERY LONG PRESS -> HOME"); } else if (duration >= LONG_PRESS_TIME) { // LONG PRESS -> ask the weather API for fresh data pendingRefresh = true; Serial.println("LONG PRESS -> REFRESH"); } else { // SHORT PRESS -> next page currentPage = (Page)((currentPage + 1) % PAGE_COUNT); screenDirty = true; Serial.print("SHORT PRESS -> PAGE "); Serial.println(currentPage + 1); } } } // ===== PROCESS MANUAL REFRESH ===== void handleManualRefresh() { if (!pendingRefresh) return; pendingRefresh = false; fetchWeather(); } // ===== AUTO API UPDATE ===== void handleApiUpdate() { if (!weather.valid) { return; } if (millis() - lastApiUpdate >= API_UPDATE_INTERVAL) { fetchWeather(); } } // ===== CLOCK ===== // The clock starts from the local time that came with the // weather answer, then millis() moves it forward. String getTimeString() { if (clockBaseSecond < 0) { return "--:--"; } unsigned long elapsed = (millis() - clockBaseMillis) / 1000UL; long nowSecond = clockBaseSecond + (long)elapsed; // Wrap around at midnight. nowSecond %= 86400L; int hour = nowSecond / 3600L; int minute = (nowSecond % 3600L) / 60L; char buffer[6]; sprintf(buffer, "%02d:%02d", hour, minute); return String(buffer); } // ===== HANDLE CLOCK ===== void handleClock() { if (currentPage != PAGE_HOME) { return; } if (millis() - lastClockUpdate < CLOCK_UPDATE_INTERVAL) { return; } lastClockUpdate = millis(); if (weather.valid && !isFetching) { drawHomeClock(); } } // ===== SETUP ===== void setup() { Serial.begin(9600); delay(500); Serial.println(); Serial.println("================================"); Serial.println("SMART WEATHER STATION"); Serial.println(LOCATION_NAME); Serial.println("================================"); // BUTTON // ezButton already set the pin to INPUT_PULLUP in its constructor. button.setDebounceTime(BUTTON_DEBOUNCE); // TFT tft.begin(); tft.setRotation(2); tft.fillScreen(BLACK); // BOOT SCREEN drawCenteredText("SMART WEATHER", 65, 2, CYAN); drawCenteredText("STATION", 92, 2, WHITE); drawCenteredText(LOCATION_NAME, 135, 2, YELLOW); delay(1500); // WIFI if (!checkWiFiModule()) { tft.fillScreen(BLACK); drawCenteredText("NO WIFI MODULE", 100, 2, RED); drawCenteredText("NEED UNO R4 WIFI", 135, 1, WHITE); // Nothing else can be done without the WiFi module. while (true) { delay(1000); } } if (connectWiFi()) { delay(500); fetchWeather(); } else { tft.fillScreen(BLACK); drawCenteredText("WIFI ERROR", 82, 2, RED); drawCenteredText("CHECK SETTINGS", 120, 1, WHITE); drawCenteredText("RETRYING...", 150, 1, GRAY); delay(1500); } screenDirty = true; } // ===== LOOP ===== void loop() { // Input handleButton(); // WiFi handleWiFi(); // Manual refresh handleManualRefresh(); // Automatic API update handleApiUpdate(); // Clock handleClock(); // Static page rendering if (screenDirty && !isFetching) { renderPage(); screenDirty = false; } // Rain animation updateRainAnimation(); }

빠른 단계

다음 지침을 단계별로 따라 하세요:

  • Arduino Uno R4 WiFi를 처음 사용한다면 아두이노 우노 R4 - 소프트웨어 설치 튜토리얼을 참고하세요.
  • 제공된 배선도에 따라 부품을 연결합니다.
  • USB 케이블로 Arduino Uno R4 보드를 컴퓨터에 연결합니다.
  • 컴퓨터에서 Arduino IDE를 실행합니다.
  • 알맞은 Arduino Uno R4 보드(Arduino Uno R4 WiFi)와 COM 포트를 선택합니다.
  • Arduino IDE 왼쪽 막대에 있는 Libraries 아이콘으로 이동합니다.
  • “DIYables TFT Round”를 검색한 다음, DIYables에서 만든 DIYables_TFT_Round 라이브러리를 찾습니다.
  • Install 버튼을 클릭하여 라이브러리를 설치합니다.
아두이노 우노 R4 tft lcd 라이브러리 설치
  • 추가 라이브러리 의존성을 설치하라는 메시지가 나타날 수 있습니다.
  • Install All 버튼을 클릭하여 모든 라이브러리 의존성을 설치합니다.
아두이노 우노 R4 tft lcd 의존성 설치
  • “ArduinoJson”을 검색한 다음, Benoit Blanchon이 만든 ArduinoJson 라이브러리를 찾아 Install을 클릭합니다.
  • “ArduinoHttpClient”를 검색한 다음, Arduino에서 만든 ArduinoHttpClient 라이브러리를 찾아 Install을 클릭합니다.
  • “ezButton”을 검색한 다음, ArduinoGetStarted에서 만든 ezButton 라이브러리를 찾아 Install을 클릭합니다.
  • 위의 코드를 복사하여 Arduino IDE 편집기에 붙여넣습니다.
  • WIFI_SSID와 WIFI_PASSWORD를 여러분의 WiFi 이름과 비밀번호로 바꿉니다.
  • LATITUDE, LONGITUDE, LOCATION_NAME, TIMEZONE을 여러분의 도시로 바꿉니다. 아래 섹션을 참고하세요.
  • Arduino IDE에서 Upload 버튼을 클릭하여 코드를 Arduino UNO R4에 업로드합니다.
  • Arduino IDE에서 시리얼 모니터를 열고 보드레이트를 9600으로 설정합니다.
  • 몇 초 기다립니다. 디스플레이에 부팅 화면, WiFi 화면, 그리고 날씨가 차례로 표시됩니다.
  • 버튼을 눌러 다음 페이지로 이동합니다.
  • 시리얼 모니터에서 결과를 확인합니다.
∞
Newbiely | Arduino IDE 2.3.8
──
☐
✕
File
Edit
Sketch
Tools
Help
Arduino Uno R4 WiFi
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'Arduino Uno R4 WiFi' on 'COM15')
New Line
9600 baud
================================ SMART WEATHER STATION SEOUL ================================ WIFI FIRMWARE: 0.5.0 CONNECTING TO WIFI... ..... WIFI CONNECTED IP: 192.168.0.24 RSSI: -47 dBm ================================ FETCHING WEATHER ================================ GET https://api.open-meteo.com/v1/forecast?latitude=37.5665&longitude=126.9780¤t=... HTTP CODE: 200 PAYLOAD SIZE: 1754 WEATHER UPDATED Temperature: 24.60 Humidity: 55.00 Weather: PARTLY CLOUDY Wind: 9.30 Rain Chance: 21 SHORT PRESS -> PAGE 2 SHORT PRESS -> PAGE 3 LONG PRESS -> REFRESH VERY LONG PRESS -> HOME
Ln 11, Col 1
Arduino Uno R4 WiFi on COM15
2

버튼 사용 방법

버튼 하나가 세 가지 일을 합니다. 차이는 얼마나 오래 누르고 있느냐입니다:

  • 짧게 누르기 (0.6초 미만): 다음 페이지로 이동합니다. 마지막 페이지 다음에는 홈 페이지로 돌아갑니다.
  • 길게 누르기 (0.6초 ~ 2초): 지금 바로 날씨 API에 새 데이터를 요청합니다.
  • 아주 길게 누르기 (2초 초과): 홈 페이지로 바로 돌아갑니다.

코드는 버튼에 ezButton 라이브러리를 사용합니다. 이 라이브러리가 접점 바운스를 알아서 제거하므로 한 번 누른 것이 두 번으로 세어지는 일이 없으며, 스케치는 버튼을 얼마나 오래 눌렀는지만 측정하면 됩니다.

ezButton button(PIN_BUTTON);

setup()에서 디바운스 시간을 설정합니다. 객체가 생성될 때 라이브러리가 이미 핀을 INPUT_PULLUP으로 설정했으므로 pinMode() 줄은 필요하지 않습니다.

button.setDebounceTime(40);

loop에서는 먼저 button.loop()를 호출한 다음, 두 이벤트를 읽습니다.

button.loop(); // MUST call the loop() function first if (button.isPressed()) buttonPressStart = millis(); if (button.isReleased()) { unsigned long duration = millis() - buttonPressStart; // duration decides the action }

이에 대해 더 알고 싶다면 아두이노 우노 R4 - 버튼 - 길게 누르기 짧게 누르기 튜토리얼을 참고하세요.

위치 변경 방법

코드는 대한민국 서울로 설정되어 있습니다. 여러분의 도시를 표시하려면 코드 맨 위의 네 줄을 바꾸세요:

#define LATITUDE 37.5665 #define LONGITUDE 126.9780 #define LOCATION_NAME "SEOUL" #define TIMEZONE "Asia%2FSeoul"
  • LATITUDE와 LONGITUDE: 여러분 도시의 위치입니다. 아무 지도 웹사이트에서나 찾을 수 있습니다.
  • LOCATION_NAME: 디스플레이에 표시되는 짧은 이름입니다. 원형 화면에 들어가도록 짧게 유지하세요.
  • TIMEZONE: 시간대 이름으로, 예를 들어 Europe/Berlin 또는 America/New_York입니다. 이름이 웹 주소에 들어가므로 / 문자는 %2F로 써야 합니다. 따라서 Europe/Berlin은 Europe%2FBerlin이 됩니다.

※ 주의:

온도는 섭씨, 풍속은 km/h 단위입니다. 이를 바꾸려면 getWeatherPath() 함수의 temperature_unit과 wind_speed_unit 부분을 수정하세요. 예를 들어 fahrenheit와 mph를 사용할 수 있습니다.

코드 설명

소스 코드 주석에 있는 각 줄의 설명을 확인하세요!

문제 해결

  • 디스플레이가 검은색으로 유지됩니다. SCL과 SDA 전선을 확인하세요. Arduino UNO R4에서는 반드시 D13과 D11에 연결해야 합니다. 이 두 핀은 다른 핀으로 옮길 수 없습니다.
  • 화면에 "NO WIFI MODULE"이 표시됩니다. Arduino UNO R4 Minima를 사용하고 있는 것입니다. 이 프로젝트에는 Arduino UNO R4 WiFi가 필요합니다.
  • 화면에 "WIFI ERROR"가 표시됩니다. WiFi 이름과 비밀번호를 확인하세요. Arduino UNO R4 WiFi는 5 GHz가 아닌 2.4 GHz 네트워크에서만 동작합니다.
  • 시리얼 모니터에 HTTP 오류가 표시됩니다. Arduino UNO R4 WiFi의 WiFi 펌웨어를 업데이트하세요. 오래된 펌웨어에는 올바른 루트 인증서가 없을 수 있습니다. 아두이노 우노 R4 펌웨어 업그레이드 방법 (아두이노 우노 R4 WiFi/DIYables STEM V4 IoT 보드)을 참고하세요.
  • 시계가 틀립니다. 시계는 날씨 응답과 함께 오는 시간을 사용합니다. TIMEZONE 값이 여러분의 도시와 맞는지 확인하세요.

동영상

비디오 제작은 시간이 많이 걸리는 작업입니다. 비디오 튜토리얼이 학습에 도움이 되었다면, YouTube 채널 을 구독하여 알려 주시기 바랍니다. 비디오에 대한 높은 수요가 있다면, 비디오를 만들기 위해 노력하겠습니다.

아래에서 이 프로젝트의 단계별 동영상 안내를 시청하세요.

댓글