#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"
#include "mapa.h"
#define MYFONT32 &myFont32pt8b
#define WIFI_SSID "Wokwi-GUEST"
// Tablice ze źródłami TLE (próbowane po kolei w przypadku błędu)
const char* ISS_TLE_SOURCES[] = {
"https://iss-tracker.com/include/tledata-ISS.txt",
"https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=tle",
"https://live.ariss.org/tle/"
};
const int ISS_SOURCES_COUNT = sizeof(ISS_TLE_SOURCES) / sizeof(ISS_TLE_SOURCES[0]);
const char* TIANGONG_TLE_SOURCES[] = {
"https://iss-tracker.com/include/tledata-TIANGONG.txt",
"https://celestrak.org/NORAD/elements/gp.php?CATNR=48274&FORMAT=tle"
};
const int TIANGONG_SOURCES_COUNT = sizeof(TIANGONG_TLE_SOURCES) / sizeof(TIANGONG_TLE_SOURCES[0]);
// Zmienne do precyzyjnego dopasowania mapy:
// Offsety: Położenie punktu 0° N/S, 0° E/W na Twoim obrazku
const int16_t g_mapOffsetX = 0; // Południk 0°
const int16_t g_mapOffsetY = 0; // Równik 0°
const float g_pxPerDegX = 1.3565f;
const float g_pxPerDegY = 1.5313f;
float scaleX = 1.10f;
float scaleY = 1.10f;
float mapOffsetX = -16.0f;
float mapOffsetY = 35.0f;
// Jedno źródło prawdy dla kalibracji mapy
int16_t g_equatorY;
int16_t g_meridianX;
#define USER_LAT 45.61f
#define USER_LON 10.31f
#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
TFT_eSPI tft = TFT_eSPI();
struct MapPoint {
uint8_t x;
uint8_t y;
};
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) {
// Wybór odpowiedniej listy źródeł na podstawie stacji
const char** sources;
int sourcesCount = 0;
if (strcmp(sat.norad, "25544") == 0) {
sources = ISS_TLE_SOURCES;
sourcesCount = ISS_SOURCES_COUNT;
} else {
sources = TIANGONG_TLE_SOURCES;
sourcesCount = TIANGONG_SOURCES_COUNT;
}
// Pętla próbująca pobrać dane z kolejnych serwerów
for (int i = 0; i < sourcesCount; i++) {
const char* url = sources[i];
Serial.printf("[%s] Próba pobrania TLE z (%d/%d): %s\n", sat.name, i + 1, sourcesCount, url);
WiFiClientSecure client;
client.setInsecure(); // Pomija weryfikację certyfikatu SSL
HTTPClient http;
http.setTimeout(8000);
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
if (!http.begin(client, url)) {
Serial.printf("[%s] Błąd bazy HTTP dla %s\n", sat.name, url);
continue; // Próbujemy następny URL
}
// Nagłówki ułatwiające przejście przez zabezpieczenia (np. CelesTrak)
http.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
http.addHeader("Accept", "*/*");
int code = http.GET();
if (code == HTTP_CODE_OK) {
String payload = http.getString();
http.end();
client.stop();
Serial.printf("[%s] Pobrano %d bajtów. Parsowanie...\n", sat.name, payload.length());
// Jeśli parsowanie przebiegło pomyślnie, inicjalizujemy SGP4 i wychodzimy z sukcesem
if (parseTLE(payload, sat)) {
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 (Źródło: %s)\n", sat.name, url);
return true;
} else {
Serial.printf("[%s] Błąd parsowania TLE ze źródła: %s\n", sat.name, url);
}
} else {
Serial.printf("[%s] HTTP BŁĄD %d dla URL: %s\n", sat.name, code, url);
http.end();
client.stop();
}
}
// Jeśli żadne źródło nie zadziałało
Serial.printf("[%s] BŁĄD Krytyczny: Wszystkie źródła TLE zawiodły!\n", sat.name);
return false;
}
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 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 latLonToPixel(float lat, float lon, int16_t &x, int16_t &y) {
// Przesunięcie od punktu zero (offsetu)
float fx = (float)g_mapOffsetX + (lon * g_pxPerDegX);
float fy = (float)g_mapOffsetY - (lat * g_pxPerDegY);
// Zawijanie mapy w poziomie (z krawędzi ekranu na drugą stronę)
while (fx < 0.0f) fx += (float)SCR_W;
while (fx >= (float)SCR_W) fx -= (float)SCR_W;
x = static_cast<int16_t>(roundf(fx));
y = static_cast<int16_t>(roundf(fy));
}
void drawMapGrid() {
int16_t x, y;
for (int lat = -90; lat <= 90; lat += 30) {
latLonToPixel((float)lat, 0.0f, x, y);
if (y >= 0 && y < MAP_H) {
uint16_t color = (lat == 0) ? TFT_RED : COLOR_GRID;
tft.drawFastHLine(0, y, SCR_W, color);
}
}
for (int lon = -180; lon < 180; lon += 30) {
latLonToPixel(0.0f, (float)lon, x, y);
if (x >= 0 && x < SCR_W) {
uint16_t color = (lon == 0) ? TFT_RED : COLOR_GRID;
tft.drawFastVLine(x, 0, MAP_H, color);
}
}
}/*
int16_t tx, ty;
latLonToPixel(52.5190f, 13.3920f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_RED);
Serial.printf("Berlin: x=%d y=%d\n", tx, ty);
latLonToPixel(41.8930f, 12.4830f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_RED);
Serial.printf("Rzym: x=%d y=%d\n", tx, ty);
latLonToPixel(51.5144f, -0.1270f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_RED);
Serial.printf("Londyn: x=%d y=%d\n", tx, ty);
latLonToPixel(45.6170f, 10.3170f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_RED);
Serial.printf("Caino: x=%d y=%d\n", tx, ty);
latLonToPixel(22.522f, 109.575f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_GREY);
Serial.printf("Kalifornia: x=%d y=%d\n", tx, ty);
latLonToPixel(-34.440f, 19.372f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_ORANGE);
Serial.printf("RPA: x=%d y=%d\n", tx, ty);
latLonToPixel(21.561f, 120.495f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_PINK);
Serial.printf("Tajwan: x=%d y=%d\n", tx, ty);
latLonToPixel(55.7558f, 37.6173f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_YELLOW);
Serial.printf("Moskwa: x=%d y=%d\n", tx, ty);
latLonToPixel(40.7128f, -74.0060f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_YELLOW);
Serial.printf("Nowy Jork: x=%d y=%d\n", tx, ty);
latLonToPixel(-33.9249f, 18.4241f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_YELLOW);
Serial.printf("Kapsztad: x=%d y=%d\n", tx, ty);
latLonToPixel(35.6762f, 139.6503f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_YELLOW);
Serial.printf("Tokio: x=%d y=%d\n", tx, ty);
latLonToPixel(64.1466f, -21.9426f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_YELLOW);
Serial.printf("Reykjavik: x=%d y=%d\n", tx, ty);
latLonToPixel(-10.442f, 142.315f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_YELLOW);
Serial.printf("Australia: x=%d y=%d\n", tx, ty);
latLonToPixel(55.215f, 13.105f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_LIME);
Serial.printf("Malmo: x=%d y=%d\n", tx, ty);
latLonToPixel(59.444f, -43.530f, tx, ty);
tft.fillRect(tx - 1, ty - 1, 2, 2, TFT_LTBLUE);
Serial.printf("Cyyp: x=%d y=%d\n", tx, ty);
Serial.printf("linia 0: y=%d\n", g_equatorY - (int16_t)( 0 * g_pxPerDegY));
Serial.printf("linia 30: y=%d\n", g_equatorY - (int16_t)(30 * g_pxPerDegY));
Serial.printf("linia 60: y=%d\n", g_equatorY - (int16_t)(60 * g_pxPerDegY));
Serial.printf("linia 90: y=%d\n", g_equatorY - (int16_t)(90 * g_pxPerDegY));*/
//}
void drawTestPoints() {
int16_t tx, ty;
// --- EUROPA ---
latLonToPixel(52.520f, 13.405f, tx, ty); // Berlin
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_RED);
latLonToPixel(41.902f, 12.496f, tx, ty); // Rzym
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_RED);
latLonToPixel(51.507f, -0.127f, tx, ty); // Londyn
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_RED);
latLonToPixel(55.605f, 13.003f, tx, ty); // Malmo
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_GREEN);
latLonToPixel(35.126f, 33.429f, tx, ty); // Cypr
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_CYAN);
latLonToPixel(55.755f, 37.617f, tx, ty); // Moskwa
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_YELLOW);
latLonToPixel(64.146f, -21.942f, tx, ty); // Reykjavik
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_WHITE);
// --- AMERYKA PÓŁNOCNA ---
latLonToPixel(34.052f, -118.243f, tx, ty); // Kalifornia (Los Angeles)
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_ORANGE);
latLonToPixel(40.712f, -74.006f, tx, ty); // Nowy Jork
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_MAGENTA);
// --- AZJA I AUSTRALIA ---
latLonToPixel(35.676f, 139.650f, tx, ty); // Tokio
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_PINK);
latLonToPixel(23.697f, 120.960f, tx, ty); // Tajwan
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_PURPLE);
latLonToPixel(-33.868f, 151.209f, tx, ty); // Australia (Sydney)
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_YELLOW);
// --- AFRYKA ---
latLonToPixel(-33.924f, 18.424f, tx, ty); // Kapsztad (RPA)
tft.fillRect(tx - 1, ty - 1, 3, 3, TFT_GOLD);
}
void drawMapBackground() {
tft.fillRect(0, 0, SCR_W, MAP_H, COLOR_OCEAN);
tft.drawBitmap(0, 0, mapa, 480, 240, COLOR_OCEAN, COLOR_LAND);
//drawMapGrid();
}
void drawUserLocation() {
int16_t x;
int16_t y;
latLonToPixel(USER_LAT, USER_LON, x, y);
tft.fillRect(x - 1, y - 1, 2, 2, TFT_RED);
}
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;
}
int16_t x;
int16_t y;
latLonToPixel(sat.lat, sat.lon, x, y);
//tft.fillCircle(x, y, 3, sat.color);
tft.fillRect(x, y, 3, 3, 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ŁĄD 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ŁĄD NTP");
return false;
}
Serial.print("Unix time: ");
Serial.println(static_cast<unsigned long>(now));
return true;
}
struct RefPoint {
const char *name;
float lat;
float lon;
};
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, 20, "Łączenie z WiFi...", TFT_WHITE, TFT_BLACK);
if (!connectWiFi()) {
drawText(20, 40, "Błąd WiFi", TFT_RED, TFT_BLACK);
return;
} else {
drawText(20, 40, "Połączono z WiFi", TFT_GREEN, TFT_BLACK);
}
if (!synchronizeTime()) {
drawText(20, 60, "Błąd NTP", TFT_RED, TFT_BLACK);
return;
} else {
drawText(20, 60, "Pobrano NTP", TFT_GREEN, TFT_BLACK);
}*/
//downloadTLE(satellites[0]);
//downloadTLE(satellites[1]);
drawMapBackground();
//drawTestPoints();
//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();
//latLonToPixel(52.520f, 13.405f, tx, ty); // Berlin
tft.fillRect(165, 60, 2, 2, TFT_YELLOW); // Grenlandia południe
tft.fillRect(252, 203, 2, 2, TFT_RED); // Afryka południe
tft.fillRect(436, 76, 2, 2, TFT_PINK); // Kamczatka południe
tft.fillRect(416, 171, 2, 2, TFT_GREEN); // Australia północ wschód
}
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