Hardware components | ||||||
![]() |
| × | 1 | |||
Software apps and online services | ||||||
![]() |
| |||||
Orbi 2.0 – Satellite Health & Space News Badge is a portable, WiFi-enabled M5Cardputer device that brings real-time satellite intelligence and space news into your pocket.
It downloads live TLE data from Celestrak, caches it locally on an SD card, and continuously cycles through active satellites to estimate their operational “health” using orbital parameters like eccentricity and mean motion drift.
Each satellite is enriched and classified (NAV, COMM, HUMAN), then evaluated through a custom risk model that generates:
✅ A dynamic risk score and health status (Minimal → Critical)
✅ Threat labels like Orbit Decay, Unstable Orbit, or Comms Hijack
✅ Context-aware risk reasoning (e.g., drag, instability, high-value target)
✅ Satellite identity (name, NORAD ID, purpose)
With a single key press, Orbi switches into live space news mode, fetching and displaying the latest headlines from SpaceNews.
All of this runs on a compact M5Cardputer device with offline caching, auto-updates, and a clean, mission-control-style UI.
It’s not just tracking satellites—it’s interpreting space.
Orbi 2.0: Orbit Intelligence & Space News Hub
C/C++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.
This badge is designed for:
- Educational demos
- Space enthusiasts
- Makers wanting to track satellite data freshness
#include <M5Cardputer.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <SD.h>
#include <time.h>
#define ORANGE 0xFD20
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 0;
const int daylightOffset_sec = 0;
File satFile;
String satName="---", satNORAD="---";
String satPurpose="UNK";
int scoreTLE, scoreOrbit, scoreActivity, scoreOperator;
int riskScore, missionRisk;
float eccentricity=0.0, meanMotionDot=0.0;
int mode = 0;
// NEWS
String newsHeadlines[5];
String newsDates[5];
int newsCount = 0;
int currentNewsIndex = 0;
// AUTO UPDATE
unsigned long lastUpdate = 0;
const unsigned long updateInterval = 6UL * 60UL * 60UL * 1000UL;
/* ---------------- UTILS ---------------- */
String getTimeStr() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) return "--:--";
char buf[10];
strftime(buf, sizeof(buf), "%H:%M", &timeinfo);
return String(buf);
}
void drawBattery() {
int level = M5.Power.getBatteryLevel();
int x = 200, y = 5, w = 30, h = 10;
M5.Display.drawRect(x, y, w, h, WHITE);
int fill = (level * (w - 2)) / 100;
uint16_t c = GREEN;
if (level < 40) c = YELLOW;
if (level < 20) c = RED;
M5.Display.fillRect(x + 1, y + 1, fill, h - 2, c);
}
/* ---------------- DB CACHE ---------------- */
bool isDBFresh() {
File meta = SD.open("/satdb.meta");
if (!meta) return false;
String t = meta.readStringUntil('\n');
meta.close();
unsigned long last = t.toInt();
unsigned long now = millis();
return (now - last < updateInterval);
}
/* ---------------- SPLASH ---------------- */
void showOrbi() {
M5.Display.fillScreen(BLACK);
int cx = 120, cy = 70; // center
int r = 40;
// Draw static orbit circle
M5.Display.drawCircle(cx, cy, r, BLUE);
// Satellite point
int satX = cx + r;
int satY = cy;
for (int angle = 0; angle < 360; angle += 10) {
// Clear previous sweep
M5.Display.fillScreen(BLACK);
M5.Display.drawCircle(cx, cy, r, BLUE);
// Radar sweep line
float rad = angle * DEG_TO_RAD;
int x = cx + r * cos(rad);
int y = cy + r * sin(rad);
M5.Display.drawLine(cx, cy, x, y, GREEN);
// Satellite moving on orbit
float satRad = (angle + 90) * DEG_TO_RAD;
satX = cx + r * cos(satRad);
satY = cy + r * sin(satRad);
M5.Display.fillCircle(satX, satY, 3, WHITE);
// ORBI text fade-in effect
if (angle > 180) {
M5.Display.setTextSize(3);
M5.Display.setTextColor(CYAN);
M5.Display.setCursor(60, 120);
M5.Display.println("ORBI");
}
delay(20);
}
// Final screen
M5.Display.fillScreen(BLACK);
M5.Display.setTextSize(4);
M5.Display.setTextColor(CYAN);
M5.Display.setCursor(40, 40);
M5.Display.println("ORBI");
M5.Display.setTextSize(2);
M5.Display.setCursor(80, 90);
M5.Display.println("v2.0");
delay(700);
}
/* ---------------- WIFI ---------------- */
void connectWiFi() {
WiFi.begin(ssid,password);
unsigned long start=millis();
while(WiFi.status()!=WL_CONNECTED && millis()-start<8000){
delay(300);
}
}
/* ---------------- DOWNLOAD ---------------- */
void downloadDB() {
WiFiClientSecure client;
client.setInsecure();
if(!client.connect("celestrak.org",443)) return;
client.print("GET /NORAD/elements/gp.php?GROUP=active&FORMAT=csv HTTP/1.1\r\nHost: celestrak.org\r\nConnection: close\r\n\r\n");
SD.remove("/satdb.csv");
File f = SD.open("/satdb.csv", FILE_WRITE);
bool body=false;
unsigned long timeout=millis();
while((client.connected()||client.available()) && millis()-timeout<10000){
if(client.available()){
String line=client.readStringUntil('\n');
if(line=="\r"){body=true; continue;}
if(body) f.println(line);
timeout=millis();
}
}
f.close();
client.stop();
File meta = SD.open("/satdb.meta", FILE_WRITE);
if (meta) {
meta.println(String(millis()));
meta.close();
}
}
/* ---------------- LOAD SAT ---------------- */
bool loadSat() {
if(!satFile) return false;
while(satFile.available()){
String line = satFile.readStringUntil('\n');
if(line.length()<100) continue;
String f[20];
int c=0,s=0;
for(int i=0;i<line.length();i++){
if(line[i]==','){
f[c++]=line.substring(s,i);
s=i+1;
if(c>=19) break;
}
}
f[c]=line.substring(s);
satName=f[0];
satNORAD=f[11];
eccentricity=f[4].toFloat();
meanMotionDot=f[15].toFloat();
return true;
}
satFile.seek(0);
return false;
}
/* ---------------- ENRICH ---------------- */
void enrichSat() {
String n=satName;
n.toUpperCase();
if(n.indexOf("NAV")>=0) satPurpose="NAV";
else if(n.indexOf("COMM")>=0) satPurpose="COMM";
else if(n.indexOf("ISS")>=0) satPurpose="HUMAN";
else satPurpose="UNK";
}
/* ---------------- RISK ---------------- */
void calcHealth() {
scoreTLE=80;
scoreOrbit=constrain(100-(int)(eccentricity*10000),0,100);
scoreActivity=constrain(100-(int)(abs(meanMotionDot)*100000),0,100);
scoreOperator=40;
if(satPurpose=="NAV") missionRisk=90;
else if(satPurpose=="COMM") missionRisk=80;
else if(satPurpose=="HUMAN") missionRisk=95;
else missionRisk=60;
riskScore =
(100-scoreTLE)*0.25 +
(100-scoreOrbit)*0.20 +
(100-scoreActivity)*0.20 +
(100-scoreOperator)*0.20 +
missionRisk*0.15;
riskScore=constrain(riskScore,0,100);
}
/* ---------------- LABELS ---------------- */
String getStatus() {
if(riskScore>85) return "CRITICAL";
if(riskScore>70) return "HIGH";
if(riskScore>55) return "ELEVATED";
if(riskScore>35) return "LOW";
return "MINIMAL";
}
String getThreatLabel() {
if(scoreActivity<40) return "ORBIT DECAY";
if(scoreOrbit<50) return "UNSTABLE ORBIT";
if(satPurpose=="NAV" && riskScore>70) return "NAV SPOOF";
if(satPurpose=="COMM" && riskScore>75) return "COMMS HIJACK";
return "NOMINAL";
}
/* ---------------- NEW: RISK REASON ---------------- */
String getRiskReason() {
if (scoreActivity < 40)
return "HIGH DRAG / DECAY";
if (scoreOrbit < 50)
return "ORBIT INSTABILITY";
if (scoreTLE < 50)
return "STALE TLE DATA";
if (scoreOperator < 50)
return "WEAK OPS SECURITY";
if (missionRisk > 85)
return "HIGH VALUE TARGET";
return "NORMAL CONDITIONS";
}
/* ---------------- UI ---------------- */
void drawUI() {
M5.Display.fillScreen(BLACK);
M5.Display.drawRect(2,2,236,130,BLUE);
M5.Display.setTextSize(2);
M5.Display.setCursor(10,8);
M5.Display.println("ORBI v2.0");
M5.Display.setTextSize(1);
M5.Display.setCursor(150,8);
M5.Display.println(getTimeStr());
drawBattery();
M5.Display.drawLine(5,25,235,25,BLUE);
M5.Display.setCursor(10,30);
M5.Display.println("SAT: "+satName);
M5.Display.setCursor(10,45);
M5.Display.println("NORAD: "+satNORAD);
M5.Display.setTextSize(1);
M5.Display.drawRect(10,60,100,35,ORANGE);
M5.Display.setCursor(15,68);
M5.Display.printf("RISK: %d",riskScore);
M5.Display.drawRect(120,60,100,35,GREEN);
M5.Display.setCursor(125,68);
M5.Display.println(getStatus());
// Threat
M5.Display.drawRect(10,100,210,18,RED);
M5.Display.setCursor(15,104);
M5.Display.println(getThreatLabel());
M5.Display.setCursor(15,120);
M5.Display.println(getRiskReason());
M5.Display.setCursor(10,135);
M5.Display.println("/:NEXT ;:NEWS");
}
/* ---------------- NEWS ---------------- */
void showNews() {
mode = 2;
M5.Display.fillScreen(BLACK);
WiFiClientSecure client;
client.setInsecure();
if (!client.connect("spacenews.com", 443)) return;
client.print("GET /feed/ HTTP/1.1\r\nHost: spacenews.com\r\nConnection: close\r\n\r\n");
newsCount = 0;
bool body = false;
while (client.connected() || client.available()) {
String line = client.readStringUntil('\n');
if (line == "\r") { body = true; continue; }
if (body && line.indexOf("<title>") >= 0) {
String t = line.substring(line.indexOf("<title>")+7, line.indexOf("</title>"));
if (t.indexOf("SpaceNews") >= 0) continue;
newsHeadlines[newsCount] = t;
}
if (body && line.indexOf("<pubDate>") >= 0) {
newsDates[newsCount] = line.substring(line.indexOf("<pubDate>")+9, line.indexOf("</pubDate>"));
newsCount++;
if (newsCount >= 5) break;
}
}
client.stop();
currentNewsIndex = 0;
displayCurrentNews();
}
void displayCurrentNews() {
M5.Display.fillRect(0, 0, 240, 135, BLACK);
M5.Display.setTextSize(2);
M5.Display.setCursor(40, 5);
M5.Display.println("SPACE NEWS");
// Content (slightly bigger than before)
M5.Display.setTextSize(1);
M5.Display.setCursor(5, 30);
String t = newsHeadlines[currentNewsIndex];
for (int i = 0; i < t.length(); i += 32) {
M5.Display.println(t.substring(i, i + 32));
}
// Date
M5.Display.setCursor(5, 95);
M5.Display.println(newsDates[currentNewsIndex]);
// Footer
M5.Display.setCursor(5, 115);
M5.Display.println(";:PREV .:NEXT /:EXIT");
}
/* ---------------- SETUP ---------------- */
void setup() {
auto cfg=M5.config();
M5.begin(cfg);
M5Cardputer.begin();
M5.Display.setRotation(1);
showOrbi();
M5.Display.fillScreen(BLACK);
M5.Display.setCursor(5,5);
SD.begin();
M5.Display.println("WIFI...");
connectWiFi();
if(WiFi.status()==WL_CONNECTED){
M5.Display.println("OK");
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
if (!isDBFresh()) {
M5.Display.println("DB: DOWNLOADING...");
downloadDB();
lastUpdate = millis();
M5.Display.println("DB: UPDATED");
} else {
M5.Display.println("DB: CACHED");
}
} else {
M5.Display.println("OFFLINE");
}
satFile=SD.open("/satdb.csv");
if(satFile){
loadSat();
enrichSat();
calcHealth();
}
delay(1000);
drawUI();
}
/* ---------------- LOOP ---------------- */
void loop() {
M5.update();
M5Cardputer.update();
if (mode == 0) {
if (M5Cardputer.Keyboard.isKeyPressed('/')) {
if (loadSat()) {
enrichSat();
calcHealth();
drawUI();
}
delay(200);
}
if (M5Cardputer.Keyboard.isKeyPressed(',')) {
if (loadSat()) {
enrichSat();
calcHealth();
drawUI();
}
delay(200);
}
if (M5Cardputer.Keyboard.isKeyPressed(';')) {
showNews();
delay(300);
}
}
else if (mode == 2) {
if (M5Cardputer.Keyboard.isKeyPressed('.')) {
if (newsCount > 0) {
currentNewsIndex = (currentNewsIndex + 1) % newsCount;
displayCurrentNews();
}
delay(200);
}
if (M5Cardputer.Keyboard.isKeyPressed(';')) {
if (newsCount > 0) {
currentNewsIndex--;
if (currentNewsIndex < 0) currentNewsIndex = newsCount - 1;
displayCurrentNews();
}
delay(200);
}
if (M5Cardputer.Keyboard.isKeyPressed('/')) {
mode = 0;
drawUI();
delay(200);
}
}
if (WiFi.status() == WL_CONNECTED && millis() - lastUpdate > updateInterval) {
downloadDB();
satFile = SD.open("/satdb.csv");
lastUpdate = millis();
}
}










Comments