', { cookie_domain: 'auto', cookie_flags: 'max-age=0;domain=.tistory.com', cookie_expires: 7 * 24 * 60 * 60 // 7 days, in seconds }); 'Making/ESP32 날씨 표시기' 카테고리의 글 목록 :: MakerLee's Workspace
728x90

아무래도 제 개인의 코딩 능력을 많이 벗어난 데이터셋 정리 등이 필요하다 보니 ChatGPT에게 도움을 청했습니다. 

한동안 이녀석과 질답을 주고받으며 코드를 많이 쌓긴 했습니다

하지만 대화가 길어지면 점점 버벅대고 결과가 산으로 가기도 하네요.

 

그래서 일단 단계별로 진행하고 코드 내용을 따로 기록하기로 합니다.

 

#include <WiFi.h>
#include <HTTPClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include "GetLocate.h"
#include <Arduino_JSON.h>

// Wi-Fi 설정
const char* ssid = "ssid";
const char* password = "password";

const char* apiKey = "API_KEY";
const long utcOffsetInSeconds = 3600 * 9; // 한국 표준시 (UTC+9)

// 타임서버 설정
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);

// 업데이트 간격 (예: 10분)
unsigned long updateInterval = 600000;
unsigned long previousMillis = 0;

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi...");
  }
  Serial.println("Connected to WiFi");

  // 타임서버 연결
  timeClient.begin();
  timeClient.update();

  // 위치 정보 가져오기
  Location loc = getLocation();
  Serial.print("Location - Country: "); Serial.print(loc.country);
  Serial.print(", Region: "); Serial.print(loc.regionName);
  Serial.print(", City: "); Serial.println(loc.city);

  // 초기 날씨 정보 업데이트
  getWeatherUpdate();
}

void loop() {
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= updateInterval) {
    previousMillis = currentMillis;

    // 타임서버 업데이트
    timeClient.update();

    // 현재 시간 출력
    Serial.print("Current time: ");
    Serial.println(timeClient.getFormattedTime());

    // 날씨 정보 업데이트
    getWeatherUpdate();
  }
}

// 날씨 정보를 업데이트하는 함수
void getWeatherUpdate() {
  Location loc = getLocation();
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    String weatherURL = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst";
    weatherURL += "?serviceKey=" + String(apiKey);
    weatherURL += "&numOfRows=60&pageNo=1&base_date=" + getCurrentDate();
    weatherURL += "&base_time=0630&nx=" + String(loc.nx) + "&ny=" + String(loc.ny);
    weatherURL += "&dataType=JSON"; // JSON 형식으로 요청

    http.begin(weatherURL);
    int httpCode = http.GET();

    if (httpCode > 0) {
      String payload = http.getString();
      Serial.println("Weather Data: ");
      
      // 특수문자 및 따옴표 제거 및 줄바꿈 출력
      String formattedPayload = formatPayload(payload);
      Serial.println(formattedPayload);
    } else {
      Serial.print("Error on HTTP request: ");
      Serial.println(httpCode);
    }
    http.end();
  }
}

// 날씨 API의 응답 데이터를 포맷하는 함수
// 이 함수는 JSON 응답에서 불필요한 특수문자와 따옴표를 제거하고,
// 쉼표를 기준으로 줄바꿈을 추가하여 가독성을 높인다.
String formatPayload(String payload) {
  payload.replace("{", ""); // '{' 제거
  payload.replace("}", ""); // '}' 제거
  payload.replace("[", ""); // '[' 제거
  payload.replace("]", ""); // ']' 제거
  payload.replace("(", ""); // '(' 제거
  payload.replace(")", ""); // ')' 제거
  payload.replace("\"", ""); // '"' 제거
  payload.replace(",", ",\n"); // ',' 뒤에 줄바꿈 추가
  return payload;
}

// 현재 날짜를 YYYYMMDD 형식으로 반환하는 함수
// 이 함수는 NTPClient를 사용하여 현재 시간을 가져오고,
// tm 구조체를 사용하여 로컬 시간으로 변환한 후, strftime 함수를 사용하여
// 날짜를 문자열 형식으로 반환한다.
String getCurrentDate() {
  time_t now = timeClient.getEpochTime(); // 현재 epoch 시간 가져오기
  struct tm* timeinfo = localtime(&now); // epoch 시간을 로컬 시간으로 변환
  char buffer[11]; // 날짜를 저장할 버퍼
  strftime(buffer, sizeof(buffer), "%Y%m%d", timeinfo); // 날짜를 YYYYMMDD 형식으로 포맷팅
  return String(buffer); // 문자열로 반환
}

이게 메인 코드이고요

 

 

 

#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>

// Grid 구조체 정의
struct Grid {
  int x;
  int y;
};

// dfs_xy_conv 함수 선언
Grid dfs_xy_conv(const char* code, double v1, double v2);

// Location 구조체 정의
struct Location {
  String country;
  String regionName;
  String city;
  int nx;
  int ny;
};

Location getLocation() {
  HTTPClient http;
  Location loc;
  http.begin("http://ip-api.com/json");
  int httpCode = http.GET();

  if (httpCode > 0) {
    String payload = http.getString();
    JSONVar doc = JSON.parse(payload);

    if (JSON.typeof(doc) != "undefined" && strcmp((const char*)doc["status"], "success") == 0) {
      loc.country = (const char*)doc["country"];
      loc.regionName = (const char*)doc["regionName"];
      loc.city = (const char*)doc["city"];
      float lat = (double)doc["lat"];
      float lon = (double)doc["lon"];

      // 좌표를 NX, NY로 변환
      auto grid = dfs_xy_conv("toXY", lat, lon);
      loc.nx = grid.x;
      loc.ny = grid.y;
    } else {
      Serial.println("Failed to parse location data");
    }
  } else {
    Serial.println("Failed to retrieve location data");
  }
  http.end();
  return loc;
}

// dfs_xy_conv 함수 정의
Grid dfs_xy_conv(const char* code, double v1, double v2) {
  const double RE = 6371.00877;
  const double GRID = 5.0;
  const double SLAT1 = 30.0;
  const double SLAT2 = 60.0;
  const double OLON = 126.0;
  const double OLAT = 38.0;
  const double XO = 43;
  const double YO = 136;

  const double DEGRAD = M_PI / 180.0;
  const double RADDEG = 180.0 / M_PI;

  double re = RE / GRID;
  double slat1 = SLAT1 * DEGRAD;
  double slat2 = SLAT2 * DEGRAD;
  double olon = OLON * DEGRAD;
  double olat = OLAT * DEGRAD;

  double sn = tan(M_PI * 0.25 + slat2 * 0.5) / tan(M_PI * 0.25 + slat1 * 0.5);
  sn = log(cos(slat1) / cos(slat2)) / log(sn);
  double sf = tan(M_PI * 0.25 + slat1 * 0.5);
  sf = pow(sf, sn) * cos(slat1) / sn;
  double ro = tan(M_PI * 0.25 + olat * 0.5);
  ro = re * sf / pow(ro, sn);

  Grid rs = {0, 0};
  if (strcmp(code, "toXY") == 0) {
    double ra = tan(M_PI * 0.25 + (v1) * DEGRAD * 0.5);
    ra = re * sf / pow(ra, sn);
    double theta = v2 * DEGRAD - olon;
    if (theta > M_PI) theta -= 2.0 * M_PI;
    if (theta < -M_PI) theta += 2.0 * M_PI;
    theta *= sn;
    rs.x = floor(ra * sin(theta) + XO + 0.5);
    rs.y = floor(ro - ra * cos(theta) + YO + 0.5);
  }
  return rs;
}

 

이건 GetLocate.h 코드입니다.

Wifi 주소를 추적해서 현재 위치를 파악하고, 그 위치정보를 기상청 API에서 요구하는 그리드 격자정보로 변환합니다.
https://gist.github.com/fronteer-kr/14d7f779d52a21ac2f16
의 코드를 참조하도록 했습니다. 

 

 

이 코드를 실행한 결과는 다음과 같습니다.

Connecting to WiFi...
Connecting to WiFi...
Connected to WiFi
Location - Country: South Korea, Region: Gyeonggi-do, City: Seongnam-si
Weather Data: 
response:header:resultCode:00,
resultMsg:NORMAL_SERVICE,
body:dataType:JSON,
items:item:baseDate:20240529,
baseTime:0630,
category:LGT,
fcstDate:20240529,
fcstTime:0700,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:LGT,
fcstDate:20240529,
fcstTime:0800,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:LGT,
fcstDate:20240529,
fcstTime:0900,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:LGT,
fcstDate:20240529,
fcstTime:1000,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:LGT,
fcstDate:20240529,
fcstTime:1100,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:LGT,
fcstDate:20240529,
fcstTime:1200,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:PTY,
fcstDate:20240529,
fcstTime:0700,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:PTY,
fcstDate:20240529,
fcstTime:0800,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:PTY,
fcstDate:20240529,
fcstTime:0900,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:PTY,
fcstDate:20240529,
fcstTime:1000,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:PTY,
fcstDate:20240529,
fcstTime:1100,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:PTY,
fcstDate:20240529,
fcstTime:1200,
fcstValue:0,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:RN1,
fcstDate:20240529,
fcstTime:0700,
fcstValue:媛뺤닔?놁쓬,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:RN1,
fcstDate:20240529,
fcstTime:0800,
fcstValue:媛뺤닔?놁쓬,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:RN1,
fcstDate:20240529,
fcstTime:0900,
fcstValue:媛뺤닔?놁쓬,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:RN1,
fcstDate:20240529,
fcstTime:1000,
fcstValue:媛뺤닔?놁쓬,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:RN1,
fcstDate:20240529,
fcstTime:1100,
fcstValue:媛뺤닔?놁쓬,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:RN1,
fcstDate:20240529,
fcstTime:1200,
fcstValue:媛뺤닔?놁쓬,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:SKY,
fcstDate:20240529,
fcstTime:0700,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:SKY,
fcstDate:20240529,
fcstTime:0800,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:SKY,
fcstDate:20240529,
fcstTime:0900,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:SKY,
fcstDate:20240529,
fcstTime:1000,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:SKY,
fcstDate:20240529,
fcstTime:1100,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:SKY,
fcstDate:20240529,
fcstTime:1200,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:T1H,
fcstDate:20240529,
fcstTime:0700,
fcstValue:17,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:T1H,
fcstDate:20240529,
fcstTime:0800,
fcstValue:19,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:T1H,
fcstDate:20240529,
fcstTime:0900,
fcstValue:21,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:T1H,
fcstDate:20240529,
fcstTime:1000,
fcstValue:23,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:T1H,
fcstDate:20240529,
fcstTime:1100,
fcstValue:24,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:T1H,
fcstDate:20240529,
fcstTime:1200,
fcstValue:25,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:REH,
fcstDate:20240529,
fcstTime:0700,
fcstValue:75,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:REH,
fcstDate:20240529,
fcstTime:0800,
fcstValue:70,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:REH,
fcstDate:20240529,
fcstTime:0900,
fcstValue:65,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:REH,
fcstDate:20240529,
fcstTime:1000,
fcstValue:60,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:REH,
fcstDate:20240529,
fcstTime:1100,
fcstValue:50,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:REH,
fcstDate:20240529,
fcstTime:1200,
fcstValue:45,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:UUU,
fcstDate:20240529,
fcstTime:0700,
fcstValue:-0.8,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:UUU,
fcstDate:20240529,
fcstTime:0800,
fcstValue:-0.4,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:UUU,
fcstDate:20240529,
fcstTime:0900,
fcstValue:0.5,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:UUU,
fcstDate:20240529,
fcstTime:1000,
fcstValue:1.6,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:UUU,
fcstDate:20240529,
fcstTime:1100,
fcstValue:2.2,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:UUU,
fcstDate:20240529,
fcstTime:1200,
fcstValue:2.6,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VVV,
fcstDate:20240529,
fcstTime:0700,
fcstValue:0.5,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VVV,
fcstDate:20240529,
fcstTime:0800,
fcstValue:0.9,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VVV,
fcstDate:20240529,
fcstTime:0900,
fcstValue:1.1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VVV,
fcstDate:20240529,
fcstTime:1000,
fcstValue:1.1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VVV,
fcstDate:20240529,
fcstTime:1100,
fcstValue:0.8,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VVV,
fcstDate:20240529,
fcstTime:1200,
fcstValue:0.6,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VEC,
fcstDate:20240529,
fcstTime:0700,
fcstValue:118,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VEC,
fcstDate:20240529,
fcstTime:0800,
fcstValue:151,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VEC,
fcstDate:20240529,
fcstTime:0900,
fcstValue:204,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VEC,
fcstDate:20240529,
fcstTime:1000,
fcstValue:236,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VEC,
fcstDate:20240529,
fcstTime:1100,
fcstValue:250,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:VEC,
fcstDate:20240529,
fcstTime:1200,
fcstValue:257,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:WSD,
fcstDate:20240529,
fcstTime:0700,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:WSD,
fcstDate:20240529,
fcstTime:0800,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:WSD,
fcstDate:20240529,
fcstTime:0900,
fcstValue:1,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:WSD,
fcstDate:20240529,
fcstTime:1000,
fcstValue:2,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:WSD,
fcstDate:20240529,
fcstTime:1100,
fcstValue:2,
nx:62,
ny:122,
baseDate:20240529,
baseTime:0630,
category:WSD,
fcstDate:20240529,
fcstTime:1200,
fcstValue:3,
nx:62,
ny:122,
pageNo:1,
numOfRows:60,
totalCount:60

 

현재 위치정보는 성남시로 나오는데요.

핸드폰 핫스팟으로 접속했더니 현재 위치정보가 좀 많이 어긋나네요. 

이건 급한게 아니니 천천히 수정하려고 합니다. 

 

 

항목설명

baseDate 예보 발표 날짜 (YYYYMMDD 형식)
baseTime 예보 발표 시각 (HHMM 형식)
category 예보 항목 코드
fcstDate 예보 날짜 (YYYYMMDD 형식)
fcstTime 예보 시각 (HHMM 형식)
fcstValue 예보 값
nx 예보 지점의 X 좌표
ny 예보 지점의 Y 좌표

 

코드 (category)설명

LGT 낙뢰 가능성
PTY 강수 형태
RN1 1시간 강수량
SKY 하늘 상태
T1H 1시간 기온
REH 습도
UUU 동서바람성분
VVV 남북바람성분
VEC 풍향
WSD 풍속

 

 

모든 코드와 자료정리는 ChatGPT가 했습니다. 
다음은 이 데이터를 간결하게 정리해서 저장하는 부분을 추가하려고 합니다. 

728x90
728x90

*아두이노 기상청 등으로 검색이 많이 들어오는데, 잘 정리된 블로그 링크를 걸어놓겠습니다. 

관련 프로젝트를 하시는 분들은 아래 링크를 참조하세요. 

https://postpop.tistory.com/86

 

아두이노 - ESP01 모듈, 기상청 / 오픈웨더맵 API 날씨 정보 받기

아두이노에서 날씨 정보를 받기 위해서는 기상 서비스 사이트의 API KEY가 있어야 합니다. 한국의 경우 공공데이터포털, 외국 사이트는 OpenWeatherMap에서 회원가입한 뒤 KEY를 할당받아 코딩해 주어

postpop.tistory.com

 

 

 

날씨 예보를 받는 서비스는 오픈웨더맵 외에 '공공 데이터 포털'을 들 수 있습니다. 

이곳에서는 단기예보와 중기예보, 미세먼지 데이터 등 한국 상황에 훨씬 더 잘 맞는 데이터들을 얻을 수 있죠. 

https://www.data.go.kr/

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

 

 

 

 

공공데이터를 이용하려면 일단 인증키를 발급받아야 하는데요

메뉴를 조금만 찾아보시면 쉽게 인증키 신규발급 메뉴를 찾으실 수 있습니다.

발급받은 인증키는 마이페이지 메뉴에서 바로 확인 가능합니다. 

 

 

 

그리고 나서 이제 원하는 API 사용신청을 하면 됩니다. 

보통 간단한 날씨예보는 "단기예보" 를 확인하면 오늘의 시간별 날씨와 3일간의 날씨를 확인할 수 있습니다. 

[데이터찾기] - [데이터목록] 을 클릭하고 "단기예보"를 입력해 검색합니다. 

 

 

 

 

 

 

 

그리고 창을 약간 내려서 오픈API 탭을 클릭하면 "기상청_단기예보 ((구)_동네예보) 조회서비스" 가 나옵니다. 

"활용신청" 버튼을 클릭합니다. 

 

신청 메뉴에는 사용 목적 등을 간단하게 적으면 되며, 신청 즉시 사용이 가능합니다. 

 

 

 

개발용 계정은 즉시 승인되어 바로 사용할 수 있습니다.

단기예보는 하루에 10000건, 대기오염정보는 500건 제한이 있긴 한데 개인용으로 쓰기엔 전혀 문제없는 용량이죠.

 

 

 

 

 

자세한 활용방법을 알고 싶으면 조회서비스를 들어가서 하단의 .zip 파일을 다운받으면 내용을 확인할 수 있습니다. 

 

 

 

 

 

 

728x90
728x90

Openweathermap에서 날씨를 받아와 사용하려 했는데 데이터 변환 과정에서 골치아픈 부분이 있었습니다. 

차후 4.2" 전자잉크 등 보다 화면이 넓은 기기를 사용하기 위해서 당일의 온도변화, 7일치의 날씨변화 등도 있었으면 했고요.

무엇보다 현재 날씨는 알 수 있는데 당일 날씨나 최저, 최고 온도 등 우리가 흔히 쓰는 기준으로 데이터를 얻기가 힘들더군요.

 

 

그리고 이 모든 것을 ChatGPT4.0 에게 맡겼습니다. 

 

 

 

 

 

 

 

 

저는 그냥 이렇게 해줘, 저렇게 해줘, 여기 틀렸잖아

를 반복하다 보니 코드는 손도 안대고 좋은 결과가 나오더군요

 

 

금일 온도 / 최저 / 최고온도 / 체감온도/ 구름 / 풍속 / 강수량 / 강설량 등이 잘 정리되어 출력된 것을 볼 수 있습니다. 

5일간 날씨 데이터도 마찬가지로 정리되어 나오고 있고요

 

 

 

아래는 이 코드입니다.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>

// WiFi 설정
const char* ssid = "Yourssid";
const char* password = "yourPW";

// OpenWeatherMap API 키
String openWeatherMapApiKey = "YourAPI";

// 서울의 도시명과 국가 코드
String city = "Seoul";
String countryCode = "KR";

// 업데이트 주기 설정 (15초)
unsigned long lastTime = 0;
unsigned long timerDelay = 15000; 

void setup() {
  Serial.begin(115200); // 시리얼 모니터 시작
  Serial.println("serial start"); // 디버깅 메시지 추가
  WiFi.begin(ssid, password); // WiFi 연결 시작
  Serial.println("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected to WiFi");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // 즉시 첫 번째 API 호출을 위해 lastTime 초기화
  lastTime = millis() - timerDelay;
}

void loop() {
  // 타이머가 만료되었을 때 데이터 요청
  if ((millis() - lastTime) > timerDelay) {
    if (WiFi.status() == WL_CONNECTED) {
      // API 요청 URL 구성 - 3시간 간격의 5일 예보 데이터
      String serverPath = "https://api.openweathermap.org/data/2.5/forecast?q=" + city + "," + countryCode + "&appid=" + openWeatherMapApiKey + "&units=metric";
      
      // 데이터 요청 및 수신
      String weatherData = httpGETRequest(serverPath.c_str());
      if (weatherData != "{}") { 
        parseWeatherData(weatherData); // 수신된 데이터 파싱
      }
      lastTime = millis();
    } else {
      Serial.println("WiFi Disconnected");
    }
  }
}

// HTTP GET 요청 함수
String httpGETRequest(const char* serverName) {
  WiFiClientSecure client; 
  client.setInsecure();    // SSL 인증서 검증 무시
  HTTPClient http;
  http.begin(client, serverName); // HTTPS 연결 시작
  Serial.println("Requesting URL: " + String(serverName));

  int httpResponseCode = http.GET();
  
  String payload = "{}";
  if (httpResponseCode == 200) {
    Serial.print("HTTP Response code: ");
    Serial.println(httpResponseCode);
    payload = http.getString();
  } else {
    Serial.print("Error code: ");
    Serial.println(httpResponseCode);
    payload = http.getString(); 
    Serial.println("Error response: " + payload);
  }
  http.end();
  return payload;
}

// 타임스탬프를 한국 표준시(KST)로 변환하는 함수
String convertUnixTimeToKST(long unixTime) {
  unixTime += 9 * 3600; // UTC+9 시간대 보정
  time_t t = unixTime;
  struct tm *tmStruct = localtime(&t);
  char buffer[30];
  strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", tmStruct);
  return String(buffer);
}

// 날씨 데이터 파싱 함수
void parseWeatherData(String json) {
  JSONVar myObject = JSON.parse(json);
  if (JSON.typeof(myObject) == "undefined") {
    Serial.println("Parsing failed!");
    return;
  }

  // 오늘 날씨 데이터 파싱 및 출력
  Serial.println("Today's Weather Data:");
  JSONVar list = myObject["list"];

  // 하루 동안의 데이터에서 최저/최고 온도 계산
  float dayMinTemp = 1000;
  float dayMaxTemp = -1000;
  for (int i = 0; i < 8; i++) { // 24시간 내의 3시간 간격 데이터
    JSONVar data = list[i];
    float temp_min = (float)(double)data["main"]["temp_min"];
    float temp_max = (float)(double)data["main"]["temp_max"];
    if (temp_min < dayMinTemp) dayMinTemp = temp_min;
    if (temp_max > dayMaxTemp) dayMaxTemp = temp_max;
  }
  JSONVar todayData = list[0];
  float temp = (float)(double)todayData["main"]["temp"];
  float feels_like = (float)(double)todayData["main"]["feels_like"];
  const char* weather_description = (const char*)(todayData["weather"][0]["description"]);
  float humidity = (float)(double)todayData["main"]["humidity"];
  float clouds = (float)(double)todayData["clouds"]["all"];
  float wind_speed = (float)(double)todayData["wind"]["speed"];
  float rain = todayData.hasOwnProperty("rain") ? (float)(double)todayData["rain"]["3h"] : 0;
  float snow = todayData.hasOwnProperty("snow") ? (float)(double)todayData["snow"]["3h"] : 0;

  Serial.print("Temp: ");
  Serial.println(temp);
  Serial.print("Min Temp: ");
  Serial.println(dayMinTemp);
  Serial.print("Max Temp: ");
  Serial.println(dayMaxTemp);
  Serial.print("Feels Like: ");
  Serial.println(feels_like);
  Serial.print("Humidity: ");
  Serial.println(humidity);
  Serial.print("Clouds: ");
  Serial.println(clouds);
  Serial.print("Weather: ");
  Serial.println(weather_description);
  Serial.print("Wind Speed: ");
  Serial.println(wind_speed);
  Serial.print("Rain Volume (3h): ");
  Serial.println(rain);
  Serial.print("Snow Volume (3h): ");
  Serial.println(snow);
  Serial.println("-------");

  // 5일간 날씨 데이터 파싱 및 출력
  Serial.println("5 Days Weather Data:");
  for (int day = 0; day < 5; day++) {
    float minTemp = 1000;
    float maxTemp = -1000;
    float avgHumidity = 0;
    float avgClouds = 0;
    float avgWindSpeed = 0;
    float totalRain = 0;
    float totalSnow = 0;
    int count = 0;
    String weatherDescriptions = "";

    for (int i = day * 8; i < (day + 1) * 8 && i < list.length(); i++) {
      JSONVar dayData = list[i];
      float temp_min = (float)(double)dayData["main"]["temp_min"];
      float temp_max = (float)(double)dayData["main"]["temp_max"];
      float humidity = (float)(double)dayData["main"]["humidity"];
      float clouds = (float)(double)dayData["clouds"]["all"];
      float wind_speed = (float)(double)dayData["wind"]["speed"];
      float rain = dayData.hasOwnProperty("rain") ? (float)(double)dayData["rain"]["3h"] : 0;
      float snow = dayData.hasOwnProperty("snow") ? (float)(double)dayData["snow"]["3h"] : 0;
      const char* weather_description = (const char*)(dayData["weather"][0]["description"]);

      if (temp_min < minTemp) minTemp = temp_min;
      if (temp_max > maxTemp) maxTemp = temp_max;
      avgHumidity += humidity;
      avgClouds += clouds;
      avgWindSpeed += wind_speed;
      totalRain += rain;
      totalSnow += snow;
      weatherDescriptions = String(weather_description);
      count++;
    }
    avgHumidity /= count;
    avgClouds /= count;
    avgWindSpeed /= count;

    long date = (long)list[day * 8]["dt"];
    Serial.print("Date: ");
    Serial.print(convertUnixTimeToKST(date));
    Serial.print(", Min Temp: ");
    Serial.print(minTemp);
    Serial.print(", Max Temp: ");
    Serial.print(maxTemp);
    Serial.print(", Humidity: ");
    Serial.print(avgHumidity);
    Serial.print(", Clouds: ");
    Serial.print(avgClouds);
    Serial.print(", Wind Speed: ");
    Serial.print(avgWindSpeed);
    Serial.print(", Rain Volume (3h): ");
    Serial.print(totalRain);
    Serial.print(", Snow Volume (3h): ");
    Serial.print(totalSnow);
    Serial.print(", Weather: ");
    Serial.println(weatherDescriptions);
    Serial.println("-------");
  }
}

 

 

그렇지만 이 모든 걸 갈아엎게 되는데.. 다음 포스팅에 이어서 쓰겠습니다.

728x90
728x90

검색해 보면 날씨정보를 받아오는 방법은 여러 가지입니다.

기상청이나 여타 날씨관련 사이트 등에서도 가능하지만 여러 모로 알아본 결과 openweathermap 이 편리할 것 같더군요




1. 오픈웨더맵 가입 - api 를 생성합니다.

 

 

이후 

http://api.openweathermap.org/data/2.5/weather?q=yourCityName,yourCountryCode&APPID=yourUniqueAPIkey

위 텍스트에서 yourCityName / yourCountryCode / yourUniqueAPIkey를 수정해야 하는데요.

 

 

 

 

서울을 예로 들면

검색하면 이렇게 나오는데 yourCityName 은 Seoul, yourCountryCode 은 KR 을 넣으면 됩니다

그리고 마지막 yourUniqueAPIkey 를 처음 발급받은 API 를 복사해서 수정하면 전체 주소가 완성됩니다.

 

 

 

 

 

 

그 주소를 인터넷 창에 입력해보면 다음과 같이 날씨 데이터가 표시되는 것을 알 수 있습니다

 

 

 

이제 이 API 주소를 이용해 ESP32C3 에서 날씨 데이터를 받아와 보겠습니다.

맨 먼저 날씨 데이터가 JSON 형식으로 나오기 때문에 이를 처리할 Arduino_JSON 라이브러리를 설치해 줬습니다.

 

 

 

 

 

 

이번에는 초기 코드를 99% 이상 ChapGPT 4.0에게 맡겼습니다. 

예전보다 훨씬 뛰어나져서 원하는 대로 코드를 깔끔하게 짜 주네요.

저는 세부 사항 몇가지를 조정하고 두세번 다시 물어보는 정도로 코드를 완성할 수 있었습니다. 

 

 

 

전체 코드는 아래와 같습니다.

#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino_JSON.h>

const char* ssid = "YourWifiID";
const char* password = "YourWifiPassword";

// Your Domain name with URL path or IP address with path
String openWeatherMapApiKey = "YourAPI";

// Replace with your country code and city
String city = "Seoul";
String countryCode = "KR";

// Timer set to 15 seconds for demonstration
unsigned long lastTime = 0;
unsigned long timerDelay = 15000;

String jsonBuffer;

void setup() {
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  Serial.println("Connecting");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to WiFi network with IP Address: ");
  Serial.println(WiFi.localIP());
  Serial.println("Timer set to 15 seconds (timerDelay variable), it will take 15 seconds before publishing the first reading.");
}

void loop() {
  if ((millis() - lastTime) > timerDelay) {
    if (WiFi.status() == WL_CONNECTED) {
      String serverPath = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "," + countryCode + "&APPID=" + openWeatherMapApiKey;
      jsonBuffer = httpGETRequest(serverPath.c_str());
      JSONVar myObject = JSON.parse(jsonBuffer);

      if (JSON.typeof(myObject) == "undefined") {
        Serial.println("Parsing input failed!");
        return;
      }

      Serial.print("Latitude: ");
      Serial.println(myObject["coord"]["lat"]);
      Serial.print("Longitude: ");
      Serial.println(myObject["coord"]["lon"]);
      
      Serial.println("Weather Details:");
      for (int i = 0; i < myObject["weather"].length(); i++) {
        Serial.print("  Weather ID: ");
        Serial.println(myObject["weather"][i]["id"]);
        Serial.print("  Main: ");
        Serial.println(myObject["weather"][i]["main"]);
        Serial.print("  Description: ");
        Serial.println(myObject["weather"][i]["description"]);
        Serial.print("  Icon: ");
        Serial.println(myObject["weather"][i]["icon"]);
      }

      Serial.print("Temperature: ");
      Serial.println(myObject["main"]["temp"]);
      Serial.print("Feels Like: ");
      Serial.println(myObject["main"]["feels_like"]);
      Serial.print("Minimum Temperature: ");
      Serial.println(myObject["main"]["temp_min"]);
      Serial.print("Maximum Temperature: ");
      Serial.println(myObject["main"]["temp_max"]);
      Serial.print("Pressure: ");
      Serial.println(myObject["main"]["pressure"]);
      Serial.print("Humidity: ");
      Serial.println(myObject["main"]["humidity"]);

      Serial.print("Wind Speed: ");
      Serial.println(myObject["wind"]["speed"]);
      Serial.print("Wind Direction (Degrees): ");
      Serial.println(myObject["wind"]["deg"]);
      if (myObject["wind"].hasOwnProperty("gust")) {
        Serial.print("Wind Gust: ");
        Serial.println(myObject["wind"]["gust"]);
      }
      
      if (myObject.hasOwnProperty("rain")) {
        Serial.print("Rain Volume for last hour: ");
        Serial.println(myObject["rain"]["1h"]);
      }
      
      if (myObject.hasOwnProperty("snow")) {
        Serial.print("Snow Volume for last hour: ");
        Serial.println(myObject["snow"]["1h"]);
      }

      Serial.print("Visibility: ");
      Serial.println(myObject["visibility"]);
      
      Serial.print("Cloudiness (%): ");
      Serial.println(myObject["clouds"]["all"]);

      lastTime = millis();
    } else {
      Serial.println("WiFi Disconnected");
    }
  }
}

String httpGETRequest(const char* serverName) {
  WiFiClient client;
  HTTPClient http;
  http.begin(client, serverName);
  int httpResponseCode = http.GET();
  String payload = "{}"; 

  if (httpResponseCode > 0) {
    Serial.print("HTTP Response code: ");
    Serial.println(httpResponseCode);
    payload = http.getString();
  } else {
    Serial.print("Error code: ");
    Serial.println(httpResponseCode);
  }
  http.end();
  return payload;
}

 

 

 

 

 

아래가 출력 결과입니다.

완벽합니다.

 

 

 

 

 

여담이지만 재미있던 것은 제가 출력 반복 간격을 늦추려고 10000(10초)를 15000(15초)로 변경했더니 

바로 다음 질답에서 시리얼 출력의 'Timer set to 10 seconds...' 부분을 'Timer set to 15 seconds...' 로 바꾸더군요

이런 센스(?)까지 발휘하는 모습에 다소 놀랐습니다. 

 

 

728x90
728x90

 

 

타오바오에서 너무 많이 사는 바람에 혼자서는 평생 쓰고도 남을 만큼의 전자잉크가 생겨서 사용처를 고민중이었습니다. 

그러다가 마음에 드는 프로젝트를 발견했죠.

 

 

 

 

 

 

 

전기도 많이 먹지 않고, 리프레쉬도 자주 필요 없는 이런 날씨정보 디스플레이 용으로 좋겠더군요.

 

 

 

 

 

 

원래는 전용 드라이버 보드를 제작해서 컨트롤러를 대체하려고 했지만 실패하고..

디버깅 중에 어차피 비용도 비슷하게 들 것 같아 그냥 편한 쪽으로 일단 진행하기로 했습니다. 

 

 

 

 

 

 

 

 

컨트롤러는 ESP32C3 보드를 사용할 예정입니다.

 

 

 

 

 

가격은 무려 2천원대에 블루투스, 와이파이도 되면서 ESP32의 성능을 가져 성능도 부족할 게 없고요.

보다시피 엄청 아담한 사이즈라 사용하기에도 좋습니다. 

728x90

+ Recent posts