はじめに
前回、1.8インチのLCDを使って気象情報モニターを作りました。
記事を見てもらえればわかるんですが、1.8インチだとせいぜい今日と明日の天気くらいしか表示できないんですよね。
有難いことにコメントもいただき、改善したかった部分のイメージも沸いたので、3.5インチのLCDを使って実用性のある気象モニタを作ってみました。
↓参考にさせていただいた記事はこちら
使用するもの
- ESP32
- ILI9488(3.5インチ LCD)
- ジャンパワイヤー
今回使用する3.5インチLCDは電子工作ステーションで購入しました。
商品名はILI9341と書かれていますが、実際は製品仕様に書かれているILI9488でした。
配線
| ILI9488 ピン | ESP32 GPIO | 説明 |
|---|---|---|
| VCC | 5V / 3.3V | 電源 |
| GND | GND | グランド |
| CS | GPIO27 | チップセレクト |
| RESET | GPIO32 | リセット |
| DC | GPIO33 | データ/コマンド切替 |
| SDI (MOSI) | GPIO13 | SPIデータ(ESP32 → LCD) |
| SCK | GPIO14 | SPIクロック |
| LED | 3.3V | バックライト(常時 ON) |
ソースコード
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
const char *ssid = "x";
const char *pass = "x";
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 3600000; // 1時間
// 晴れ(大)
void drawSunLarge(int x, int y) {
uint16_t sun = tft.color565(255, 60, 20);
tft.fillCircle(x, y, 28, sun);
tft.drawLine(x, y - 45, x, y - 30, sun);
tft.drawLine(x, y + 45, x, y + 30, sun);
tft.drawLine(x - 45, y, x - 30, y, sun);
tft.drawLine(x + 45, y, x + 30, y, sun);
tft.drawLine(x - 32, y - 32, x - 20, y - 20, sun);
tft.drawLine(x + 32, y - 32, x + 20, y - 20, sun);
tft.drawLine(x - 32, y + 32, x - 20, y + 20, sun);
tft.drawLine(x + 32, y + 32, x + 20, y + 20, sun);
}
// 晴れ(小)
void drawSunSmall(int x, int y) {
uint16_t sun = tft.color565(255, 60, 20);
// 本体
tft.fillCircle(x, y, 10, sun);
// 縦横
tft.drawLine(x, y - 18, x, y - 12, sun);
tft.drawLine(x, y + 18, x, y + 12, sun);
tft.drawLine(x - 18, y, x - 12, y, sun);
tft.drawLine(x + 18, y, x + 12, y, sun);
// 斜め
tft.drawLine(x - 13, y - 13, x - 8, y - 8, sun);
tft.drawLine(x + 13, y - 13, x + 8, y - 8, sun);
tft.drawLine(x - 13, y + 13, x - 8, y + 8, sun);
tft.drawLine(x + 13, y + 13, x + 8, y + 8, sun);
}
// 雨(大)
void drawUmbrellaLarge(int x, int y) {
uint16_t blue = tft.color565(80, 140, 255);
int R = 28;
for (int yy = -R; yy <= 0; yy++) {
int w = sqrt(R * R - yy * yy);
tft.drawFastHLine(x - w, y + yy, w * 2, blue);
}
tft.drawLine(x - R, y, x - 12, y - 13, blue);
tft.drawLine(x - R + 1, y, x - 11, y - 13, blue);
tft.drawLine(x - 12, y - 13, x, y, blue);
tft.drawLine(x - 11, y - 13, x + 1, y, blue);
tft.drawLine(x, y, x + 12, y - 13, blue);
tft.drawLine(x + 1, y, x + 13, y - 13, blue);
tft.drawLine(x + 12, y - 13, x + R, y, blue);
tft.drawLine(x + 13, y - 13, x + R - 1, y, blue);
tft.fillRect(x - 2, y, 4, 42, blue);
tft.fillRect(x - 14, y + 40, 16, 4, blue);
}
// 雨(小)
void drawUmbrellaSmall(int x, int y) {
uint16_t blue = tft.color565(80, 140, 255);
int R = 15;
for (int yy = -R; yy <= 0; yy++) {
int w = sqrt(R * R - yy * yy);
tft.drawFastHLine(x - w, y + yy, w * 2, blue);
}
tft.drawLine(x - R, y, x - 6, y - 7, blue);
tft.drawLine(x - R + 1, y, x - 5, y - 7, blue);
tft.drawLine(x - 6, y - 7, x, y, blue);
tft.drawLine(x - 5, y - 7, x + 1, y, blue);
tft.drawLine(x, y, x + 6, y - 7, blue);
tft.drawLine(x + 1, y, x + 7, y - 7, blue);
tft.drawLine(x + 6, y - 7, x + R, y, blue);
tft.drawLine(x + 7, y - 7, x + R - 1, y, blue);
tft.fillRect(x - 1, y, 3, 22, blue);
tft.fillRect(x - 9, y + 20, 10, 3, blue);
}
// 雲(大)
void drawCloudLarge(int x, int y) {
uint16_t c = tft.color565(230, 230, 230);
tft.fillCircle(x - 22, y, 18, c);
tft.fillCircle(x + 22, y, 20, c);
tft.fillCircle(x, y - 13, 22, c);
tft.fillCircle(x - 9, y + 7, 16, c);
tft.fillCircle(x + 9, y + 7, 16, c);
}
// 雲(小)
void drawCloudSmall(int x, int y) {
uint16_t c = tft.color565(230, 230, 230);
tft.fillCircle(x - 12, y, 11, c);
tft.fillCircle(x + 12, y, 12, c);
tft.fillCircle(x, y - 8, 13, c);
tft.fillCircle(x - 5, y + 5, 9, c);
tft.fillCircle(x + 5, y + 5, 9, c);
}
// ❄ 雪(大)
void drawSnowLarge(int x, int y) {
uint16_t w = TFT_WHITE;
tft.fillCircle(x, y, 8, w);
tft.drawLine(x - 22, y, x + 22, y, w);
tft.drawLine(x, y - 22, x, y + 22, w);
tft.drawLine(x - 16, y - 16, x + 16, y + 16, w);
tft.drawLine(x - 16, y + 16, x + 16, y - 16, w);
}
// ❄ 雪(小)
void drawSnowSmall(int x, int y) {
uint16_t w = TFT_WHITE;
tft.fillCircle(x, y, 5, w);
tft.drawLine(x - 10, y, x + 10, y, w);
tft.drawLine(x, y - 10, x, y + 10, w);
}
// weatherMap
struct WeatherPair { const char* a; const char* b; };
WeatherPair weatherMap[500];
void setPair(int code, const char* a, const char* b) {
weatherMap[code].a = a;
weatherMap[code].b = b;
}
void initWeatherMap() {
// 代表コード
setPair(100,"sun","sun"); setPair(101,"sun","cloud"); setPair(102,"sun","rain");
setPair(104,"sun","snow"); setPair(110,"sun","cloud"); setPair(112,"sun","rain");
setPair(115,"sun","snow");
setPair(200,"cloud","cloud"); setPair(201,"cloud","sun"); setPair(202,"cloud","rain");
setPair(204,"cloud","snow"); setPair(210,"cloud","sun"); setPair(212,"cloud","rain");
setPair(215,"cloud","snow");
setPair(300,"rain","rain"); setPair(301,"rain","sun"); setPair(302,"rain","cloud");
setPair(303,"rain","snow"); setPair(308,"rain","rain"); setPair(311,"rain","sun");
setPair(313,"rain","cloud"); setPair(314,"rain","snow");
setPair(400,"snow","snow"); setPair(401,"snow","sun"); setPair(402,"snow","cloud");
setPair(403,"snow","rain"); setPair(406,"snow","cloud"); setPair(411,"snow","sun");
setPair(413,"snow","cloud"); setPair(414,"snow","rain");
// 全コード → 代表コード
int mapList[][2] = {
{100,100},{101,101},{102,102},{103,102},{104,104},{105,104},{106,102},{107,102},{108,102},
{110,110},{111,110},{112,112},{113,112},{114,112},{115,115},{116,115},{117,115},{118,112},
{119,112},{120,102},{121,102},{122,112},{123,100},{124,100},{125,112},{126,112},{127,112},
{128,112},{130,100},{131,100},{132,101},{140,102},{160,104},{170,104},{181,115},
{200,200},{201,201},{202,202},{203,202},{204,204},{205,204},{206,202},{207,202},{208,202},
{209,200},{210,210},{211,210},{212,212},{213,212},{214,212},{215,215},{216,215},{217,215},
{218,212},{219,212},{220,202},{221,202},{222,212},{223,201},{224,212},{225,212},{226,212},
{228,215},{229,215},{230,215},{231,200},{240,202},{250,204},{260,204},{270,204},{281,215},
{300,300},{301,301},{302,302},{303,303},{304,300},{306,300},{308,308},{309,303},{311,311},
{313,313},{314,314},{315,314},{316,311},{317,313},{320,311},{321,313},{322,303},{323,311},
{324,311},{325,311},{326,314},{327,314},{328,300},{329,300},{340,400},{350,300},{361,411},
{371,413},
{400,400},{401,401},{402,402},{403,403},{405,400},{406,406},{407,406},{409,403},{411,411},
{413,413},{414,414},{420,411},{421,413},{422,414},{423,414},{425,400},{426,400},{427,400},
{450,400}
};
int N = sizeof(mapList)/sizeof(mapList[0]);
for(int i=0;i<N;i++){
int code = mapList[i][0];
int rep = mapList[i][1];
weatherMap[code] = weatherMap[rep];
}
}
// 遷移アイコン
void drawWeatherTransitionLarge(int code, int x, int y) {
WeatherPair p = weatherMap[code];
String A = p.a;
String B = p.b;
// 単体表示(100,200,300)
if (code == 100) { drawSunLarge(x, y); return; }
if (code == 200) { drawCloudLarge(x, y); return; }
if (code == 300) { drawUmbrellaLarge(x, y); return; }
// 前(左)
int frontX = x - 20;
int frontY = y;
// 後ろ(右)
int backX = x + 35;
int backY = y - 8;
// 前
if (A == "sun") drawSunLarge(frontX, frontY);
else if (A == "cloud") drawCloudLarge(frontX, frontY);
else if (A == "rain") drawUmbrellaLarge(frontX, frontY);
else drawSnowLarge(frontX, frontY);
// 後ろ
if (B == "sun") drawSunLarge(backX, backY);
else if (B == "cloud") drawCloudLarge(backX, backY);
else if (B == "rain") drawUmbrellaLarge(backX, backY);
else drawSnowLarge(backX, backY);
}
void drawWeatherTransitionSmall(int code, int x, int y) {
WeatherPair p = weatherMap[code];
String A = p.a;
String B = p.b;
// 単体表示(100,200,300のみ)
if (code == 100) { drawSunSmall(x, y); return; }
if (code == 200) { drawCloudSmall(x, y); return; }
if (code == 300) { drawUmbrellaSmall(x, y); return; }
// 前(左)
int frontX = x - 10;
int frontY = y;
// 後ろ(右)
int backX = x + 20;
int backY = y - 4;
// 前
if (A == "sun") drawSunSmall(frontX, frontY);
else if (A == "cloud") drawCloudSmall(frontX, frontY);
else if (A == "rain") drawUmbrellaSmall(frontX, frontY);
else drawSnowSmall(frontX, frontY);
// 後ろ
if (B == "sun") drawSunSmall(backX, backY);
else if (B == "cloud") drawCloudSmall(backX, backY);
else if (B == "rain") drawUmbrellaSmall(backX, backY);
else drawSnowSmall(backX, backY);
}
// 日付変換
String ymdToMD(const String &ymd) {
return String(ymd.substring(5,7).toInt()) + "/" + String(ymd.substring(8,10).toInt());
}
// 天気更新
void updateWeather() {
WiFi.disconnect(true);
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) delay(200);
delay(300);
HTTPClient httpJMA;
httpJMA.begin("https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json");
if (httpJMA.GET() != 200) return;
String jmaBody = httpJMA.getString();
StaticJsonDocument<20000> jma;
deserializeJson(jma, jmaBody);
// 今日・明日の天気コード
JsonArray weatherCodesToday = jma[0]["timeSeries"][0]["areas"][0]["weatherCodes"];
JsonArray timesToday = jma[0]["timeSeries"][0]["timeDefines"];
int todayCode = weatherCodesToday[0].as<int>();
int tomorrowCode = weatherCodesToday[1].as<int>();
String d_today = ymdToMD(timesToday[0]);
String d_tomorrow = ymdToMD(timesToday[1]);
JsonArray tempsShort = jma[0]["timeSeries"][2]["areas"][0]["temps"];
String todayMinStr = "-";
String todayMaxStr = "-";
float tomorrowMin = 0;
float tomorrowMax = 0;
if (tempsShort.size() == 2) {
// 明日の最低・最高のみ
tomorrowMin = tempsShort[0].as<float>();
tomorrowMax = tempsShort[1].as<float>();
} else if (tempsShort.size() == 4) {
// 今日の最低・最高
todayMinStr = tempsShort[0].as<const char*>();
todayMaxStr = tempsShort[1].as<const char*>();
// 明日の最低・最高
tomorrowMin = tempsShort[2].as<float>();
tomorrowMax = tempsShort[3].as<float>();
}
// 週間
JsonArray weatherCodesWeek = jma[1]["timeSeries"][0]["areas"][0]["weatherCodes"];
JsonArray timesWeek = jma[1]["timeSeries"][0]["timeDefines"];
JsonArray tempsMin = jma[1]["timeSeries"][1]["areas"][0]["tempsMin"];
JsonArray tempsMax = jma[1]["timeSeries"][1]["areas"][0]["tempsMax"];
// 描画
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
// Today
tft.drawString("Today (" + d_today + ")", 40, 30, 4);
drawWeatherTransitionLarge(todayCode, 80, 110);
tft.drawString(todayMinStr + "/" + todayMaxStr + "C", 160, 110, 4);
// Tomorrow
tft.drawString("Tomorrow (" + d_tomorrow + ")", 260, 30, 4);
drawWeatherTransitionLarge(tomorrowCode, 300, 110);
tft.drawString(String(tomorrowMin, 0) + "/" + String(tomorrowMax, 0) + "C", 380, 110, 4);
// Week(明後日〜5日後を表示)
int baseX = 40;
int stepX = 90;
for (int i = 0; i < 5; i++) {
int idxDay = i + 1;
int code = weatherCodesWeek[idxDay].as<int>();
String dstr = ymdToMD(timesWeek[idxDay]);
float mn = tempsMin[idxDay].as<float>();
float mx = tempsMax[idxDay].as<float>();
int x = baseX + i * stepX;
tft.drawString(dstr, x, 190, 2);
drawWeatherTransitionSmall(code, x + 15, 235);
tft.drawString(String(mn, 0) + "/" + String(mx, 0) + "C", x, 265, 2);
}
WiFi.disconnect(true);
}
void setup() {
Serial.begin(115200);
tft.init();
tft.setRotation(3);
tft.fillScreen(TFT_BLACK);
initWeatherMap();
updateWeather();
lastUpdate = millis();
}
void loop() {
if (millis() - lastUpdate >= updateInterval) {
updateWeather();
lastUpdate = millis();
}
}
注意点
-
地域コードについて
上記のJSON URLは福岡地方(400000)になっています。
他の地域を使う場合は、気象庁のコード表から適宜変更してください -
今日の気温について
気象庁の天気予報データでは、今日の最低/最高気温が常に取得できるわけではありません。
時間帯によっては最高気温のみだったり、最低/最高気温が「-」になることがあります。 -
TFT_eSPI の設定
User_Setup.h も変更が必要です。
私の環境では、以下の設定を有効にしています。
#define ILI9488_DRIVER
#define TFT_MISO -1
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_CS 27
#define TFT_DC 33
#define TFT_RST 32
#define TFT_BL -1
動作確認
気象庁が発表していたデータと表示内容が一致していたので、問題なさそうです。
ちなみにアイコンは例のごとく生成AIと壁打ちして作りました。
おわりに
とりあえず「使えるレベル」には到達しました。
ブレッドボードのままだと存在感がすごいので、
そのうちケースを作って、玄関にそっと置こうと思っています。
家に眠っているESP32がある方は、ぜひ天気端末に転生させてみてください。

