Last active
June 9, 2021 10:56
-
-
Save sayacom/41a642b215368e73c958aeafeddb290d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2021 Hisaya OKADA | |
* Released under the MIT license | |
* https://opensource.org/licenses/mit-license.php | |
*/ | |
#define IMAGE_BUFFER_SIZE 10 * 1024 | |
#define IMAGE_ICON_DIR "/icons" | |
#define FONT_FILE_PATH "/fonts/KosugiMaru-Regular.ttf" | |
#define TINY_GSM_MODEM_UBLOX | |
#define CONSOLE Serial | |
#include <M5Stack.h> | |
#include <HTTPClient.h> | |
#include <M5FontRender.h> | |
#include <TinyGsmClient.h> | |
#include <ArduinoHttpClient.h> | |
#include <ArduinoJson.h> | |
TinyGsm GsmModem(Serial2); | |
TinyGsmClient GsmClient(GsmModem); | |
M5FontRender render; | |
void initializeGsmModem() { | |
// Begin GSM modem initialization process | |
CONSOLE.println("Initializing GSM modem..."); | |
renderProcessingScreen("通信モジュールを初期化中…"); | |
// Open Serial2 as 3G Module | |
Serial2.begin(115200, SERIAL_8N1, 16, 17); | |
// Start GSM Modem | |
GsmModem.restart(); | |
CONSOLE.println("--- Modem Info ---"); | |
String info = GsmModem.getModemInfo(); | |
CONSOLE.println(info); | |
CONSOLE.println("--- End Info ---"); | |
// Wait for catch cellular netwoek | |
CONSOLE.println("Waiting cellular network..."); | |
renderProcessingScreen("セルラーネットワークに接続中…"); | |
while(!GsmModem.waitForNetwork()) CONSOLE.print("."); | |
// Connect SORACOM network | |
CONSOLE.println("Connecting SORACOM..."); | |
renderProcessingScreen("SORACOMに接続中…"); | |
GsmModem.gprsConnect("soracom.io", "sora", "sora"); | |
while(!GsmModem.isNetworkConnected()) CONSOLE.print("."); | |
// Show device IP address | |
CONSOLE.println("Device IP Address: "); | |
IPAddress ipaddr = GsmModem.localIP(); | |
CONSOLE.println(ipaddr); | |
} | |
String getWeatherJSON() { | |
// Create HTTP Client | |
HttpClient httpClient = HttpClient(GsmClient, "beam.soracom.io", 8888); | |
// Send request | |
int err = httpClient.get("/weather"); | |
if (err != 0) { | |
CONSOLE.println("Failed to get weather: connection failed."); | |
renderErrorScreen("Beamとの接続に失敗しました"); | |
return ""; | |
} | |
renderProcessingScreen("Beamに接続しました"); | |
// Get header | |
int statusCode = httpClient.responseStatusCode(); | |
CONSOLE.print("Status: HTTP "); CONSOLE.println(statusCode); | |
if (statusCode >= 400) { | |
CONSOLE.println("Failed to get weather: invalid status code."); | |
renderErrorScreen("異常なステータスコードです"); | |
return ""; | |
} | |
// Get response | |
String response = httpClient.responseBody(); | |
CONSOLE.print("Response: "); CONSOLE.println(response); | |
// Close connection | |
httpClient.stop(); | |
renderProcessingScreen("天気データを取得しました"); | |
return response; | |
} | |
int getWeatherIconViaOWM(String iconCode, uint8_t *imageBuf) { | |
// Create HTTP Client | |
HttpClient httpClient = HttpClient(GsmClient, "openweathermap.org", 80); | |
// Send request: @2x means large size icon | |
int err = httpClient.get("/img/wn/" + iconCode + "@2x.png"); | |
if (err != 0) { | |
CONSOLE.println("Failed to get weather icon: connection failed."); | |
renderErrorScreen("OWMとの接続に失敗しました"); | |
return -1; | |
} | |
renderProcessingScreen("OWMに接続しました"); | |
// Get header | |
int statusCode = httpClient.responseStatusCode(); | |
CONSOLE.print("Status: HTTP "); CONSOLE.println(statusCode); | |
if (statusCode >= 400) { | |
CONSOLE.println("Failed to get weather icon: invalid status code."); | |
renderErrorScreen("異常なステータスコードです"); | |
return -1; | |
} | |
// Get response header | |
while (httpClient.connected()) { | |
String line = httpClient.readStringUntil('\n'); | |
if (line == "\r") { | |
break; | |
} | |
} | |
// Get response body | |
int length = httpClient.read(imageBuf, IMAGE_BUFFER_SIZE); | |
// Close connection | |
httpClient.stop(); | |
renderProcessingScreen("天気アイコンを取得しました"); | |
return length; | |
} | |
String getWeatherIconFilePath(String iconCode) { | |
char filename[50] = { 0 }; | |
sprintf(filename, "%s/%s.png", IMAGE_ICON_DIR, iconCode); | |
// If weather icon already exists on SD card, return immediately its filename. | |
if (SD.exists(filename)) { | |
CONSOLE.print("Weather icon is exist on SD card: "); CONSOLE.println(iconCode); | |
renderProcessingScreen("SDカードからアイコンを取得しました"); | |
return filename; | |
} | |
// Otherwise, try to download from OpenWeatherMap | |
static uint8_t imageBuf[IMAGE_BUFFER_SIZE] = { 0 }; | |
int imageSize = getWeatherIconViaOWM(iconCode, imageBuf); | |
CONSOLE.print(imageSize); CONSOLE.println(" bytes received"); | |
// If download is successful, save to SD card | |
int writtenBytes = 0; | |
if (imageSize > 0) { | |
File file = SD.open(filename, FILE_WRITE); | |
writtenBytes = file.write(imageBuf, imageSize); | |
CONSOLE.print(writtenBytes); CONSOLE.println(" bytes wrote on SD."); | |
file.close(); | |
} | |
if (writtenBytes <= 0) { | |
CONSOLE.println("Failed to write file on SD card, abort."); | |
renderErrorScreen("天気アイコンの保存に失敗しました"); | |
return ""; | |
} | |
renderProcessingScreen("天気アイコンを保存しました"); | |
return filename; | |
} | |
void setup() { | |
M5.begin(); | |
// Setup font renderer | |
if (!render.loadFont(FONT_FILE_PATH)) { | |
CONSOLE.println("Failed to load font file."); | |
// Display error message on LCD using default font | |
M5.Lcd.setTextSize(2); | |
M5.Lcd.setTextColor(TFT_RED); | |
M5.Lcd.printf("Failed to load font file, abort."); | |
while(1); | |
} | |
render.enableAutoNewline(true); | |
// Initialize GSM modem | |
initializeGsmModem(); | |
} | |
void loop() | |
{ | |
M5.update(); | |
M5.Lcd.clear(TFT_BLACK); | |
// Get weather data | |
CONSOLE.println("Receiving weather data..."); | |
renderProcessingScreen("天気データを取得中…"); | |
String responseString = getWeatherJSON(); | |
// Parse received JSON data | |
CONSOLE.println("Processing response JSON data..."); | |
renderProcessingScreen("JSONオブジェクトを処理中…"); | |
StaticJsonDocument<2048> J_ROOT; | |
DeserializationError error = deserializeJson(J_ROOT, responseString); | |
if (error) { | |
CONSOLE.println("Failed to parse JSON!"); | |
CONSOLE.println(error.f_str()); | |
renderErrorScreen("JSONの処理に失敗しました"); | |
while(1); | |
} | |
// Extarct weather data from JSON Objects | |
JsonObject J_WEATHER = J_ROOT["weather"][0]; | |
JsonObject J_MAIN = J_ROOT["main"]; | |
JsonObject J_WIND = J_ROOT["wind"]; | |
String city = J_ROOT["name"].as<String>(); | |
String description = J_WEATHER["description"].as<String>(); | |
String iconCode = J_WEATHER["icon"].as<String>(); | |
int temperature = (int) J_MAIN["temp"].as<float>(); | |
int humidity = (int) J_MAIN["humidity"].as<float>(); | |
int windSpeed = J_WIND["speed"].as<int>(); | |
// Get weather image icon from SD or OpenWeatherMap | |
renderProcessingScreen("天気アイコンを取得中…"); | |
String imageFilePath = getWeatherIconFilePath(iconCode); | |
// Render main weather screen | |
renderMainScreen(city, imageFilePath, description, temperature, humidity, windSpeed); | |
// wait for press center button (Btn B) | |
do { | |
M5.update(); | |
} | |
while(M5.BtnB.isReleased()); | |
} | |
/** | |
* Draw center aligned text into boundary | |
* This is ROUGH calcuration, so it can't centering if str is mixed multi-byte characters. | |
*/ | |
void drawCenterAlignedText(const char *str, int bx, int by, int bw, int bh, int fontSize, bool useMultibyte) { | |
int tw = (float)fontSize * (float)((float)strlen(str) / (float)(useMultibyte ? 3 : 2)); | |
int th = (float)(fontSize * 1.1); | |
int marginX = (bw - tw > 0) ? ((float)(bw - tw) / 2) : 0; | |
int marginY = (bh - th > 0) ? ((float)(bh - th) / 2) : 0; | |
render.setTextSize(fontSize); | |
render.drawString(str, bx + marginX, by + marginY); | |
} | |
/** | |
* Render main weather screen. | |
*/ | |
void renderMainScreen(String city, String weatherIconPath, String description, int temperature, int humidity, int windSpeed) { | |
M5.Lcd.clear(BLACK); | |
render.setTextColor(TFT_WHITE); | |
// Render city name | |
M5.Lcd.drawRect(0, 0, 320, 40, TFT_ORANGE); | |
drawCenterAlignedText(city.c_str(), 0, 0, 320, 40, 30, true); | |
// Render weather icon (if exist) | |
if (weatherIconPath != "") { | |
M5.Lcd.drawPngFile(SD, weatherIconPath.c_str(), 0, 40, 120, 120, 0, 0, 1.2, 127); | |
} | |
// Render weather description | |
render.setTextColor(TFT_WHITE); | |
drawCenterAlignedText(description.c_str(), 120, 40, 180, 120, 36, true); | |
// Temperature | |
M5.Lcd.drawRect(0, 160, 100, 75, TFT_WHITE); | |
drawCenterAlignedText("気 温", 0, 160, 100, 20, 20, true); | |
render.setTextSize(32); | |
render.setCursor(10, 190); | |
render.printf("%-3d℃", temperature); | |
// Humidity | |
M5.Lcd.drawRect(110, 160, 100, 75, TFT_WHITE); | |
drawCenterAlignedText("湿 度", 110, 160, 100, 20, 20, true); | |
render.setTextSize(32); | |
render.setCursor(125, 190); | |
render.printf("%-3d%%", humidity); | |
// Wind speed | |
M5.Lcd.drawRect(220, 160, 100, 75, TFT_WHITE); | |
drawCenterAlignedText("風 速", 220, 160, 100, 20, 20, true); | |
render.setTextSize(32); | |
render.setCursor(230, 190); | |
render.printf("%-2dm/s", windSpeed); | |
} | |
/** | |
* Render processing screen | |
*/ | |
static int progressState = 0; | |
const static char* progressBar[6] = {"○●●●●", "●○●●●", "●●○●●", "●●●○●", "●●●●○", "●●●●●"}; | |
void renderProcessingScreen(String message) { | |
drawCenterAlignedText("★処理中です★", 0, 0, 320, 25, 25, true); | |
M5.Lcd.fillRect(0, 100, 320, 140, TFT_BLACK); | |
drawCenterAlignedText(message.c_str(), 0, 100, 320, 40, 20, true); | |
drawCenterAlignedText(progressBar[progressState % 6], 0, 200, 320, 20, 20, true); | |
progressState++; | |
} | |
/** | |
* Render error screen | |
*/ | |
void renderErrorScreen(String message) { | |
M5.Lcd.clear(TFT_BLACK); | |
render.setTextColor(TFT_RED); | |
drawCenterAlignedText("!! ERROR !!", 0, 0, 320, 25, 25, false); | |
drawCenterAlignedText(message.c_str(), 0, 100, 320, 40, 20, true); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment