#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <SPI.h>
#include <Sgp4.h>
#include <TFT_eSPI.h>
#include "adventpro_regular18pt.h"
#include "MyFont.h"
#include "colors.h"
#define MYFONT32 &myFont32pt8b
#define WIFI_SSID "Wokwi-GUEST"
#define TLE_ISS_URL "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=tle"
#define TLE_TIANGONG_URL "https://iss-tracker.com/include/tledata-TIANGONG.txt"
#define USER_LAT 52.23f
#define USER_LON 21.01f
#define POSITION_INTERVAL 1000UL
#define TLE_REFRESH_INTERVAL 21600000UL
#define COLOR_OCEAN 0x0217
#define COLOR_LAND 0x2C67
#define COLOR_PANEL 0x2965
#define COLOR_GRID 0x0015
#define COLOR_EQUATOR 0x01E0
TFT_eSPI tft = TFT_eSPI();
struct Satellite {
const char *name;
const char *norad;
uint16_t color;
Sgp4 sgp4;
char tleName[64];
char tle1[80];
char tle2[80];
float lat;
float lon;
float alt;
int16_t prevX;
int16_t prevY;
bool tleValid;
bool positionValid;
};
Satellite satellites[2] = {
{"ISS", "25544", TFT_YELLOW, Sgp4(), "", "", "", 0.0f, 0.0f, 0.0f, -1, -1, false, false},
{"Tiangong", "48274", TFT_CYAN, Sgp4(), "", "", "", 0.0f, 0.0f, 0.0f, -1, -1, false, false}
};
int16_t SCR_W = 0;
int16_t SCR_H = 0;
int16_t MAP_H = 0;
int16_t PANEL_Y = 0;
int16_t PANEL_H = 0;
unsigned long lastPositionUpdate = 0;
unsigned long lastTLEUpdate = 0;
// Prosty helper: rysuje tekst tak, zeby (x,y) bylo LEWYM GORNYM rogiem
void drawText(int16_t x, int16_t y, const char *text, uint16_t fg, uint16_t bg) {
tft.loadFont(adventpro_regular18pt);
tft.setTextColor(fg, bg);
tft.setCursor(x, y);
tft.print(text);
}
bool parseTLE(const String &payload, Satellite &sat) {
String lines[8];
int count = 0;
int start = 0;
while (start < payload.length() && count < 8) {
int end = payload.indexOf('\n', start);
if (end < 0) {
end = payload.length();
}
String line = payload.substring(start, end);
line.trim();
if (line.length() > 0) {
lines[count++] = line;
}
start = end + 1;
}
int line1Index = -1;
int line2Index = -1;
for (int i = 0; i < count; i++) {
if (lines[i].startsWith("1 ")) {
line1Index = i;
}
if (lines[i].startsWith("2 ")) {
line2Index = i;
}
}
if (line1Index < 0 || line2Index < 0) {
Serial.printf("[%s] TLE: brak linii 1/2\n", sat.name);
return false;
}
if (line1Index > 0) {
strncpy(sat.tleName, lines[line1Index - 1].c_str(), sizeof(sat.tleName) - 1);
} else {
strncpy(sat.tleName, sat.name, sizeof(sat.tleName) - 1);
}
sat.tleName[sizeof(sat.tleName) - 1] = '\0';
strncpy(sat.tle1, lines[line1Index].c_str(), sizeof(sat.tle1) - 1);
sat.tle1[sizeof(sat.tle1) - 1] = '\0';
strncpy(sat.tle2, lines[line2Index].c_str(), sizeof(sat.tle2) - 1);
sat.tle2[sizeof(sat.tle2) - 1] = '\0';
Serial.printf("[%s] TLE: %s\n", sat.name, sat.tleName);
Serial.printf("[%s] TLE1: %s\n", sat.name, sat.tle1);
Serial.printf("[%s] TLE2: %s\n", sat.name, sat.tle2);
return true;
}
bool downloadTLE(Satellite &sat) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
const char *url;
if (strcmp(sat.norad, "25544") == 0) {
url = TLE_ISS_URL;
} else {
url = TLE_TIANGONG_URL;
}
Serial.printf("[%s] Pobieranie TLE...\n", sat.name);
Serial.println(url);
http.setTimeout(10000);
if (!http.begin(client, url)) {
Serial.printf("[%s] HTTP begin ERROR\n", sat.name);
return false;
}
int code = http.GET();
if (code != HTTP_CODE_OK) {
Serial.printf("[%s] HTTP ERROR: %d\n", sat.name, code);
http.end();
return false;
}
String payload = http.getString();
http.end();
client.stop();
Serial.printf("[%s] Otrzymano %d bajtow\n", sat.name, payload.length());
if (!parseTLE(payload, sat)) {
return false;
}
sat.sgp4.site(USER_LAT, USER_LON, 100);
sat.sgp4.init(sat.tleName, sat.tle1, sat.tle2);
sat.tleValid = true;
sat.positionValid = false;
Serial.printf("[%s] SGP4 INIT OK\n", sat.name);
return true;
}
unsigned long getUnixTime() {
time_t now = time(nullptr);
if (now < 1000000000) {
return 0;
}
return static_cast<unsigned long>(now);
}
bool updateSatellitePosition(Satellite &sat) {
if (!sat.tleValid) {
return false;
}
unsigned long unixTime = getUnixTime();
if (unixTime == 0) {
Serial.println("Brak poprawnego czasu Unix");
return false;
}
sat.sgp4.findsat(unixTime);
sat.lat = sat.sgp4.satLat;
sat.lon = sat.sgp4.satLon;
sat.alt = sat.sgp4.satAlt;
sat.positionValid = true;
Serial.printf("[%s] LAT=%8.3f LON=%9.3f ALT=%8.1f km\n", sat.name, sat.lat, sat.lon, sat.alt);
return true;
}
void latLonToPixel(float lat, float lon, int16_t &x, int16_t &y) {
float fx = (lon + 180.0f) / 360.0f * (SCR_W - 1);
float fy = (90.0f - lat) / 180.0f * (MAP_H - 1);
x = static_cast<int16_t>(fx);
y = static_cast<int16_t>(fy);
if (x < 0) x = 0;
if (x >= SCR_W) x = SCR_W - 1;
if (y < 0) y = 0;
if (y >= MAP_H) y = MAP_H - 1;
}
void drawLandmass(int x, int y, int w, int h) {
int r = min(w, h) / 5;
if (r < 3) r = 3;
tft.fillRoundRect(x, y, w, h, r, COLOR_LAND);
int radiusW1 = static_cast<int>(w * 0.10f);
int radiusW2 = static_cast<int>(w * 0.08f);
int radiusH1 = static_cast<int>(h * 0.10f);
int radiusH2 = static_cast<int>(h * 0.08f);
if (radiusW1 < 2) radiusW1 = 2;
if (radiusW2 < 2) radiusW2 = 2;
if (radiusH1 < 2) radiusH1 = 2;
if (radiusH2 < 2) radiusH2 = 2;
tft.fillCircle(x + static_cast<int>(w * 0.20f), y, radiusW1, COLOR_LAND);
tft.fillCircle(x + static_cast<int>(w * 0.80f), y + h, radiusW2, COLOR_LAND);
tft.fillCircle(x, y + static_cast<int>(h * 0.60f), radiusH1, COLOR_LAND);
tft.fillCircle(x + w, y + static_cast<int>(h * 0.30f), radiusH2, COLOR_LAND);
}
void drawMapBackground() {
tft.fillRect(0, 0, SCR_W, MAP_H, COLOR_OCEAN);
int gridX = SCR_W / 12;
int gridY = MAP_H / 8;
if (gridX < 1) gridX = 1;
if (gridY < 1) gridY = 1;
for (int x = 0; x < SCR_W; x += gridX) {
tft.drawFastVLine(x, 0, MAP_H, COLOR_GRID);
}
for (int y = 0; y < MAP_H; y += gridY) {
tft.drawFastHLine(0, y, SCR_W, COLOR_GRID);
}
tft.drawFastHLine(0, MAP_H / 2, SCR_W, COLOR_EQUATOR);
drawLandmass(static_cast<int>(SCR_W * 0.05f), static_cast<int>(MAP_H * 0.05f), static_cast<int>(SCR_W * 0.22f), static_cast<int>(MAP_H * 0.28f));
drawLandmass(static_cast<int>(SCR_W * 0.22f), static_cast<int>(MAP_H * 0.42f), static_cast<int>(SCR_W * 0.09f), static_cast<int>(MAP_H * 0.36f));
drawLandmass(static_cast<int>(SCR_W * 0.44f), static_cast<int>(MAP_H * 0.07f), static_cast<int>(SCR_W * 0.10f), static_cast<int>(MAP_H * 0.16f));
drawLandmass(static_cast<int>(SCR_W * 0.43f), static_cast<int>(MAP_H * 0.27f), static_cast<int>(SCR_W * 0.13f), static_cast<int>(MAP_H * 0.38f));
drawLandmass(static_cast<int>(SCR_W * 0.55f), static_cast<int>(MAP_H * 0.05f), static_cast<int>(SCR_W * 0.29f), static_cast<int>(MAP_H * 0.34f));
drawLandmass(static_cast<int>(SCR_W * 0.78f), static_cast<int>(MAP_H * 0.56f), static_cast<int>(SCR_W * 0.12f), static_cast<int>(MAP_H * 0.20f));
tft.fillCircle(static_cast<int>(SCR_W * 0.70f), static_cast<int>(MAP_H * 0.52f), 4, COLOR_LAND);
tft.fillCircle(static_cast<int>(SCR_W * 0.85f), static_cast<int>(MAP_H * 0.78f), 4, COLOR_LAND);
tft.fillCircle(static_cast<int>(SCR_W * 0.55f), static_cast<int>(MAP_H * 0.66f), 4, COLOR_LAND);
}
void drawUserLocation() {
int16_t x;
int16_t y;
latLonToPixel(USER_LAT, USER_LON, x, y);
tft.fillRect(x - 3, y - 3, 7, 7, TFT_RED);
tft.drawCircle(x, y, 6, TFT_WHITE);
}
void drawPanel() {
tft.fillRect(0, PANEL_Y, SCR_W, PANEL_H, COLOR_PANEL);
tft.drawFastHLine(0, PANEL_Y, SCR_W, TFT_WHITE);
drawText(8, PANEL_Y + 2, "Tracker stacji kosmicznych", TFT_WHITE, COLOR_PANEL);
}
void drawSatelliteInfo(Satellite &sat, int line) {
int y = PANEL_Y + 27 + line * 20;
tft.fillRect(0, y, SCR_W, 20, COLOR_PANEL);
char buf[32];
drawText(8, y, sat.name, sat.color, COLOR_PANEL);
drawText(90, y, "LAT", TFT_WHITE, COLOR_PANEL);
if (sat.positionValid) {
snprintf(buf, sizeof(buf), "%7.2f", sat.lat);
drawText(125, y, buf, sat.color, COLOR_PANEL);
drawText(195, y, "LON", TFT_WHITE, COLOR_PANEL);
snprintf(buf, sizeof(buf), "%8.2f", sat.lon);
drawText(230, y, buf, sat.color, COLOR_PANEL);
drawText(305, y, "ALT", TFT_WHITE, COLOR_PANEL);
snprintf(buf, sizeof(buf), "%7.1f", sat.alt);
drawText(340, y, buf, sat.color, COLOR_PANEL);
drawText(400, y, "km", TFT_WHITE, COLOR_PANEL);
} else {
drawText(125, y, "---", TFT_RED, COLOR_PANEL);
drawText(195, y, "LON", TFT_WHITE, COLOR_PANEL);
drawText(230, y, "---", TFT_RED, COLOR_PANEL);
drawText(305, y, "ALT", TFT_WHITE, COLOR_PANEL);
drawText(340, y, "---", TFT_RED, COLOR_PANEL);
}
}
void drawSatellite(Satellite &sat) {
if (!sat.positionValid) {
return;
}
if (sat.prevX != -1) {
tft.fillCircle(sat.prevX, sat.prevY, 9, COLOR_OCEAN);
tft.fillRect(sat.prevX - 60, sat.prevY - 4, 120, 12, COLOR_OCEAN);
}
int16_t x;
int16_t y;
latLonToPixel(sat.lat, sat.lon, x, y);
tft.fillCircle(x, y, 5, sat.color);
tft.drawCircle(x, y, 8, TFT_WHITE);
int16_t tx = x + 10;
if (tx > SCR_W - 55) tx = x - 55;
if (tx < 0) tx = 0;
int16_t ty = y - 6;
if (ty < 0) ty = 0;
if (ty > MAP_H - 12) ty = MAP_H - 12;
drawText(tx, ty, sat.name, sat.color, COLOR_OCEAN);
sat.prevX = x;
sat.prevY = y;
}
bool connectWiFi() {
Serial.println();
Serial.println("Łączenie z WiFi...");
WiFi.begin(WIFI_SSID, "", 6);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
delay(250);
Serial.print(".");
}
Serial.println();
if (WiFi.status() != WL_CONNECTED) {
Serial.println("BŁAD WiFi");
return false;
}
Serial.println("WiFi OK");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
return true;
}
bool synchronizeTime() {
Serial.println("Synchronizacja czasu NTP...");
configTime(0, 0, "pool.ntp.org", "time.nist.gov", "time.google.com");
time_t now = time(nullptr);
unsigned long start = millis();
while (now < 1000000000 && millis() - start < 15000) {
delay(250);
now = time(nullptr);
}
if (now < 1000000000) {
Serial.println("BŁAD NTP");
return false;
}
Serial.print("Unix time: ");
Serial.println(static_cast<unsigned long>(now));
return true;
}
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("=================================");
Serial.println(" SATELLITE TRACKER - SGP4");
Serial.println("=================================");
tft.init();
tft.setRotation(1);
SCR_W = tft.width();
SCR_H = tft.height();
MAP_H = SCR_H * 3 / 4;
PANEL_Y = MAP_H;
PANEL_H = SCR_H - MAP_H;
Serial.printf("TFT: %d x %d\n", SCR_W, SCR_H);
Serial.printf("MAP: %d x %d\n", SCR_W, MAP_H);
Serial.printf("PANEL: Y=%d H=%d\n", PANEL_Y, PANEL_H);
tft.fillScreen(TFT_BLACK);
drawText(20, SCR_H / 2 - 10, "Łączenie z WiFi...", TFT_WHITE, TFT_BLACK);
if (!connectWiFi()) {
tft.fillScreen(TFT_BLACK);
drawText(20, SCR_H / 2, "Błąd WiFi", TFT_RED, TFT_BLACK);
return;
}
if (!synchronizeTime()) {
tft.fillScreen(TFT_BLACK);
drawText(20, SCR_H / 2, "Błąd NTP", TFT_RED, TFT_BLACK);
return;
}
downloadTLE(satellites[0]);
downloadTLE(satellites[1]);
drawMapBackground();
drawUserLocation();
drawPanel();
updateSatellitePosition(satellites[0]);
updateSatellitePosition(satellites[1]);
drawSatellite(satellites[0]);
drawSatellite(satellites[1]);
drawSatelliteInfo(satellites[0], 0);
drawSatelliteInfo(satellites[1], 1);
lastTLEUpdate = millis();
lastPositionUpdate = millis();
}
void loop() {
unsigned long now = millis();
if (now - lastTLEUpdate >= TLE_REFRESH_INTERVAL) {
downloadTLE(satellites[0]);
downloadTLE(satellites[1]);
lastTLEUpdate = now;
}
if (now - lastPositionUpdate >= POSITION_INTERVAL) {
lastPositionUpdate = now;
updateSatellitePosition(satellites[0]);
updateSatellitePosition(satellites[1]);
drawSatellite(satellites[0]);
drawSatellite(satellites[1]);
drawSatelliteInfo(satellites[0], 0);
drawSatelliteInfo(satellites[1], 1);
}
}Unikać GPIO 26–32: Są zarezerwowane wewnętrznie dla pamięci Flash / PSRAM
Unikać GPIO 19 i 20: Służą do komunikacji USB / JTAG (programowanie przez wbudowany port USB)
Piny "Strapping" (GPIO 0, 3, 45, 46): ich stan (HIGH/LOW) podczas restartu ESP32 decyduje o trybie uruchamiania układu