This project adds custom firmware to an ESP32 that transforms it into a weather and satellite tracking station with a TFT display. By connecting an existing user GPS module via UART and linking to WiFi, the device displays real-time weather data, temperature history, and ISS tracking information with a charming animated interface.
The guide provides a complete wiring diagram for connecting the GPS module to the ESP32, a full parts list, the compiled firmware ready to flash, and step-by-step assembly instructions. Users will learn how to configure WiFi credentials, set location preferences, and customize display units while preserving their existing equipment through non-invasive integration.
Wiring diagram

Gather all the parts
Assemble it in 4 steps
1. Conservar el equipo y respaldo
Guarda dos copias del respaldo completo de flash y copia la microSD por separado. No cambies el cableado existente del GPS. Este proyecto sustituye el firmware anterior; no recupera su código ni sus animaciones.
- No cargues firmware hasta comprobar que tu respaldo puede restaurarse en esta misma placa. Retira la microSD con el equipo apagado para conservarla intacta.
2. Comprobar el GPS sin modificarlo
TX del GPS va a GPIO4 (entrada de datos al ESP32), RX del GPS a GPIO13 y GND a GND. Se conserva la alimentación ya existente porque el modelo y su tensión de alimentación no están identificados. Serie a 9600 baudios.
- No cambies la alimentación del GPS desconocido. Su señal TX debe ser compatible con lógica de 3.3 V; si no lo es, no la conectes directamente al ESP32.
3. Colocar y alimentar la estación
Usa el conector USB y la alimentación integrada original. Mantén libre la antena del GPS. No se añaden baterías ni se modifica el cargador integrado.
- La posición GPS suele tardar varios minutos y necesita visión del cielo. Los datos meteorológicos son estimaciones de Open-Meteo para la ubicación, no mediciones de interior. No hace falta conectar un BME280.
- El pronóstico se descarga mediante HTTP sin autenticar; puede ser alterado por la red. No sirve como alerta de seguridad. La ubicación se envía a Open-Meteo. La hora y el descanso de Nubi usan UTC, no tu zona horaria local.
4. Abrir el explorador ISS y avisos de Nubi
Conecta el Wi-Fi guardado. Usa las flechas inferiores hasta la sexta pantalla y toca GLOBO, DATOS o RADIO. La posición orbital se consulta cada 10 segundos también en segundo plano si hay GPS válido o coordenadas manuales; no necesita GPS ni BME280. El globo es esquemático y se centra en la longitud de la ISS.
- Nubi se emociona y la cabecera avisa cuando la elevación ISS supera 1 grado; termina al bajar de 0 grados. Usa GPS reciente o coordenadas manuales. Aviso visual, sin sonido; necesita Wi-Fi y datos orbitales recientes. No predice pases futuros ni garantiza visibilidad óptica o radio activa.
- DATOS muestra la hora UTC de la posición y su antigüedad; sin red conserva la última posición marcada como antigua. No es vídeo, telemetría directa ni predicción de próximos pases.
- RADIO es una ficha de recepción ARISS, no un receptor. Las frecuencias y modos pueden cambiar; confirma el estado en ariss.org. Para escuchar necesitas un receptor o SDR externo compatible y su antena.
- La consulta ISS usa HTTPS sin verificar el certificado del servidor; los datos no son adecuados para navegación ni seguridad. No se envía la posición del usuario al servicio ISS.
Review all connections
1. Connections between "gps" and "ESP32"
Deploy the firmware
#include <Arduino.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <TinyGPSPlus.h>
#include <ArduinoJson.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <WebServer.h>
#include <Preferences.h>
#include <time.h>
#include <sys/time.h>
#include <math.h>
// Exact official Marauder v6.1 pinout. SD is deselected and never mounted.
TFT_eSPI tft;
TFT_eSprite pet(&tft);
TinyGPSPlus gps;
HardwareSerial gpsPort(2);
WebServer server(80);
Preferences prefs;
SemaphoreHandle_t lock;
const uint16_t BG=0x0823, CARD=0x10E6, INK=0xF7BF, MINT=0x5F59, PINK=0xFC79;
String ssid,pass,apName,apPass;
double manualLat=0,manualLon=0;
bool fahrenheit=false, miles=false, hour12=false;
bool manualLocation=false,dirty=true,petReady=false;
float tempC=NAN,humidity=NAN,pressure=NAN,dew=NAN;
float history[120]; int histCount=0,histHead=0,page=0;
uint32_t lastSample=0,lastHist=0,lastPet=0,lastTouch=0,lovedUntil=0,lastGpsByte=0;
uint32_t nextBlink=2500,blinkUntil=0,petMoveAt=0; int pose=0,prevPose=-1;
String lastPanel;
bool issAbove=false,issTracking=false;
double issElevation=0;
struct Forecast {bool ok=false,dayKnown=false,isDay=true; float t=NAN,wind=NAN,rh=NAN,p=NAN,dp=NAN; int code=-1; float hi[3],lo[3]; int rain[3]; uint32_t received=0; double lat=0,lon=0; char city[96]=""; char status[40]="Esperando Wi-Fi y ubicacion";};
Forecast fc;
struct Request {bool wifi=false,location=false; double lat=0,lon=0;};
Request request;
const char HTML[] PROGMEM=R"HTML(<!doctype html><html lang="es"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Nubi</title><style>body{font:18px system-ui;background:#101b2c;color:#e6fff9;max-width:550px;margin:30px auto;padding:20px}input,button{box-sizing:border-box;width:100%;padding:12px;margin:8px 0;border-radius:12px}button{background:#87ead6}small{color:#b9c9dd}</style><h1>☁ Nubi · tu pequeño clima</h1><p>Configura tu red de 2.4 GHz. El GPS tiene prioridad cuando su posicion es reciente.</p><form method="POST" action="/save"><label>Nombre de tu Wi-Fi<input name="ssid" maxlength="32" required></label><label>Contraseña Wi-Fi<input name="pass" type="password" maxlength="63"></label><label>Latitud de respaldo (opcional)<input name="lat" type="number" step="any" min="-90" max="90"></label><label>Longitud de respaldo (opcional)<input name="lon" type="number" step="any" min="-180" max="180"></label><button>Guardar y conectar</button></form><p><a href="/status">Ver estado</a></p><small>Coordenadas enviadas a Open-Meteo para obtener el pronostico. No usa tu SD ni conserva rutas GPS. Hora UTC. Nombre de ciudad: OpenStreetMap contributors (ODbL), coordenadas enviadas a Nominatim. Servicio Open-Meteo, datos CC BY 4.0. No es un sistema de alertas de emergencia.</small></html>)HTML";
// Resolve only when the forecast location changes substantially; cache in RAM.
// City lookup failure must never prevent a weather update.
void resolveCity(Forecast &f, double lat, double lon) {
static double cachedLat=1000,cachedLon=1000;
static String cachedCity;
static uint32_t lastLookup=0;
bool same=fabs(lat-cachedLat)<0.05 && fabs(lon-cachedLon)<0.05;
if(same && cachedCity.length()) {
strlcpy(f.city,cachedCity.c_str(),sizeof(f.city)); return;
}
f.city[0]=0;
if(lastLookup && millis()-lastLookup<1800000UL)return;
lastLookup=millis();
WiFiClientSecure client;
// Public place names only; TLS encryption without certificate verification.
client.setInsecure();
HTTPClient http;http.setConnectTimeout(5000);http.setTimeout(5000);
String url="https://nominatim.openstreetmap.org/reverse?format=jsonv2&zoom=10&addressdetails=1&accept-language=es&lat="+String(lat,4)+"&lon="+String(lon,4);
if(http.begin(client,url)) {
http.setUserAgent("NubiWeather/1.0 (ESP32 personal weather station)");
if(http.GET()==200) {
JsonDocument doc;
if(!deserializeJson(doc,http.getString())) {
String city=doc["address"]["city"] | "";
if(!city.length())city=doc["address"]["town"] | "";
if(!city.length())city=doc["address"]["village"] | "";
if(!city.length())city=doc["address"]["municipality"] | "";
if(!city.length())city=doc["name"] | "";
if(city.length()) {
cachedCity=city;cachedLat=lat;cachedLon=lon;
strlcpy(f.city,city.c_str(),sizeof(f.city));
}
}
}
http.end();
}
}
void networkTask(void*) {
uint32_t lastTry=0; bool first=true; bool wasReady=false;
for(;;){
Request r; xSemaphoreTake(lock,portMAX_DELAY); r=request; xSemaphoreGive(lock);
bool ready=r.wifi && r.location;
if(!ready){
xSemaphoreTake(lock,portMAX_DELAY);
snprintf(fc.status,sizeof(fc.status),"%s",r.wifi?"Falta GPS o ubicacion manual":"Esperando conexion Wi-Fi");
xSemaphoreGive(lock);
}
if(ready && !wasReady) first=true;
wasReady=ready;
if(ready && (first || millis()-lastTry>=300000UL)){
first=false;lastTry=millis();
Forecast n; xSemaphoreTake(lock,portMAX_DELAY);n=fc;xSemaphoreGive(lock);
IPAddress resolved;
bool dnsOK = WiFi.hostByName("api.open-meteo.com", resolved) == 1;
if (!dnsOK && WiFi.status() == WL_CONNECTED) {
// Preserve the current lease and gateway; replace only the failing resolver.
WiFi.config(WiFi.localIP(), WiFi.gatewayIP(), WiFi.subnetMask(),
IPAddress(1,1,1,1), IPAddress(8,8,8,8));
dnsOK = WiFi.hostByName("api.open-meteo.com", resolved) == 1;
}
if (!dnsOK) {
snprintf(n.status,sizeof(n.status),"Fallo DNS / revisa Internet");
Serial.println("Pronostico: fallo DNS incluso con respaldo; revisar acceso a Internet del router");
xSemaphoreTake(lock,portMAX_DELAY);fc=n;xSemaphoreGive(lock);
first=true;
vTaskDelay(pdMS_TO_TICKS(60000));
continue;
}
WiFiClient tls; // Public model forecast via HTTP, unauthenticated; not safety-critical.
HTTPClient http; http.setConnectTimeout(7000);http.setTimeout(7000);
String url="http://api.open-meteo.com/v1/forecast?latitude="+String(r.lat,4)+"&longitude="+String(r.lon,4)+"¤t=temperature_2m,relative_humidity_2m,surface_pressure,dew_point_2m,weather_code,wind_speed_10m,is_day&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max&forecast_days=3&timezone=auto&timeformat=unixtime";
int code=-1;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
if(http.begin(tls,url)) code=http.GET();
if(code==200){
String body=http.getString(); JsonDocument doc;
auto err=deserializeJson(doc,body);
if(!err && doc["current"]["temperature_2m"].is<float>() && doc["daily"]["temperature_2m_max"].size()>=3){
// Open-Meteo Unix timestamps are UTC. Avoid the failing UDP/SNTP path.
time_t weatherTime=doc["current"]["time"].as<long>();
if(weatherTime>1700000000){struct timeval tv={weatherTime,0};settimeofday(&tv,nullptr);}
n.rh=doc["current"]["relative_humidity_2m"] | NAN;n.p=doc["current"]["surface_pressure"] | NAN;n.dp=doc["current"]["dew_point_2m"] | NAN;
n.dayKnown=doc["current"]["is_day"].is<int>();n.isDay=(doc["current"]["is_day"] | 1)==1;
n.ok=true;n.t=doc["current"]["temperature_2m"].as<float>();n.wind=doc["current"]["wind_speed_10m"].as<float>();n.code=doc["current"]["weather_code"].as<int>();
for(int i=0;i<3;i++){n.hi[i]=doc["daily"]["temperature_2m_max"][i].as<float>();n.lo[i]=doc["daily"]["temperature_2m_min"][i].as<float>();n.rain[i]=doc["daily"]["precipitation_probability_max"][i].isNull()?-1:doc["daily"]["precipitation_probability_max"][i].as<int>();}
n.received=millis();n.lat=r.lat;n.lon=r.lon;snprintf(n.status,sizeof(n.status),"Pronostico actualizado");
}else snprintf(n.status,sizeof(n.status),"Respuesta invalida; reintento 1m");
}else{snprintf(n.status,sizeof(n.status),"Error HTTP %d; reintento 1m",code); if(!n.ok)first=true;}
first=String(n.status)!="Pronostico actualizado";
Serial.printf("Pronostico: HTTP %d; %s\n",code,n.status);
http.end();
if(n.ok)resolveCity(n,r.lat,r.lon);
xSemaphoreTake(lock,portMAX_DELAY);fc=n;xSemaphoreGive(lock);
if(first)vTaskDelay(pdMS_TO_TICKS(60000));
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
String num(float v,int decimals=1){return isfinite(v)?String(v,decimals):"--";}
float displayTemp(float c){return fahrenheit?c*1.8f+32:c;}
String temperature(float c,int decimals=1){return num(displayTemp(c),decimals)+(fahrenheit?" F":" C");}
bool rainy(int c){return (c>=51&&c<=67)||(c>=80&&c<=82)||c>=95;}
bool snowy(int c){return (c>=71&&c<=77)||c==85||c==86;}
bool freshWeather(const Forecast &f){return f.ok && millis()-f.received<=1800000UL;}
String weather(int c){if(c<0)return "Sin datos";if(c==0)return "Despejado";if(c==1)return "Casi despejado";if(c==2)return "Parcial nublado";if(c==3)return "Nublado";if(c==45||c==48)return "Niebla";if(c>=95)return "Tormenta";if(snowy(c))return "Nieve";if(c>=51&&c<=57)return "Llovizna";if(rainy(c))return "Lluvia";return "Sin datos";}
void text(String s,int x,int y,int font=2,uint16_t color=INK){tft.setTextColor(color,CARD);tft.drawString(s,x,y,font);}
Forecast snapshot(){Forecast f;xSemaphoreTake(lock,portMAX_DELAY);f=fc;xSemaphoreGive(lock);return f;}
bool fixed(){return gps.location.isValid() && gps.location.age()<30000;}
#include "dashboard.h"
#include "iss_ui.h"
void drawMoon(){
static int oldDay=-1,moonPage=-1;time_t now=time(nullptr);int day=now/86400;if(!moonDirty&&day==oldDay&&moonPage==page)return;moonDirty=false;oldDay=day;moonPage=page;
tft.setTextSize(1);tft.setTextDatum(TL_DATUM);tft.setTextPadding(0);
tft.fillRect(140,200,92,91,BG);tft.setTextColor(INK,BG);tft.drawString("LUNA",165,207,1);
if(now<1700000000){tft.drawString("Sin hora",149,244,1);return;}
double cycle=fmod((double(now)-947182440.0)/2551442.88,1.0);if(cycle<0)cycle+=1;
double phase=cycle*2*PI;float lit=(1-cos(phase))*50;
// Approximate synodic moon, not a navigation ephemeris.
for(int y=-22;y<=22;y++)for(int x=-22;x<=22;x++){
if(x*x+y*y>484)continue;double z=sqrt(max(0.0,1.0-(x*x+y*y)/484.0));
bool light=(x/22.0*sin(phase)-z*cos(phase))>0;tft.drawPixel(185+x,248+y,light?INK:CARD);
}
tft.drawString(String(int(lit))+"% "+(cycle<0.5?"crece":"mengua"),146,281,1);
}
void animate(){
if(!petReady)return;uint32_t now=millis();if(now-lastPet<100)return;lastPet=now;
Forecast f=snapshot();bool stale=!f.ok||now-f.received>1800000;
float t=isfinite(tempC)?tempC:(!stale?f.t:NAN);
bool night=!stale&&f.dayKnown&&!f.isDay;
int mood=issAbove?10:now<lovedUntil?1:stale?5:f.code>=95?9:snowy(f.code)?8:rainy(f.code)?4:night?6:f.code==3?7:t<8?2:t>30?3:0;
if(now>=nextBlink){blinkUntil=now+180;nextBlink=now+random(2400,6500);}
if(now>=petMoveAt){pose=random(0,3);petMoveAt=now+random(700,1800);}
int frame=now/150;
int state=mood*100000+pose*10000+(now<blinkUntil)*1000+((mood==4||mood==6||mood==7||mood==8||mood==9||mood==10)?frame%1000:0);if(state==prevPose)return;prevPose=state;
pet.setTextSize(1);pet.setTextDatum(TL_DATUM);pet.setTextPadding(0);
pet.fillSprite(BG);
if(night){pet.fillCircle(107,13,9,0xFFCE);pet.fillCircle(111,10,8,BG);for(int i=0;i<4;i++)pet.drawPixel(9+i*25,8+(i*11)%22,frame%6==i?INK:0x426C);}
if(mood==4||mood==8||mood==9)for(int i=0;i<8;i++){int x=8+i*15,y=5+(frame*4+i*9)%62;if(mood==8){pet.drawFastHLine(x-2,y,5,INK);pet.drawFastVLine(x,y-2,5,INK);}else pet.drawLine(x,y,x-2,y+5,0x5DFF);}
if(mood==7){int dx=int(sin(frame*.15f)*4);pet.fillRoundRect(5+dx,9,32,9,4,0x8C96);pet.fillCircle(20+dx,9,8,0x8C96);}
int y=36+(pose==0?-2:pose==1?1:0);uint16_t body=mood==2?0xAEFF:mood==3?0xFCB0:INK;
pet.fillEllipse(56,y+15,38,24,body);pet.fillCircle(35,y,17,body);pet.fillCircle(58,y-7,20,body);pet.fillCircle(80,y,16,body);
pet.fillEllipse(33,y+16,6,3,PINK);pet.fillEllipse(80,y+16,6,3,PINK);
for(int x:{45,68}){if(now<blinkUntil||mood==6)pet.drawFastHLine(x-3,y+8,7,BG);else{pet.fillCircle(x,y+7,3,BG);pet.drawPixel(x+1,y+6,INK);}}
pet.drawLine(54,y+19,57,y+21,BG);pet.drawLine(57,y+21,60,y+18,BG);
const char* message=mood==10?"ISS sobre nosotros!":mood==1?"Te quiero!":mood==2?"Brrr...":mood==3?"Aguita, porfa":mood==4?"Mi paraguas!":mood==5?"Buscando el clima":mood==6?"Zzz...":mood==7?"Entre nubecitas":mood==8?"Copitos!":mood==9?"Truenos!":"Hola, soy Nubi";
if(mood==10){
int sx=94+int(sin(frame*.2f)*5);
pet.fillRect(sx-12,6,7,12,MINT);pet.fillRect(sx+5,6,7,12,MINT);
pet.drawLine(sx-5,12,sx+5,12,INK);pet.fillRect(sx-2,3,5,18,INK);
pet.drawLine(20,28,27,21,PINK);pet.drawLine(90,28,84,21,PINK);
}
if(mood==1){pet.fillCircle(95,9,4,PINK);pet.fillCircle(102,9,4,PINK);pet.fillTriangle(91,11,106,11,99,20,PINK);}
if(mood==4||mood==9){pet.fillCircle(100,12,11,MINT);pet.fillRect(88,13,25,12,BG);pet.drawFastVLine(100,12,23,PINK);}
if(mood==9&&frame%16<3){pet.fillTriangle(14,8,7,23,16,20,0xFFE0);pet.drawLine(16,20,11,29,0xFFE0);}
if(mood==6)pet.drawString(frame%12<6?"z":"Zz",87,27,1);
if(mood==3)pet.fillCircle(90,40,4,MINT);
pet.setTextColor(MINT,BG);pet.drawCentreString(message,62,85,1);pet.pushSprite(8,199);
}
#include "wifi_ui.h"
void setup(){
hud.setColorDepth(16);hudReady=hud.createSprite(224,157)!=nullptr;
Serial.begin(115200);lock=xSemaphoreCreateMutex();
pinMode(32,OUTPUT);digitalWrite(32,LOW);
pinMode(14,OUTPUT);digitalWrite(14,HIGH);pinMode(17,OUTPUT);digitalWrite(17,HIGH);pinMode(21,OUTPUT);digitalWrite(21,HIGH);
tft.init();tft.setRotation(0);uint16_t cal[5]={339,3470,237,3438,2};tft.setTouch(cal);digitalWrite(32,HIGH);
pet.setColorDepth(8);petReady=pet.createSprite(124,94)!=nullptr;
gpsPort.begin(9600,SERIAL_8N1,4,13);
prefs.begin("nubiWX",false);fahrenheit=prefs.getBool("degF",false);miles=prefs.getBool("mph",false);hour12=prefs.getBool("hour12",false);ssid=prefs.getString("ssid","");pass=prefs.getString("pass","");manualLocation=prefs.getBool("manual",false);manualLat=prefs.isKey("lat")?prefs.getDouble("lat",0):0;manualLon=prefs.isKey("lon")?prefs.getDouble("lon",0):0;
WiFi.mode(WIFI_AP_STA);String suffix=String(uint32_t(ESP.getEfuseMac()>>24)&0xFFFFFF,HEX);suffix.toUpperCase();apName="Nubi-"+suffix;apPass="Nubi-"+suffix;
WiFi.softAP(apName.c_str(),apPass.c_str());WiFi.setAutoReconnect(true);if(ssid.length())WiFi.begin(ssid.c_str(),pass.c_str());
// Use GPS or HTTP weather time instead of SNTP.
// never from the TCP/IP callback thread.
// Clock is set from GPS or the successful weather response, without UDP.
server.on("/",HTTP_GET,[]{server.send_P(200,"text/html; charset=utf-8",HTML);});
server.on("/status",HTTP_GET,[]{Forecast f=snapshot();String s=String("Nubi\nModo: clima por Internet")+"\nCiudad: "+String(f.city)+"\nTemperatura: "+num(tempC)+" C\nHumedad: "+num(humidity)+" %\nPresion del modelo: "+num(pressure)+" hPa\nGPS: "+String(fixed()?"valido":"esperando")+"\n"+f.status+"\nFuente: Open-Meteo (CC BY 4.0)\n";server.send(200,"text/plain; charset=utf-8",s);});
server.on("/save",HTTP_POST,[]{
if(server.arg("ssid").length()>32 || server.arg("pass").length()>63){server.send(400,"text/plain","Datos demasiado largos");return;}
String a=server.arg("lat"),b=server.arg("lon");bool manual=a.length()&&b.length();char *endA,*endB;double la=strtod(a.c_str(),&endA),lo=strtod(b.c_str(),&endB);
if((a.length()!=0)!=(b.length()!=0) || (manual&&(*endA||*endB||!isfinite(la)||!isfinite(lo)||la<-90||la>90||lo<-180||lo>180))){server.send(400,"text/plain","Revisa las coordenadas");return;}
ssid=server.arg("ssid");pass=server.arg("pass");manualLocation=manual;manualLat=la;manualLon=lo;
prefs.putString("ssid",ssid);prefs.putString("pass",pass);prefs.putBool("manual",manual);prefs.putDouble("lat",la);prefs.putDouble("lon",lo);
server.send(200,"text/plain; charset=utf-8","Guardado. Nubi se conecta. Puedes volver a la pantalla.");WiFi.begin(ssid.c_str(),pass.c_str());dirty=true;
});server.begin();
xTaskCreatePinnedToCore(networkTask,"forecast",12288,nullptr,1,nullptr,0);
xTaskCreatePinnedToCore(issTask,"iss",8192,nullptr,1,nullptr,0);
chrome();
}
void loop(){
uint32_t now=millis();
while(gpsPort.available()){char c=gpsPort.read();gps.encode(c);lastGpsByte=now;}
static uint32_t lastTimeSet=0;
if(gps.date.isValid()&&gps.time.isValid()&&gps.time.age()<1500&&gps.date.age()<1500 && now-lastTimeSet>60000){
struct tm tm={};tm.tm_year=gps.date.year()-1900;tm.tm_mon=gps.date.month()-1;tm.tm_mday=gps.date.day();tm.tm_hour=gps.time.hour();tm.tm_min=gps.time.minute();tm.tm_sec=gps.time.second();time_t epoch=mktime(&tm);if(epoch>1700000000){struct timeval tv={epoch,0};settimeofday(&tv,nullptr);lastTimeSet=now;}}
server.handleClient();
wifiTick();
if(now-lastSample>=1000){
lastSample=now;Forecast f=snapshot();bool fresh=f.ok && now-f.received<=1800000UL;
tempC=fresh?f.t:NAN;humidity=fresh?f.rh:NAN;pressure=fresh?f.p:NAN;dew=fresh?f.dp:NAN;
dirty=true;
}
if(now-lastHist>=60000){lastHist=now;history[histHead]=tempC;histHead=(histHead+1)%120;histCount=min(histCount+1,120);dirty=true;}
Request r;r.wifi=WiFi.status()==WL_CONNECTED;r.location=fixed()||manualLocation;r.lat=fixed()?gps.location.lat():manualLat;r.lon=fixed()?gps.location.lng():manualLon;
xSemaphoreTake(lock,portMAX_DELAY);request=r;xSemaphoreGive(lock);
issAlertTick();
uint16_t x,y;static bool held=false;bool touched=tft.getTouch(&x,&y);
if(touched&&!held&&now-lastTouch>250){lastTouch=now;if(wifiUI){wifiTouch(x,y);}else if(y>=292){page=(page+(x<70?5:1))%6;issVisible=page==5;chrome();}else if(page==5 && y>=35 && y<60){issTab=min(3,int(x/60));dirty=true;}else if(page==4 && y>=63 && y<167){if(y<95){fahrenheit=!fahrenheit;prefs.putBool("degF",fahrenheit);}else if(y>=99&&y<131){miles=!miles;prefs.putBool("mph",miles);}else if(y>=135){hour12=!hour12;prefs.putBool("hour12",hour12);}dirty=true;}else if(page==3 && y>=35 && y<=192){openWifi();}else if(y>195&&x<140){lovedUntil=now+6000;prevPose=-1;}}held=touched;
issBuffers(page==5 && !wifiUI);
if(wifiUI)wifiDraw();else if(page==5){drawISS();}else{drawPanel();drawMoon();animate();}if(!wifiUI)drawISSAlert();delay(5);
}Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.




