Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
/*
* 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, "api.openweathermap.org", 80);
// Send request
int err = httpClient.get("/data/2.5/weather?q=Yokohama&appid=<YOUR_API_KEY>&lang=ja&units=metric");
if (err != 0) {
CONSOLE.println("Failed to get weather: connection failed.");
renderErrorScreen("OWMとの接続に失敗しました");
return "";
}
renderProcessingScreen("OWMに接続しました");
// 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