Druk
Published © GPL3+

Orbi - Satellite Health Badge

Orbi is a portable, WiFi-enabled display that continuously fetches real-time TLE data from Celestrak to visualize satellite health.

IntermediateFull instructions provided254
Orbi - Satellite Health Badge

Things used in this project

Hardware components

M5StickC PLUS ESP32-PICO Mini IoT Development Kit
M5Stack M5StickC PLUS ESP32-PICO Mini IoT Development Kit
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Schematic

This project uses the M5StickC Plus as an all-in-one microcontroller with built-in display, WiFi, and power management. No additional wiring is required.

Code

How do you use Orbi ?

C/C++
1) Flash this code onto an M5StickC Plus (or Plus2) via Arduino IDE.
2) Enter your WiFi credentials in the lines below:
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
3) Power on the device.
4) The badge connects to WiFi and fetches TLE data automatically.
5) Every 10 seconds, it cycles to the next satellite, showing live info.

This badge is designed for:
- Educational demos
- Space enthusiasts
- Makers wanting to track satellite data freshness
#include <M5Unified.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>

// WiFi credentials
const char* ssid = "YOUR_SSID"; 
const char* password = "YOUR_PASSWORD";

#define NUM_SATELLITES 20

const char* satNames[NUM_SATELLITES] = {
  "ISS", "Hubble", "NOAA 15", "NOAA 18", "NOAA 19",
  "Terra", "Aqua", "Landsat 8", "Envisat", "Sentinel-1A",
  "Sentinel-2A", "Jason-3", "TDRS 10", "TDRS 11", "Starlink 1",
  "Starlink 2", "Starlink 3", "OneWeb 1", "OneWeb 2", "CubeSat Demo"
};

const char* satNORAD[NUM_SATELLITES] = {
  "25544", "20580", "25338", "28654", "33591",
  "25994", "27424", "39084", "27386", "39634",
  "40697", "41240", "27689", "28366", "44060",
  "44713", "44714", "44875", "44876", "43017"
};

const char* satCountries[NUM_SATELLITES] = {
  "Intl", "USA", "USA", "USA", "USA",
  "USA", "USA", "USA", "ESA", "ESA",
  "ESA", "USA", "USA", "USA", "USA",
  "USA", "USA", "UK", "UK", "Intl"
};

const char* satPurposes[NUM_SATELLITES] = {
  "Research", "Astronomy", "Weather", "Weather", "Weather",
  "Earth Obs", "Earth Obs", "Earth Obs", "Earth Obs", "Radar Imaging",
  "Optical Imaging", "Ocean Topography", "Relay Comm", "Relay Comm", "Internet",
  "Internet", "Internet", "Internet", "Internet", "Demo"
};

float tle_age_days = 0.0;
float tle_age_log[NUM_SATELLITES][3] = {0};
int currentSatIndex = 0;

void fetchTLE(const char* norad_id) {
  WiFiClientSecure client;
  client.setInsecure();

  if (!client.connect("celestrak.org", 443)) {
    tle_age_days = 999;
    return;
  }

  String url = "/NORAD/elements/gp.php?CATNR=" + String(norad_id);
  client.print(String("GET ") + url + " HTTP/1.1\r\nHost: celestrak.org\r\nConnection: close\r\n\r\n");

  String line;
  bool foundEpoch = false;
  while (client.connected()) {
    line = client.readStringUntil('\n');
    if (line.startsWith("1 ")) {
      String epochStr = line.substring(18,32);
      double epoch = epochStr.toDouble();
      int year = int(epoch / 1000);
      double doy = epoch - year*1000;
      struct tm timeinfo;
      timeinfo.tm_year = year + 100;
      timeinfo.tm_mon = 0;
      timeinfo.tm_mday = 1;
      timeinfo.tm_hour = 0;
      timeinfo.tm_min = 0;
      timeinfo.tm_sec = 0;
      time_t epochTime = mktime(&timeinfo) + (doy * 86400);
      time_t now = time(nullptr);
      tle_age_days = double(now - epochTime) / 86400.0;
      foundEpoch = true;
      break;
    }
  }
  if (!foundEpoch) tle_age_days = 999;

  // Update rolling log
  tle_age_log[currentSatIndex][2] = tle_age_log[currentSatIndex][1];
  tle_age_log[currentSatIndex][1] = tle_age_log[currentSatIndex][0];
  tle_age_log[currentSatIndex][0] = tle_age_days;
}

void displayHealth() {
  M5.Lcd.fillScreen(BLACK);

  int health = 100;
  if (tle_age_days > 5) health -= 20;
  if (tle_age_days > 10) health -= 40;
  if (tle_age_days > 20) health -= 60;
  if (tle_age_days > 30) health -= 80;

  String status;
  if (health >= 80) status = "Excellent";
  else if (health >= 60) status = "Good";
  else if (health >= 40) status = "Fair";
  else if (health >= 20) status = "Poor";
  else status = "Critical";

  int screenWidth = M5.Lcd.width();

  // Title
  M5.Lcd.setTextSize(2);
  M5.Lcd.setTextColor(TFT_CYAN, TFT_BLACK);
  M5.Lcd.setCursor(20, 5);
  M5.Lcd.print(satNames[currentSatIndex]);

  // Divider
  M5.Lcd.drawLine(0, 25, screenWidth, 25, TFT_DARKGREY);

  // Country
  M5.Lcd.setTextSize(1);
  M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
  M5.Lcd.setCursor(10, 30);
  M5.Lcd.print("Country: ");
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.print(satCountries[currentSatIndex]);

  // Purpose with wrapping
  M5.Lcd.setCursor(10, 45);
  M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
  M5.Lcd.print("Purpose: ");
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);

  if (strlen(satPurposes[currentSatIndex]) > 12) {
    char buf1[13];
    strncpy(buf1, satPurposes[currentSatIndex], 12);
    buf1[12] = '\0';
    M5.Lcd.println(buf1);
    M5.Lcd.setCursor(10, 58);
    M5.Lcd.print(&satPurposes[currentSatIndex][12]);
  } else {
    M5.Lcd.print(satPurposes[currentSatIndex]);
  }

  // TLE Age
  M5.Lcd.setCursor(10, 70);
  M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
  M5.Lcd.print("TLE Age: ");
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.printf("%.1f days", tle_age_days);

  // Health bar background
  M5.Lcd.drawRect(10, 85, screenWidth - 20, 20, TFT_WHITE);
  int barWidth = map(health, 0, 100, 0, screenWidth - 20);
  uint16_t color;
  if (health >= 80) color = GREEN;
  else if (health >= 60) color = YELLOW;
  else if (health >= 40) color = ORANGE;
  else color = RED;
  M5.Lcd.fillRect(10, 85, barWidth, 20, color);

  // Health % text
  M5.Lcd.setTextColor(TFT_BLACK, color);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(screenWidth / 2 - 20, 87);
  M5.Lcd.printf("%d%%", health);

  // Health status
  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.setTextSize(1);
  M5.Lcd.setCursor(10, 110);
  M5.Lcd.printf("Status: %s", status.c_str());

  // Divider
  M5.Lcd.drawLine(0, 120, screenWidth, 120, TFT_DARKGREY);

  // Last 3 Ages
  M5.Lcd.setCursor(10, 125);
  M5.Lcd.setTextColor(TFT_YELLOW, TFT_BLACK);
  M5.Lcd.println("Last 3 Ages:");

  M5.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
  M5.Lcd.setCursor(10, 140);
  M5.Lcd.printf("1: %.1f days", tle_age_log[currentSatIndex][0]);

  M5.Lcd.setCursor(10, 155);
  M5.Lcd.printf("2: %.1f days", tle_age_log[currentSatIndex][1]);

  M5.Lcd.setCursor(10, 170);
  M5.Lcd.printf("3: %.1f days", tle_age_log[currentSatIndex][2]);
}

void setup() {
  auto cfg = M5.config();
  M5.begin(cfg);
  M5.Lcd.setRotation(0);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }

  configTime(0,0,"pool.ntp.org","time.nist.gov");
  while(time(nullptr) < 100000){
    delay(500);
  }
}

void loop() {
  fetchTLE(satNORAD[currentSatIndex]);
  displayHealth();
  delay(10000);

  currentSatIndex = (currentSatIndex + 1) % NUM_SATELLITES;
}

Credits

Druk
5 projects • 2 followers

Comments