Hardware components | ||||||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
| |||||
Orbi - Satellite Health Badge is a portable, WiFi-enabled display that continuously fetches real-time TLE data from Celestrak to estimate and visualize satellite "health."
The badge rotates through 20 popular Earth observation, communication, and research satellites (like ISS, Starlink, NOAA), showing:
✅ The satellite name, country, and purpose
✅ Age of the TLE data (a freshness indicator)
✅ An intuitive health bar and status
✅ Rolling log of the last 3 update ages
All on a pocket-sized M5StickC Plus screen.
It’s like having a mission control dashboard in your hand.
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
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;
}






Comments