Druk
Published

Orbi 2.0: Orbit Intelligence & Space News Hub

Monitor satellite health and stream real-time space news in one powerful handheld device.

IntermediateFull instructions provided18
Orbi 2.0: Orbit Intelligence & Space News Hub

Things used in this project

Hardware components

Cardputer-Adv
M5Stack Cardputer-Adv
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

sch_m5cardputer_sch_01_mCdLOT3Lhv.png

Code

Orbi 2.0: Orbit Intelligence & Space News Hub

C/C++
) Flash this code onto an M5StickCardputer Adv 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.

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();
  }
}

Credits

Druk
3 projects • 1 follower

Comments