Satera Sorami
Published © GPL3+

【Aurora visibility Device】Aurora Watcher Mini ver.1

Pocket-sized space weather forecast. Continuously obtain real-time Aurora information to support decision-making during outdoor.

IntermediateShowcase (no instructions)5 hours3
【Aurora visibility Device】Aurora Watcher Mini ver.1

Things used in this project

Hardware components

M5Stack M5StickC Plus2
×1
M5Stack GPS/BDS Unit v1.1 (AT6668)
×1
StickC Bridge
×1

Software apps and online services

Arduino IDE
Arduino IDE
NOAA Space Weather Prediction Center API

Story

Read more

Schematics

接続図

M5StickCPlus2とGPSユニットをGroveコネクターで接続

Code

Aurora Watcher Mini Ver1.0

Arduino
Real-time aurora forecast device
#include <M5Unified.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <TinyGPSPlus.h>
#include "sun_icon.h"       //Please make your own 32x32
#include "cute_icon.h"      //Please make your own 64x64
#include "title_image.h"    //Please make your own 240x135
#include "wind.h"           //Please make your own 20x20
#include "secrets.h"       // Please make your SSID and Pass file

#define GRAY     0x8410
#define DARKGREY 0x4208

// ===== API URL =====
const char* apiURL   = "https://services.swpc.noaa.gov/json/planetary_k_index_1m.json";
const char* plasmaURL = "https://services.swpc.noaa.gov/products/solar-wind/plasma-1-day.json";
const char* magURL    = "https://services.swpc.noaa.gov/products/solar-wind/mag-1-day.json";

// ===== グローバル変数 =====
M5Canvas canvas(&M5.Lcd);
float globalKp = 0.0;
String level = "----";
String alertComment = "Loading...";
int currentScreen = 0;

TinyGPSPlus gps;
HardwareSerial GPSRaw(2);  // RX=G33, TX=G32
float currentLat = 0.0;
float currentLon = 0.0;
bool hasLocation = false;

// 太陽風スピードとBz
float solarWindSpeed = 0.0;
float bzComponent    = 0.0;

// ===== WiFi接続 =====
void connectWiFi() {
  WiFi.begin(WIFI_SSID1, WIFI_PASS1);
  for (int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) delay(500);

  if (WiFi.status() != WL_CONNECTED) {
    WiFi.begin(WIFI_SSID2, WIFI_PASS2);
    for (int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) delay(500);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[WARN] WiFi connection failed. Running in offline mode.");
  }
}

// ===== 判定関数 =====
String getGeomagneticLevel(float kp) {
  if (kp < 5) return "G0: Quiet";
  else if (kp < 6) return "G1: Slight";
  else if (kp < 7) return "G2: Active";
  else return "G3+: Alert";
}

String getComment(float kp) {
  if (kp < 5) return "All clear.";
  else if (kp < 6) return "Minor change.";
  else if (kp < 7) return "Watch it.";
  else return "Be ready!";
}

String getAuroraChance(float kp, float lat, float solarWindSpeed, float bzValue) {
  String baseChance = "";

  if (lat > 64.0) {
    if (kp < 4) baseChance = "Unlikely.";
    else if (kp < 5) baseChance = "Weak.";
    else if (kp < 6) baseChance = "Slight.";
    else if (kp < 7) baseChance = "Visible.";
    else baseChance = "Strong!";
  } else if (lat > 42.0) {
    if (kp < 4) baseChance = "No chance.";
    else if (kp < 5) baseChance = "Very unlikely.";
    else if (kp < 6) baseChance = "Slight chance.";
    else if (kp < 7) baseChance = "Possible.";
    else baseChance = "Likely!";
  } else {
    if (kp < 6) baseChance = "No chance.";
    else if (kp < 7) baseChance = "Very unlikely.";
    else if (kp < 8) baseChance = "Slight chance.";
    else baseChance = "Possible!";
  }

  // 太陽風の条件で補正
  bool strongSolarActivity = (solarWindSpeed >= 500.0) && (bzValue <= -5.0);

  if (strongSolarActivity) {
    if (baseChance == "Unlikely.") return "Slight (Boosted)";
    else if (baseChance == "Weak.") return "Slight (Boosted)";
    else if (baseChance == "Slight.") return "Visible (Boosted)";
    else if (baseChance == "Visible.") return "Strong! (Boosted)";
    else if (baseChance == "Very unlikely.") return "Slight chance (Boosted)";
    else if (baseChance == "Slight chance.") return "Possible (Boosted)";
    else if (baseChance == "Possible.") return "Likely! (Boosted)";
    else if (baseChance == "Likely!") return "Strong! (Boosted)";
  }

  return baseChance;
}



// ===== API取得 =====
void fetchSpaceWeather() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[ERROR] No WiFi connection.");
    return;
  }

  HTTPClient http;
  http.begin(apiURL);
  int httpCode = http.GET();

  if (httpCode == 200) {
    String payload = http.getString();
    DynamicJsonDocument doc(8192);
    if (deserializeJson(doc, payload) == DeserializationError::Ok) {
      JsonObject latest = doc[doc.size() - 1];
      if (latest.containsKey("estimated_kp")) globalKp = latest["estimated_kp"].as<float>();
      else if (latest.containsKey("kp_index")) globalKp = latest["kp_index"].as<float>();
      level = getGeomagneticLevel(globalKp);
      alertComment = getComment(globalKp);
    } else {
      alertComment = "JSON Error!";
    }
  } else {
    alertComment = "API Error...";
  }

  http.end();
}

// ===== 画面描画 =====
void drawTitle() {
  canvas.createSprite(240, 135);
  canvas.pushImage(0, 0, 240, 135, title_image);
  canvas.pushSprite(0, 0);
  canvas.deleteSprite();
}

//Kp値表示
void drawKpInfo() {
  canvas.createSprite(240, 135);
  canvas.fillScreen(BLACK);
  canvas.pushImage(20, 10, sun_width, sun_height, sun_icon);
  canvas.pushImage(5, 65, cute_width, cute_height, cute_icon);

  uint16_t textColor = (globalKp < 5) ? WHITE : (globalKp < 6) ? YELLOW : (globalKp < 7) ? ORANGE : RED;
  canvas.setTextColor(textColor);
  canvas.setTextSize(2);
  canvas.setCursor(80, 10);
  canvas.printf("Kp: %.1f", globalKp);

  canvas.setCursor(80, 40);
  canvas.print(level);

  canvas.setCursor(80, 70);
  canvas.print(alertComment);

  time_t now = time(nullptr);
  struct tm *timeInfo = localtime(&now);
  char timeStr[32];
  strftime(timeStr, sizeof(timeStr), "JST: %Y/%m/%d %H:%M", timeInfo);

  canvas.setTextColor(WHITE);
  canvas.setTextSize(1);
  canvas.setCursor(75, 115);
  canvas.print(timeStr);

  canvas.pushSprite(0, 0);
  canvas.deleteSprite();
}

//--オーロラ判定画面
void drawAuroraChance() {
  canvas.createSprite(240, 135);
  canvas.fillScreen(BLACK);

  canvas.setTextColor(WHITE);
  canvas.setTextSize(2);
  canvas.setCursor(35, 5);
  canvas.println("AURORA CHANCE");

  // 地点表示関数
  auto drawLocation = [&](int y, const char* icon, const char* name, String msg, uint16_t color) {
    canvas.setTextColor(WHITE);
    canvas.setTextSize(1.4);
    canvas.setCursor(5, y);
    canvas.printf("%s %s", icon, name);

    canvas.setTextColor(color);
    canvas.setCursor(100, y);
    canvas.printf("| %s", msg.c_str());

    const char* mark = " ";
    if (msg.indexOf("Likely") >= 0) mark = "OK";
    else if (msg.indexOf("Slight") >= 0) mark = "!?";
    else if (msg.indexOf("not ready") >= 0) mark = "x";
    else mark = "x";

    canvas.setTextColor(WHITE);
    canvas.setCursor(215, y);
    canvas.print(mark);
  };

  // 各地点


  drawLocation(35, ":", "Reykjavik", getAuroraChance(globalKp, 64.1, solarWindSpeed, bzComponent), GREEN);
  drawLocation(60, ":", "Sapporo", getAuroraChance(globalKp, 43.1, solarWindSpeed, bzComponent), YELLOW);



 if (hasLocation) {
    String result = getAuroraChance(globalKp, currentLat, solarWindSpeed, bzComponent);
    uint16_t color = result.indexOf("Likely") >= 0 ? RED :
                     result.indexOf("Slight") >= 0 ? ORANGE : GRAY;
    drawLocation(85, ":", "Your Spot", result, color);
  } else {
    drawLocation(85, ":", "Your Spot", "GPS Nothing", DARKGREY);
  }

  // Kp値
  canvas.setTextColor(WHITE);
  canvas.setTextSize(1.7);
  canvas.setCursor(150, 115);
  canvas.printf("Kp: %.1f", globalKp);

  canvas.pushSprite(0, 0);
  canvas.deleteSprite();
}

// 太陽風とBzのデータ取得関数を追加
void fetchSolarData() {
  // Solar Wind Speed
  HTTPClient http;
  http.begin(plasmaURL);
  int code = http.GET();
  if (code == 200) {
    String payload = http.getString();
    DynamicJsonDocument doc(8192);
    if (deserializeJson(doc, payload) == DeserializationError::Ok) {
      JsonArray array = doc.as<JsonArray>();
      JsonArray last = array[array.size() - 1];
      solarWindSpeed = last[2].as<float>();  // km/s
    }
  }
  http.end();

  // IMF Bz
  http.begin(magURL);
  code = http.GET();
  if (code == 200) {
    String payload = http.getString();
    DynamicJsonDocument doc(8192);
    if (deserializeJson(doc, payload) == DeserializationError::Ok) {
      JsonArray array = doc.as<JsonArray>();
      JsonArray last = array[array.size() - 1];
      bzComponent = last[2].as<float>();  // nT
    }
  }
  http.end();
}

//データ表示用UI関数を追加

void drawSolarWindInfo() {
  canvas.createSprite(240, 135);
  canvas.fillScreen(BLACK);

  canvas.setTextColor(WHITE);
  canvas.setTextSize(2);
  canvas.setCursor(10, 10);
  canvas.println("SOLAR WIND");


 // 「SOLAR WIND」の右横に20x20の風アイコンを表示
 canvas.drawBitmap(140, 5, wind_bits, wind_width, wind_height, WHITE);



  canvas.setTextSize(2);
  canvas.setCursor(10, 43);
  canvas.printf("Speed: %.1f km/s", solarWindSpeed);
  canvas.setCursor(10, 65);
  if (solarWindSpeed >= 500)
    canvas.setTextColor(GREEN);
  else
    canvas.setTextColor(RED);
  canvas.println((solarWindSpeed >= 500) ? "Impact: Possible" : "Impact: Low");

  canvas.setTextColor(WHITE);
  canvas.setCursor(10, 87);
  canvas.printf("IMF Bz: %.1f nT", bzComponent);
  canvas.setCursor(10, 110);
  if (bzComponent <= -5)
    canvas.setTextColor(GREEN);
  else
    canvas.setTextColor(RED);
  canvas.println((bzComponent <= -5) ? "Aurora Risk: High" : "Aurora Risk: Low");

  canvas.pushSprite(0, 0);
  canvas.deleteSprite();
}



// ===== setup & loop =====
void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(BLACK);
  Serial.begin(115200);

  GPSRaw.begin(115200, SERIAL_8N1, 33, 32);
  Serial.println("GPS start...");

  connectWiFi();
  configTime(9 * 3600, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");

  fetchSpaceWeather();
  drawTitle();
}

void loop() {
  M5.update();

  while (GPSRaw.available()) {
    gps.encode(GPSRaw.read());
  }

  if (gps.location.isValid()) {
    currentLat = gps.location.lat();
    currentLon = gps.location.lng();
    hasLocation = true;
  }

  if (M5.BtnA.wasPressed()) {
    if (currentScreen == 1) {
      fetchSolarData();
      drawSolarWindInfo();
      currentScreen = 0;
    } else {
      drawKpInfo();
      currentScreen = 1;
    }
  }

  if (M5.BtnB.wasPressed()) {
    drawAuroraChance();
    currentScreen = 2;
  }

  delay(10);
}

Credits

Satera Sorami
1 project • 0 followers
Hello, I'm Soramo Satera! New VTuber ☆ I love space and device building!

Comments