jakew /
Who Am I?Blog
Get in touch

© 2026 Jake Walklate. All rights reserved.

Back to all articles

International Space Station Orbit Tracker

October 13, 2026
•
28 min read

My implementation of Grady Hillhouses' arduino powered ISS tracker

In December 2020 I stumbled across Grady Hillhouse's YouTube video on building an International Space Station tracker.


Grady who runs the YouTube channel Practical Engineering made a desktop clock-like gadget that follows the international space station around as it orbits the earth, pointing to it's direction via the shortest path around the earths surface.


I was infinitely curious, so I sent Grady an email and asked him about how it worked, he replied which was very polite and kind of him (he has millions of fans commenting on his videos) and he explained to me the variables were hard-coded and static.


Grady suggested an upgrade, so I started to think about what I could do to create version 2.0 of this device.

I drew up some rough sketches, building on his design with auto-homing and Wi-Fi connectivity.


Then I started by importing Grady's code from his GitHub repoistory, and adding WiFi modules and GPS with stepper motors.


And suddenly, I was out of memory. I was using an ATMEGA2560 arduino, but I still didn't have enough flash, so in December 2020 I made the git commit 'I'm out of memory, I need a better microcontroller' and I shelved the project for a later date.

In September 2025 I picked up the project again after I was given an Ardunino Uno R4 Wi-Fi.

I started working on a housing to be 3D printed, I chose ASA as the material due to it's ease to print and toughness, after researching what plastic works best for moving parts I decided on Nylon-PA12 for the bearings, but I didn't actually have Nylon, so I used ASA again but greased it to kingdom come.


The most difficult part of this was the software. Getting a 16x2 LCD to talk via i2c is easy, but getting calculations and movements timing accurate and testing it takes time.

I used an external API to find the ISS's location, then after I got the TLE data I parsed it by sending it as an argument to Grady's code.

Then all I had to do was point in that direction, right?

I used a GPS module to pull my own location data, but after asking some AI chat bots why my pointer was wrong apparently the earths magnetic field moves and a compass doesn't always point true north. Who would have known?

Ok, so I used an API to get the magnetic declination, the ISS TLE data, my own GPS location, and then pointed.

Success, a 5 year long project in the back of my mind finally completed.


Arduino C++ code
/**
 * International Space Station (ISS) orbit tracker
 * Tracks the ISS using TLE data from Celestrak, observer location from GPS,
 * magnetic declination from NOAA, and points a motorized antenna mount
 * towards the ISS.
 *
 * @author svnty (Jake Walklate)
 * @date September 2025
 * @copyright © 2025 Jake Walklate
 */

#include <Arduino.h>
#include <ArduinoJson.h>
#include <LiquidCrystal_I2C.h>
#include <QMC5883LCompass.h>
#include <Servo.h>
#include <TinyGPSPlus.h>
#include <WiFiS3.h>
#include <Wire.h>
#include <math.h>

#include "SGP4_vallado/sgp4coord.cpp"
#include "SGP4_vallado/sgp4coord.h"
#include "SGP4_vallado/sgp4ext.cpp"
#include "SGP4_vallado/sgp4ext.h"
#include "SGP4_vallado/sgp4unit.cpp"
#include "SGP4_vallado/sgp4unit.h"

// -------- SYSTEM TOGGLES --------
static const bool ENABLE_WIFI = true;
static const bool ENABLE_LOG = true;
static const bool ENABLE_GPS = true;

// -------- CONSTANTS --------
// pins
static const uint8_t ELEVATION_PIN = 9;
static const uint8_t AZIMUTH_DIR_PIN = 10;
static const uint8_t AZIMUTH_STEP_PIN = 11;
static const uint8_t GPS_RX_PIN = 12;
// stepper motor
static const float TMC2209_RESISTANCE = 0.11;  // ohms
// wifi
static const char *WIFI_SSID = "Jakes iPhone", *WIFI_PASSWORD = "izeh5zemxa05r";
// http
static const char *DECLINATION_HOST = "www.ngdc.noaa.gov";
static const char *DECLINATION_API_KEY = "zNEw7";
static const char *TLE_API_HOST = "celestrak.org";
static const uint16_t PORT = 443;
// i2c
static const uint8_t MAX_i2c_DEVICES = 16;
// compass
static const float COMPASS_AZIMUTH_OFFSET = 17.0;               // degrees
static const float COMPASS_JUMP_THRESHOLD = 5.0;               // degrees
static const unsigned long COMPASS_CHECK_INTERVAL = 5 * 1000;  // 5 seconds
// lcd
static const unsigned long LCD_UPDATE_INTERVAL = 5 * 1000;  // 5 seconds
// wifi
static const unsigned long WIFI_UPDATE_INTERVAL = 60 * 60 * 1000 + 60 * 30 * 1000;  // 1 hour + 30 minutes
static const unsigned long WIFI_TIMEOUT_TIME = 60 * 1000;          // 1 minute
// gps
static const unsigned long GPS_UPDATE_INTERVAL = 10 * 60 * 1000;  // 10 minutes
static const unsigned long GPS_TIMEOUT_INTERVAL = 60 * 1000;      // 1 minute

// -------- VARIABLES --------
// timers
static unsigned long lastLcdUpdateTime = 0;
static unsigned long lastLcdResetTime = 0;
static unsigned long lastGpsUpdateTime = 0;
static unsigned long lastWiFiUpdateTime = 0;
static unsigned long lastWiFiReconnectAttemptTime = 0;
static unsigned long gpsTimeoutTimer = 0;
static unsigned long lastTimeUpdateMillis = 0;
// http data
static bool foundISSbyWifi = false;
static uint8_t findISSbyWifiAttempts = 0;
static bool foundDeclinationByWifi = false;
static uint8_t findDeclinationByWifiAttempts = 0;
static float magneticDeclination = 12.7;  // degrees
static char tle_line1[70];
static char tle_line2[70];
// gps data
static double observerLatitude = 0.0, observerLongitude = 0.0, observerAltitude = 0.0;
static int year, month, day, hour, minute, second;
static bool isGpsFixed = false;
static uint32_t gpsDateAge = 0;
static uint32_t gpsLocationAge = 0;
// motor control
static float currentAzimuth = 0.0;
static float currentElevation = 95.0;
// compass
static float previousCompassHeading = 0.0;
static unsigned long lastCompassCheckTime = 0;
static unsigned long compassLastCalibrate = 0;
static float compassOffsetX = 0.0, compassOffsetY = 0.0, compassOffsetZ = 0.0;
static float compassScaleX = 1.0, compassScaleY = 1.0, compassScaleZ = 1.0;
// i2c
static uint8_t i2cDeviceAddresses[MAX_i2c_DEVICES];
static uint8_t i2cDeviceCount = 0;
// lcd
static unsigned long lcdDisplayMode = 0;

// -------- SERIALS --------
HardwareSerial &GPS_SERIAL = Serial1;

// -------- OBJECTS --------
TinyGPSPlus gps;
LiquidCrystal_I2C lcd(0x27, 16, 2);
QMC5883LCompass compass;
elsetrec satrec;
Servo elevation;

// -------- FUNCTIONS --------
void calibrateCompass() {
  int minX = 32767, maxX = -32768;
  int minY = 32767, maxY = -32768;
  int minZ = 32767, maxZ = -32768;

  Serial.println("Starting QMC5883L calibration. Please rotate the sensor in all directions for 20 seconds.");
  unsigned long startTime = millis();
  while (millis() - startTime < 20 * 1000) {
    compass.read();

    int x = compass.getX();
    int y = compass.getY();
    int z = compass.getZ();
    minX = min(minX, x); maxX = max(maxX, x);
    minY = min(minY, y); maxY = max(maxY, y);
    minZ = min(minZ, z); maxZ = max(maxZ, z);

    digitalWrite(AZIMUTH_STEP_PIN, HIGH);
    delayMicroseconds(1000);
    digitalWrite(AZIMUTH_STEP_PIN, LOW);
    delayMicroseconds(1000);
  }

  compassOffsetX = (maxX + minX) / 2.0;
  compassOffsetY = (maxY + minY) / 2.0;
  compassOffsetZ = (maxZ + minZ) / 2.0;
  compassScaleX = (maxX - minX) / 2.0;
  compassScaleY = (maxY - minY) / 2.0;
  compassScaleZ = (maxZ - minZ) / 2.0;

  Serial.println("Compass calibration complete.");
  Serial.print("Offset X: "); Serial.println(compassOffsetX);
  Serial.print("Offset Y: "); Serial.println(compassOffsetY);
  Serial.print("Offset Z: "); Serial.println(compassOffsetZ);
  Serial.print("Scale X: "); Serial.println(compassScaleX);
  Serial.print("Scale Y: "); Serial.println(compassScaleY);
  Serial.print("Scale Z: "); Serial.println(compassScaleZ);
  lcdSetSecondLine("OK");
}

void lcdClear() {
  lcd.setCursor(0, 0);
  lcd.print("                ");
  lcd.setCursor(0, 1);
  lcd.print("                ");
}

void lcdSetFirstLine(String firstLineText) {
  lcd.setCursor(0, 0);
  lcd.print("                ");
  lcd.setCursor(0, 0);
  lcd.print(firstLineText);
}

void lcdSetSecondLine(String secondLineText) {
  lcd.setCursor(0, 1);
  lcd.print("                ");
  lcd.setCursor(0, 1);
  lcd.print(secondLineText);
}

void updateLCD(double azimuth_deg, double elevation_deg, double range_km) {
  // reset the LCD every 2 minutes to prevent it from printing garbage
  if (millis() - lastLcdResetTime >= 2 * 60 * 1000) {
    lcd.begin(16, 2);
    lastLcdResetTime = millis();
  }
  lcdClear();

  // If ISS is overhead (elevation > 10°), always show ISS position
  bool issOverhead = elevation_deg > 10.0;

  if (issOverhead) {
    lcdSetFirstLine("ISS VISIBLE!");
    char buffer[17];
    snprintf(buffer, sizeof(buffer), "Az:%03.0f El:%02.0f", azimuth_deg, elevation_deg);
    lcdSetSecondLine(buffer);
    return;
  }

  // If ISS is visible but not overhead, show ISS info
  if (elevation_deg > 0) {
    lcdSetFirstLine("ISS UPWARDS");
    char buffer[17];
    snprintf(buffer, sizeof(buffer), "Az:%03.0f El:%02.0f", azimuth_deg, elevation_deg);
    lcdSetSecondLine(buffer);
    return;
  }

  unsigned long modeTime = (millis() / LCD_UPDATE_INTERVAL) % 7;

  switch (modeTime) {
    case 0:  // ISS status and range
      lcdSetFirstLine("ISS DISTANCE");
      {
        char buffer[17];
        snprintf(buffer, sizeof(buffer), "%03.0f km", range_km);
        lcdSetSecondLine(buffer);
      }
      break;
    case 1:  // Observer location
      lcdSetFirstLine("OBSERVER LOCATION");
      {
        char buffer[17];
        snprintf(buffer, sizeof(buffer), "%.2f,%.2f", observerLatitude, observerLongitude);
        lcdSetSecondLine(buffer);
      }
      break;
    case 2:  // Current time
      lcdSetFirstLine("UTC TIME (24 HR)");
      {
        char buffer[17];
        snprintf(buffer, sizeof(buffer), "%02d:%02d %d/%d/%02d", hour, minute, day, month, (year % 100));
        lcdSetSecondLine(buffer);
      }
      break;
    case 3:  // Compass heading and motor position
      lcdSetFirstLine("COMPASS/MOTOR");
      {
        char buffer[17];
        float compassHeading = getCompassHeading();
        snprintf(buffer, sizeof(buffer), "C:%05.1f M:%05.1f", compassHeading, currentAzimuth);
        lcdSetSecondLine(buffer);
      }
      break;
    case 4:  // ISS current azimuth (even when below horizon)
      lcdSetFirstLine("ISS DIRECTION");
      {
        char buffer[17];
        snprintf(buffer, sizeof(buffer), "Az:%03.0f E:%0.0fkm", azimuth_deg, elevation_deg);
        lcdSetSecondLine(buffer);
      }
      break;
    case 5:
      lcdSetFirstLine("ISS");
      lcdSetSecondLine("BELOW HORIZON");
      break;
    case 6:
      if (isGpsFixed) {
        lcdSetFirstLine("GPS ACCURATE");
      } else {
        lcdSetFirstLine("GPS ERROR");
      }
      if (foundISSbyWifi) {
        lcdSetSecondLine("ISS ACCURATE");
      } else {
        lcdSetSecondLine("ISS DATA ERROR");
      }
      break;
  }
}

void makeTleApiRequest() {
  Serial.println("Making HTTPS API request...");

  WiFiSSLClient client;

  Serial.print("Connecting to ");
  Serial.print(TLE_API_HOST);
  Serial.print(":");
  Serial.println(PORT);

  if (!client.connect(TLE_API_HOST, PORT)) {
    Serial.println("Connection to server failed!");
    lcdSetSecondLine("TLE ERROR");
    delay(2000);
    return;
  }

  Serial.println("Connected to server");

  String request = "GET " + String("/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle") + " HTTP/1.1\r\n";
  request += "Host: " + String(TLE_API_HOST) + "\r\n";
  request += "User-Agent: ArduinoUnoR4Wifi/1.0\r\n";
  request += "Cache-Control: no-cache\r\n";
  request += "Connection: close\r\n";
  request += "\r\n";

  client.print(request);
  Serial.println("Request sent");
  Serial.println("Request details:");
  Serial.println(request);

  String httpResponse = "";
  bool headersEnded = false;
  unsigned long timeout = millis();
  const unsigned long timeoutLimit = 10000;  // 10 second timeout

  while ((client.connected() || client.available()) && (millis() - timeout < timeoutLimit)) {
    if (client.available()) {
      String line = client.readStringUntil('\n');

      if (!headersEnded) {
        Serial.println("Header: " + line);

        // Check for end of headers (empty line)
        if (line.length() <= 1) {
          headersEnded = true;
          Serial.println("--- End of Headers ---");
          Serial.println("Reading response body...");
        }
      } else {
        // We're in the body now
        httpResponse += line;
      }

      // Reset timeout when data is received
      timeout = millis();
    }
    delay(1);
  }

  client.stop();
  Serial.println("Connection closed");

  int issIndex = httpResponse.indexOf("ISS (ZARYA)");
  if (httpResponse.isEmpty() == false && issIndex >= 0) {
    foundISSbyWifi = true;
    Serial.println(httpResponse);
    parseTLEData(httpResponse, issIndex);
    lcdSetSecondLine("OK");
  } else {
    foundISSbyWifi = false;
  }

  if (!foundISSbyWifi) {
    Serial.println("No valid TLE data received!");
    return;
  }
}

void parseTLEData(String data, int issIndex) {
  Serial.println("Parsing TLE data...");

  // Find the first TLE line (starts with "1 25544")
  int line1Index = data.indexOf("1 25544", issIndex);
  if (line1Index == -1) {
    Serial.println("TLE Line 1 not found!");
    return;
  }

  // Find the second TLE line (starts with "2 25544")
  int line2Index = data.indexOf("2 25544", line1Index);
  if (line2Index == -1) {
    Serial.println("TLE Line 2 not found!");
    return;
  }

  Serial.println("Found ISS (ZARYA) data!");

  // Extract Line 1 (69 characters for standard TLE format)
  String line1 = data.substring(line1Index, line1Index + 69);
  line1.trim();

  // Extract Line 2 (69 characters for standard TLE format)
  String line2 = data.substring(line2Index, line2Index + 69);
  line2.trim();

  // Copy to global variables
  strcpy(tle_line1, line1.c_str());
  strcpy(tle_line2, line2.c_str());

  Serial.println("TLE Line 1: " + line1);
  Serial.println("TLE Line 2: " + line2);
  Serial.println("TLE data parsed successfully!");
  lcdSetSecondLine("TLE OK");
}

void makeDeclinationApiRequest() {
  Serial.println("Making HTTPS API request...");

  WiFiSSLClient client;

  Serial.print("Connecting to ");
  Serial.print(DECLINATION_HOST);
  Serial.print(":");
  Serial.println(PORT);

  if (!client.connect(DECLINATION_HOST, PORT)) {
    Serial.println("Connection to server failed!");
    lcdSetSecondLine("DECLINATION ERROR");
    delay(2000);
    return;
  }

  Serial.println("Connected to server");

  char apiPath[256];
  snprintf(apiPath, sizeof(apiPath), "/geomag-web/calculators/calculateDeclination?lat1=%.6f&lon1=%.6f&key=%s&resultFormat=json", observerLatitude, observerLongitude, DECLINATION_API_KEY);

  String request = "GET " + String(apiPath) + " HTTP/1.1\r\n";
  request += "Host: " + String(DECLINATION_HOST) + "\r\n";
  request += "User-Agent: ArduinoUnoR4Wifi/1.0\r\n";
  request += "Accept: application/json\r\n";
  request += "Cache-Control: no-cache\r\n";
  request += "Connection: close\r\n";
  request += "\r\n";

  client.print(request);
  Serial.println("Request sent");
  Serial.println("Request details:");
  Serial.println(request);

  String jsonResponse = "";
  bool headersEnded = false;
  unsigned long timeout = millis();
  const unsigned long timeoutLimit = 10000;

  while ((client.connected() || client.available()) && (millis() - timeout < timeoutLimit)) {
    if (client.available()) {
      String line = client.readStringUntil('\n');

      if (!headersEnded) {
        Serial.println("Header: " + line);

        // Check for end of headers (empty line)
        if (line.length() <= 1) {
          headersEnded = true;
          Serial.println("--- End of Headers ---");
          Serial.println("Reading response body...");
        }
      } else {
        jsonResponse += line;
      }

      // Reset timeout when data is received
      timeout = millis();
    }
    delay(1);
  }

  client.stop();
  Serial.println("Connection closed");

  if (jsonResponse.length() == 0) {
    Serial.println("No response body received!");
    return;
  } else {
    // assuming the data is valid if we got something
    // todo: improve this check
    foundDeclinationByWifi = true;
  }

  parseDeclinationJsonResponse(jsonResponse);
}

String cleanDeclinationChunkedJSON(String rawJson) {
  int jsonStart = rawJson.indexOf('{');

  int jsonEnd = rawJson.lastIndexOf('}');

  if (jsonStart >= 0 && jsonEnd > jsonStart) {
    String cleanJson = rawJson.substring(jsonStart, jsonEnd + 1);
    Serial.println("Cleaned JSON extracted successfully");
    return cleanJson;
  } else {
    Serial.println("Could not find JSON boundaries!");
    return rawJson;
  }
}

void parseDeclinationJsonResponse(String jsonString) {
  Serial.println("Parsing JSON response...");
  Serial.println("Raw JSON:");
  Serial.println(jsonString);
  Serial.println("JSON length: " + String(jsonString.length()));

  String cleanedJson = cleanDeclinationChunkedJSON(jsonString);
  Serial.println("Using cleaned JSON:");
  Serial.println(cleanedJson);

  JsonDocument doc;

  DeserializationError error = deserializeJson(doc, cleanedJson);

  if (error) {
    Serial.print("JSON parsing failed: ");
    Serial.println(error.c_str());
    Serial.println("Attempting to debug JSON structure...");
    Serial.println("First 50 chars: " + cleanedJson.substring(0, 50));
    return;
  }

  Serial.println("JSON parsed successfully!");

  // Parse the NOAA geomagnetic data
  if (doc["result"].is<JsonArray>()) {
    JsonArray results = doc["result"];
    if (results.size() > 0) {
      JsonObject result = results[0];

      if (!result["declination"].isNull()) {
        magneticDeclination = result["declination"];
        foundDeclinationByWifi = true;
        lcdSetSecondLine("DECLINATION OK");
        Serial.print("Magnetic Declination: ");
        Serial.print(magneticDeclination, 5);
        Serial.println("°");
      }
    }
  } else {
    Serial.println("Unexpected JSON structure: 'result' is not an array");
  }
}

bool fetchUtcTimeFromWifi() {
  lcdClear();
  lcdSetFirstLine("GPS BYPASS");
  lcdSetSecondLine("FETCHING TIME...");
  Serial.println("Fetching UTC time via timeapi.io...");

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi not connected, attempting to connect...");
    connectToWiFi();
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("Cannot fetch time: no WiFi");
    lcdSetSecondLine("WIFI ERROR");
    return false;
  }

  const char *TIME_HOST = "timeapi.io";
  const uint16_t TIME_PORT = 443;
  const char *TIME_PATH = "/api/v1/time/current/utc";

  WiFiSSLClient client;
  Serial.print("Connecting to "); Serial.print(TIME_HOST); Serial.print(":"); Serial.println(TIME_PORT);
  if (!client.connect(TIME_HOST, TIME_PORT)) {
    Serial.println("Time API connection failed");
    lcdSetSecondLine("API ERROR");
    return false;
  }

  String request = "GET " + String(TIME_PATH) + " HTTP/1.1\r\n";
  request += "Host: " + String(TIME_HOST) + "\r\n";
  request += "User-Agent: ArduinoUnoR4Wifi/1.0\r\n";
  request += "Accept: application/json\r\n";
  request += "Connection: close\r\n\r\n";

  Serial.println("Time API request:");
  Serial.println(request);
  client.print(request);

  String response = "";
  unsigned long start = millis();
  const unsigned long timeoutLimit = 10000;

  while ((client.connected() || client.available()) && (millis() - start < timeoutLimit)) {
    if (client.available()) {
      response += (char)client.read();
      start = millis();
    }
    delay(1);
  }

  client.stop();

  if (response.length() == 0) {
    Serial.println("Time API returned empty raw response");
    lcdSetSecondLine("INVALID RESPONSE");
    return false;
  }

  Serial.print("Time API raw response length: ");
  Serial.println(response.length());

  int statusLineEnd = response.indexOf('\n');
  String statusLine = "";
  if (statusLineEnd > 0) {
    statusLine = response.substring(0, statusLineEnd);
    Serial.print("Time API status line: ");
    Serial.println(statusLine);
  }

  if (statusLine.indexOf("200") == -1) {
    lcdSetSecondLine("HTTP ERROR");
    return false;
  }

  int bodyStart = response.indexOf("\r\n\r\n");
  Serial.print("Time API body start index: ");
  Serial.println(bodyStart);

  String body = (bodyStart >= 0) ? response.substring(bodyStart + 4) : response;

  String preview = body;
  if (preview.length() > 400) {
    preview = preview.substring(0, 400);
  }

  Serial.println("Time API raw preview:");
  Serial.println(preview);

  // Handles both plain JSON and chunked body framing by extracting between { and }
  String cleaned = cleanDeclinationChunkedJSON(body);

  DynamicJsonDocument doc(512);
  DeserializationError err = deserializeJson(doc, cleaned);
  if (err) {
    Serial.print("Time JSON parse failed: ");
    Serial.println(err.c_str());
    lcdSetSecondLine("TIME PARSE ERR");
    return false;
  }

  if (doc["utc_time"].isNull()) {
    Serial.println("Time API missing utc_time");
    lcdSetSecondLine("TIME FIELD ERR");
    return false;
  }

  String dt = doc["utc_time"].as<String>();
  if (dt.length() < 19) {
    Serial.println("Time API utc_time too short");
    lcdSetSecondLine("TIME FORMAT ERR");
    return false;
  }

  year = dt.substring(0, 4).toInt();
  month = dt.substring(5, 7).toInt();
  day = dt.substring(8, 10).toInt();
  hour = dt.substring(11, 13).toInt();
  minute = dt.substring(14, 16).toInt();
  second = dt.substring(17, 19).toInt();
  lastTimeUpdateMillis = millis();

  Serial.print("UTC time set from API: ");
  Serial.print(year); Serial.print("-"); Serial.print(month); Serial.print("-"); Serial.print(day);
  Serial.print(" "); Serial.print(hour); Serial.print(":"); Serial.print(minute); Serial.print(":"); Serial.println(second);

  lcdSetSecondLine("TIME OK");
  delay(1000);
  return true;
}

void parseISSTLE(const char *line1, const char *line2, elsetrec &satrec) {
  Serial.println("=== TLE PARSING DEBUG ===");
  Serial.print("Input Line 1: ");
  Serial.println(line1);
  Serial.print("Input Line 2: ");
  Serial.println(line2);

  // Extract key parameters for sgp4init
  char temp[20];

  // Extract epoch year and days
  strncpy(temp, line1 + 18, 2);
  temp[2] = '\0';
  int epochyr = atoi(temp);

  strncpy(temp, line1 + 20, 12);
  temp[12] = '\0';
  double epochdays = atof(temp);

  // Extract BSTAR
  strncpy(temp, line1 + 53, 8);
  temp[8] = '\0';
  double bstar = 0.0;
  if (strlen(temp) >= 6) {
    char mantissa[6], exponent[3];
    strncpy(mantissa, temp, 5);
    mantissa[5] = '\0';
    strncpy(exponent, temp + 6, 2);
    exponent[2] = '\0';
    double mant = atof(mantissa) / 100000.0;
    int exp = atoi(exponent);
    if (temp[5] == '-') exp = -exp;
    bstar = mant * pow(10.0, exp);
  }

  // Extract orbital elements from line 2
  strncpy(temp, line2 + 8, 8);
  temp[8] = '\0';
  double inclo = radians(atof(temp));  // Convert to radians

  strncpy(temp, line2 + 17, 8);
  temp[8] = '\0';
  double nodeo = radians(atof(temp));  // Convert to radians

  strncpy(temp, line2 + 26, 7);
  temp[7] = '\0';
  double ecco = atof(temp) / 10000000.0;

  strncpy(temp, line2 + 34, 8);
  temp[8] = '\0';
  double argpo = radians(atof(temp));  // Convert to radians

  strncpy(temp, line2 + 43, 8);
  temp[8] = '\0';
  double mo = radians(atof(temp));  // Convert to radians

  strncpy(temp, line2 + 52, 11);
  temp[11] = '\0';
  double no_revs_per_day = atof(temp);
  double no = no_revs_per_day * 2.0 * PI / 1440.0;  // Convert to rad/min

  // Calculate epoch Julian Day
  int year;
  if (epochyr < 57)
    year = epochyr + 2000;
  else
    year = epochyr + 1900;

  int mon, day, hr, minute;
  double sec;
  days2mdhms(year, epochdays, mon, day, hr, minute, sec);
  double jdsatepoch;
  jday(year, mon, day, hr, minute, sec, jdsatepoch);

  Serial.print("Calling sgp4init with:");
  Serial.print(" inclo=");
  Serial.print(degrees(inclo), 6);
  Serial.print(" nodeo=");
  Serial.print(degrees(nodeo), 6);
  Serial.print(" ecco=");
  Serial.print(ecco, 8);
  Serial.print(" argpo=");
  Serial.print(degrees(argpo), 6);
  Serial.print(" mo=");
  Serial.print(degrees(mo), 6);
  Serial.print(" no=");
  Serial.println(no_revs_per_day, 8);

  // Initialize SGP4 with proper parameters
  bool success = sgp4init(wgs72, 'i', 25544, jdsatepoch, bstar, ecco, argpo, inclo, mo, no, nodeo, satrec);

  satrec.jdsatepoch = jdsatepoch;
  satrec.epochdays = epochdays;
  satrec.epochyr = epochyr;

  if (success) {
    Serial.println("SGP4 initialization successful!");
  } else {
    Serial.print("SGP4 initialization failed with error: ");
    Serial.println(satrec.error);
  }

  Serial.println("========================");
}

void connectToWiFi() {
  lcdClear();
  lcdSetFirstLine("WIFI INIT");
  Serial.print("Connecting to WiFi network: ");
  Serial.println(WIFI_SSID);
  WiFi.disconnect();

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long startTime = millis();

  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - startTime >= WIFI_TIMEOUT_TIME) {
      break;
    }
    delay(500);
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.println();
    Serial.println("WiFi connected!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    Serial.print("Signal strength (RSSI): ");
    Serial.print(WiFi.RSSI());
    Serial.println(" dBm");
    lcdSetSecondLine("OK");

    // Dummy HTTP request to wake up ESP32
    Serial.println("Sending dummy data to wake up ESP32");
    WiFiSSLClient client;
    client.connect("google.com", 443);
    client.println("GET / HTTP/1.1");
    client.println("Host: google.com");
    client.println("Connection: close");
    client.println();
    client.stop();
  }
}

float getCompassHeading() {
  compass.read();
  // Get raw values
  float x = (compass.getX() - compassOffsetX) / compassScaleX;
  float y = (compass.getY() - compassOffsetY) / compassScaleY;
  // Calculate heading from calibrated values
  float heading = atan2(y, x) * 180.0 / PI;
  if (heading < 0) heading += 360.0;
  heading = heading + magneticDeclination + COMPASS_AZIMUTH_OFFSET;
  if (heading < 0.0) heading += 360.0;
  if (heading >= 360.0) heading -= 360.0;
  // If sensor is upside-down, invert
  heading = 360.0 - heading;
  if (heading < 0.0) heading += 360.0;
  if (heading >= 360.0) heading -= 360.0;
  return heading;
}

float calculateAngleDifference(float angle1, float angle2) {
  float diff = angle2 - angle1;
  // Handle wraparound cases
  if (diff > 180) {
    diff -= 360;
  } else if (diff < -180) {
    diff += 360;
  }
  return fabs(diff);
}

void recalibrateAzimuth() {
  lcdSetFirstLine("CALIBRATING");
  lcdSetSecondLine("AZIMUTH");
  float currentHeading = 0.0;

  // Rotate until we're pointing north again
  while (true) {
    i2cBusCheck();
    currentHeading = getCompassHeading();
    previousCompassHeading = currentHeading;

    Serial.print("Calibrating - Current heading: ");
    Serial.println(currentHeading);

    if (fabs(currentHeading) <= 0.5 || fabs(currentHeading) >= 359.5) {
      break;
    }

    if (currentHeading > 180) {
      digitalWrite(AZIMUTH_DIR_PIN, LOW);
    } else {
      digitalWrite(AZIMUTH_DIR_PIN, HIGH);
    }

    // Step the motor towards north
    digitalWrite(AZIMUTH_STEP_PIN, HIGH);
    delayMicroseconds(1000);
    digitalWrite(AZIMUTH_STEP_PIN, LOW);
    delayMicroseconds(1000);
  }

  // Reset our azimuth position tracking
  currentAzimuth = currentHeading;
  compassLastCalibrate = millis();

  lcdSetSecondLine("OK");
  delay(2000);

  Serial.println("Compass recalibration complete");
}

bool checkForCompassJump() {
  float currentHeading = getCompassHeading();
  Serial.print("Current compass heading: ");
  Serial.println(currentHeading);
  Serial.print("Previous compass heading: ");
  Serial.println(previousCompassHeading);

  if (millis() - compassLastCalibrate < 10000) {
    Serial.println("Skipping jump check - within 10s of calibration");
    previousCompassHeading = currentHeading;
    return false;
  }

  if (millis() - lastCompassCheckTime >= COMPASS_CHECK_INTERVAL) {
    lastCompassCheckTime = millis();
    if (fabs(calculateAngleDifference(currentHeading, previousCompassHeading)) >= COMPASS_JUMP_THRESHOLD) {
      previousCompassHeading = currentHeading;
      return true;
    }
  }

  return false;
}

float normalize(float angle) {
  angle = fmod(angle, 360.0);
  if (angle < 0) angle += 360.0;
  return angle;
}

double calculateAzimuth(double lat1, double lon1, double lat2, double lon2) {
  // Convert degrees to radians
  double phi1 = radians(lat1);
  double phi2 = radians(lat2);
  double deltaLambda = radians(lon2 - lon1);

  double y = sin(deltaLambda) * cos(phi2);
  double x = cos(phi1) * sin(phi2) - sin(phi1) * cos(phi2) * cos(deltaLambda);

  double theta = atan2(y, x);
  double azimuth = degrees(theta);

  // Normalize to 0-360
  if (azimuth < 0) azimuth += 360.0;
  if (azimuth >= 360.0) azimuth -= 360.0;
  return azimuth;
}

void moveAzimuthTo(float targetAzimuth) {
  // Apply offset to target azimuth so movement is corrected
  float angleDifference = targetAzimuth - currentAzimuth;
  float currentHeading = 0.0;

  Serial.print("moveAzimuthTo: Target=");
  Serial.print(targetAzimuth, 2);
  Serial.print("°, Current=");
  Serial.print(currentAzimuth, 2);
  Serial.print("°, Diff=");
  Serial.println(angleDifference, 2);

  // Handle wraparound (e.g., from 350° to 10°)
  if (angleDifference > 180) {
    angleDifference -= 360;
  } else if (angleDifference < -180) {
    angleDifference += 360;
  }

  if (fabs(angleDifference) >= 5.0F) {
    lcdClear();
    lcdSetFirstLine("FINDING ISS");
  }

  if (fabs(angleDifference) > 0.5) {
    float initialHeading = getCompassHeading();

    float a1 = normalize(targetAzimuth);
    float a2 = normalize(initialHeading);

    double ccw = fmod((a2 - a1 + 360.0), 360.0);
    double cw = fmod((a1 - a2 + 360.0), 360.0);

    bool moveCounterClockwise = (cw > ccw);

    // COMPASS IS UPSIDE DOWN - invert motor direction
    digitalWrite(AZIMUTH_DIR_PIN, moveCounterClockwise ? HIGH : LOW);

    while (true) {
      i2cBusCheck();
      currentHeading = getCompassHeading();
      float headingDifference = targetAzimuth - currentHeading;
      // Handle wraparound for heading difference
      if (headingDifference > 180) {
        headingDifference -= 360;
      } else if (headingDifference < -180) {
        headingDifference += 360;
      }

      // Check if close enough to target
      if (fabs(headingDifference) <= 0.5) {
        break;
      }

      digitalWrite(AZIMUTH_STEP_PIN, HIGH);
      delayMicroseconds(fabs(headingDifference) > 20.0F ? 500 : 1000);  // fast stepping if far away
      digitalWrite(AZIMUTH_STEP_PIN, LOW);
      delayMicroseconds(fabs(headingDifference) > 20.0F ? 500 : 1000);  // fast stepping if far away
    }

    float finalHeading = getCompassHeading();
    currentAzimuth = finalHeading;
    compassLastCalibrate = millis();

    Serial.println("Finish moveAzimuthTo");
    Serial.print("Previous compass heading: ");
    Serial.println(previousCompassHeading);
    Serial.print("Current azimuth: ");
    Serial.println(currentAzimuth);

    previousCompassHeading = currentHeading;
  }
}

void moveElevationTo(float targetElevation) {
  // Convert elevation angle to servo position, Horizon = 90°, up = 0°, down = 180°
  // ISS elevation: 0° = horizon, 90° = directly up, negative = below horizon

  float servoPosition;
  if (targetElevation < 0) {
    // ISS is below horizon, point down at it
    // Convert negative elevation to servo position below horizon
    servoPosition = 90 + fabs(targetElevation);

    // Constrain to maximum downward angle (180°)
    if (servoPosition > 180) servoPosition = 180;
  } else {
    // Convert ISS elevation (0° = horizon, 90° = up) to servo position (90° = horizon, 0° = up)
    servoPosition = 90 - targetElevation;

    // Constrain servo position to valid range
    if (servoPosition < 0) servoPosition = 0;
  }

  // Only move if the difference is significant (>= 1 degree)
  if (fabs(servoPosition - currentElevation) >= 1.0) {
    elevation.write((int)servoPosition);
    currentElevation = servoPosition;

    // Small delay to allow servo to move
    delay(50);
  }
}

void testAzimuthCalculation(double issLat, double issLon) {
  // Manual calculation using great-circle formula
  double manualAzimuth = calculateAzimuth(observerLatitude, observerLongitude, issLat, issLon);

  Serial.println("*** AZIMUTH COMPARISON TEST ***");
  Serial.print("Observer: ");
  Serial.print(observerLatitude, 6);
  Serial.print(", ");
  Serial.println(observerLongitude, 6);
  Serial.print("ISS: ");
  Serial.print(issLat, 6);
  Serial.print(", ");
  Serial.println(issLon, 6);
  Serial.print("Manual azimuth calculation: ");
  Serial.print(manualAzimuth, 2);
  Serial.println("°");
  Serial.println("********************************");
}

void i2cBusCheck() {
  bool busOk = true;
  for (uint8_t i = 0; i < i2cDeviceCount; i++) {
    Wire.beginTransmission(i2cDeviceAddresses[i]);
    byte err = Wire.endTransmission();
    if (err != 0) {
      busOk = false;
      break;
    }
  }

  if (!busOk) {
    Serial.println("Bus frozen! Recovering...");
    i2cBusRecover();
    return;
  }
}

void i2cBusRecover() {
  const uint8_t SDA_PIN = SDA;
  const uint8_t SCL_PIN = SCL;

  // Clock out 9 pulses to release any stuck slave
  pinMode(SCL_PIN, OUTPUT);
  pinMode(SDA_PIN, INPUT_PULLUP);

  for (int i = 0; i < 9; i++) {
    digitalWrite(SCL_PIN, HIGH);
    delayMicroseconds(5);
    digitalWrite(SCL_PIN, LOW);
    delayMicroseconds(5);
  }

  // Send STOP condition
  pinMode(SDA_PIN, OUTPUT);
  digitalWrite(SDA_PIN, LOW);
  digitalWrite(SCL_PIN, HIGH);
  digitalWrite(SDA_PIN, HIGH);

  // Restore pins for Wire
  pinMode(SDA_PIN, INPUT_PULLUP);
  pinMode(SCL_PIN, INPUT_PULLUP);

  // Reinitialize I2C master
  Wire.begin();
  Wire.setClock(10000);
  Serial.println("I2C bus recovered!");
}

// -------- ARDUINO --------
void setup() {
  Serial.begin(115200);
  Serial.println("Initializing Serial...");
  Wire.begin();
  Wire.setClock(10000);
  i2cBusRecover();

  for (uint8_t address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    byte error = Wire.endTransmission();
    if (error == 0) {
      if (i2cDeviceCount < MAX_i2c_DEVICES) {
        i2cDeviceAddresses[i2cDeviceCount++] = address;
        Serial.print("Found device at 0x");
        Serial.println(address, HEX);
      }
    }
  }

  // Elevation Motor Initialization
  elevation.attach(ELEVATION_PIN);
  elevation.write(currentElevation);

  // Azimuth Motor Initialization
  pinMode(AZIMUTH_STEP_PIN, OUTPUT);
  pinMode(AZIMUTH_DIR_PIN, OUTPUT);
  digitalWrite(AZIMUTH_DIR_PIN, HIGH);

  // LCD Initialization
  lcd.init();
  lcd.backlight();
  lcdSetFirstLine("ARDUINO INIT");
  delay(1500);

  // WiFi Initialization
  if (ENABLE_WIFI) {
    connectToWiFi();

    int wifiReconnectAttempts = 1;

    while (WiFi.status() != WL_CONNECTED) {
      wifiReconnectAttempts++;
      lcdSetSecondLine("ATTEMPT " + String(wifiReconnectAttempts));
      connectToWiFi();
      if (wifiReconnectAttempts >= 3) {
        lcdSetSecondLine("ERROR");
        break;
      }
    }

    if (WiFi.status() == WL_CONNECTED) {
      lcdSetSecondLine("FETCH ISS TLE");
      while (!foundISSbyWifi && findISSbyWifiAttempts < 3) {
        findISSbyWifiAttempts++;
        makeTleApiRequest();
      }

      lcdSetSecondLine("FETCH DECLINATION");
      while (!foundDeclinationByWifi && findDeclinationByWifiAttempts < 3) {
        findDeclinationByWifiAttempts++;
        makeDeclinationApiRequest();
      }
    } else {
      lcdSetSecondLine("ERROR");
    }
  }
  if (!foundISSbyWifi) {
    strcpy(tle_line1, "1 25544U 98067A   26122.84497350  .00006801  00000+0  13115-3 0  9991");
    strcpy(tle_line2, "2 25544  51.6308 164.5377 0007264  16.4817 343.6406 15.49068051564723");
    lcdSetFirstLine("USING DEFAULT TLE");
  }
  if (foundDeclinationByWifi && foundISSbyWifi) {
    lastWiFiUpdateTime = millis();
  }
  parseISSTLE(tle_line1, tle_line2, satrec);
  lcdSetSecondLine("DATA PARSED OK");
  delay(1000);
  lcdClear();

  // GPS Initialization
  if (ENABLE_GPS) {
    lcdSetFirstLine("GPS INIT");
    Serial.println("Initializing GPS...");
    GPS_SERIAL.begin(9600);
    updateGPSData();

    if (gps.location.isValid()) {
      lcdSetSecondLine("LOCATION OK");
      delay(2000);
    } else {
      lcdSetSecondLine("NO LOCATION");
      delay(2000);
    }

    if (gps.date.isValid() && gps.date.year() > 2024) {
      lcdSetSecondLine("DATE OK");
      delay(2000);
    } else {
      lcdSetSecondLine("NO DATE");
      delay(2000);
    }

    lcdClear();
  }

  if (!gps.location.isValid()) {
    Serial.println("Gps location: INVALID");
    // Default to Haymarket, Australia if GPS data is not available
    observerLatitude = -33.88336;
    observerLongitude = 152.20148;
    observerAltitude = 0.035;
  }
  if (!gps.date.isValid() || gps.date.year() < 2024) {
    Serial.println("Gps date: INVALID");
    bool timeSet = false;
    // Try getting UTC time over WiFi if available
    if (ENABLE_WIFI) {
      int attempts = 0;
      while (attempts < 3 && !timeSet) {
        attempts++;
        if (fetchUtcTimeFromWifi()) {
          timeSet = true;
          break;
        }
        delay(500);
      }
    }
    lcdClear();
    // Fallback to hardcoded guess if time fetch failed
    if (!timeSet) {
      lcdSetFirstLine("TIME FAIL");
      lcdSetSecondLine("GUESSING DATE");
      year = 2026;
      month = 5;
      day = 4;
      hour = 12;
      minute = 0;
      second = 0;
      delay(500);
    }
  }

  // Magnometer Initialization
  lcdSetFirstLine("QMC5883L INIT");
  compass.init();
  calibrateCompass();
  delay(2000);
  lcdClear();

  // Initial compass calibration for azimuth
  recalibrateAzimuth();
  previousCompassHeading = getCompassHeading();
  lastCompassCheckTime = millis();

  lcdClear();
}

void updateLocalTime() {
  unsigned long currentMillis = millis();

  // If this is the first time or if millis() has rolled over, initialize
  if (lastTimeUpdateMillis == 0 || currentMillis < lastTimeUpdateMillis) {
    lastTimeUpdateMillis = currentMillis;
    return;
  }

  // Calculate elapsed seconds since last update
  unsigned long elapsedMillis = currentMillis - lastTimeUpdateMillis;
  unsigned long elapsedSeconds = elapsedMillis / 1000;

  if (elapsedSeconds > 0) {
    // Update the time variables
    second += elapsedSeconds;

    // Handle seconds overflow
    if (second >= 60) {
      minute += second / 60;
      second = second % 60;
    }

    // Handle minutes overflow
    if (minute >= 60) {
      hour += minute / 60;
      minute = minute % 60;
    }

    // Handle hours overflow (24-hour format)
    if (hour >= 24) {
      hour = hour % 24;
      // Note: We're not updating day/month/year here since that would
      // require more complex calendar logic. GPS updates every 10 minutes
      // should handle date changes adequately.
    }

    // Update the timestamp
    lastTimeUpdateMillis = currentMillis;
  }
}

void updateGPSData() {
  Serial.println("Updating GPS data...");
  gpsTimeoutTimer = millis();
  uint8_t lastCountdownSec = 255;

  while (
    (!gps.location.isValid() && !gps.date.isValid()) || 
    ((gps.date.age() > GPS_UPDATE_INTERVAL) || (gps.location.age() > GPS_UPDATE_INTERVAL))
  ) {
    unsigned long elapsed = millis() - gpsTimeoutTimer;
    unsigned long remainingMs = (elapsed >= GPS_TIMEOUT_INTERVAL) ? 0 : (GPS_TIMEOUT_INTERVAL - elapsed);
    uint8_t remainingSec = (uint8_t)((remainingMs + 999) / 1000);

    if (remainingSec != lastCountdownSec) {
      char countdown[17];
      snprintf(countdown, sizeof(countdown), "SEARCHING.. %lus", remainingSec);
      lcdSetSecondLine(countdown);
      lastCountdownSec = remainingSec;
    }

    if (millis() - gpsTimeoutTimer >= GPS_TIMEOUT_INTERVAL) {
      if (ENABLE_LOG) {
        Serial.println("GPS timeout waiting for initial fix");
      }
      lcdSetSecondLine("GPS TIMEOUT");
      break;
    }
    while (GPS_SERIAL.available()) {
      char c = GPS_SERIAL.read();
      if (ENABLE_LOG) {
        Serial.write(c);
      }
      gps.encode(c);
    }
  }

  if (
    (!gps.location.isValid() && !gps.date.isValid()) || 
    ((gps.date.age() > GPS_UPDATE_INTERVAL) || (gps.location.age() > GPS_UPDATE_INTERVAL))
  ) {
    if (ENABLE_LOG) {
      Serial.println("GPS lost fix or outdated - marking as unfixed");
    }
    isGpsFixed = false;
  } else {
    isGpsFixed = true;
  }

  // Update our variables with the current GPS data (fresh or existing)
  if (gps.date.isValid()) {
    // Compose GPS time as seconds since midnight
    if (gps.date.year() > 2024) {
      // If GPS date is valid and reasonable, use it to update local time
      int gpsTotalSeconds = gps.time.hour() * 3600 + gps.time.minute() * 60 + gps.time.second();
      int localTotalSeconds = hour * 3600 + minute * 60 + second;
      // Only update if GPS time is ahead of local time, or if local time is out of bounds
      if (gpsTotalSeconds >= localTotalSeconds || localTotalSeconds < 0 || localTotalSeconds >= 86400) {
        year = gps.date.year();
        month = gps.date.month();
        day = gps.date.day();
        hour = gps.time.hour();
        minute = gps.time.minute();
        second = gps.time.second();
        // Reset local time tracking since we have GPS time
        lastTimeUpdateMillis = millis();
        gpsDateAge = gps.date.age();
        gpsLocationAge = gps.location.age();

        if (ENABLE_LOG) {
          Serial.println("GPS time data age: ");
          Serial.print(gps.time.age());
          Serial.println(" ms");
        }
    }
    } else {
      if (ENABLE_LOG) {
        Serial.println("GPS time is older than local time, not updating.");
      }
    }
  }

  // If GPS date is not valid, try fetching UTC time over WiFi as a fallback
  if (!gps.date.isValid() || gps.date.year() < 2024) {
    if (ENABLE_WIFI) {
      if (WiFi.status() != WL_CONNECTED) {
        connectToWiFi();
      }
      if (WiFi.status() == WL_CONNECTED) {
        int attempts = 0;
        while (attempts < 3 && !fetchUtcTimeFromWifi()) {
          attempts++;
          delay(500);
        }
      }
    }
  }

  if (gps.location.isValid()) {
    observerAltitude = gps.altitude.kilometers();
    observerLatitude = gps.location.lat();
    observerLongitude = gps.location.lng();

    if (ENABLE_LOG) {
      Serial.println("GPS location data age: ");
      Serial.print(gps.location.age());
      Serial.println(" ms");
    }
  }

  if (ENABLE_LOG) {
    Serial.println();
    Serial.print("GPS Date: ");
    Serial.print(gps.date.year());
    Serial.print("-");
    Serial.print(gps.date.month());
    Serial.print("-");
    Serial.println(gps.date.day());
    Serial.print("GPS Time (UTC): ");
    Serial.print(gps.time.hour());
    Serial.print(":");
    Serial.print(gps.time.minute());
    Serial.print(":");
    Serial.println(gps.time.second());
    Serial.print("GPS Latitude: ");
    Serial.println(gps.location.lat(), 6);
    Serial.print("GPS Longitude: ");
    Serial.println(gps.location.lng(), 6);
    Serial.print("GPS Altitude (km): ");
    Serial.println(gps.altitude.kilometers(), 3);
  }

  lastGpsUpdateTime = millis();
}

void loop() {
  i2cBusCheck();
  updateLocalTime();
  
  // update GPS data every hour
  if (ENABLE_GPS) {
    if (millis() - lastGpsUpdateTime >= GPS_UPDATE_INTERVAL) {
      lcdClear();
      lcdSetFirstLine("UPDATING GPS");
      updateGPSData();
    }
  }

  // update the TLE and declination every hour
  if (ENABLE_WIFI) {
    if (millis() - lastWiFiUpdateTime >= WIFI_UPDATE_INTERVAL) {
      lcdClear();
      lcdSetFirstLine("UPDATING TLE");
      if (WiFi.status() != WL_CONNECTED) {
        connectToWiFi();
      }
      if (WiFi.status() == WL_CONNECTED) {
        makeTleApiRequest();
        makeDeclinationApiRequest();
      }
      lastWiFiUpdateTime = millis();
    }
  }

  // Check for compass jumps indicating the tracker was moved
  if (checkForCompassJump()) {
    recalibrateAzimuth();
  }

  double jdnow;
  jday(year, month, day, hour, minute, second, jdnow);

  if (ENABLE_LOG) {
    Serial.println("==================");
    Serial.print("Current time: ");
    Serial.print(year);
    Serial.print("/");
    Serial.print(month);
    Serial.print("/");
    Serial.print(day);
    Serial.print(" ");
    Serial.print(hour);
    Serial.print(":");
    Serial.print(minute);
    Serial.print(":");
    Serial.println(second);
    Serial.print("Julian day now: ");
    Serial.println(jdnow, 8);
    Serial.print("Satellite epoch JD: ");
    Serial.println(satrec.jdsatepoch, 8);
    Serial.print("TLE Line 1: ");
    Serial.println(tle_line1);
    Serial.print("TLE Line 2: ");
    Serial.println(tle_line2);
  }

  double tsince = (jdnow - satrec.jdsatepoch) * 24.0 * 60.0;

  if (ENABLE_LOG) {
    Serial.print("Time since epoch (minutes): ");
    Serial.println(tsince, 2);
  }

  double ro[3], vo[3];
  sgp4(wgs72, satrec, tsince, ro, vo);

  if (satrec.error != 0) {
    lcdSetFirstLine("SGP4 ERROR");
    lcdSetSecondLine(String(satrec.error));
    Serial.print("SGP4 Error: ");
    Serial.println(satrec.error);
    delay(1000);
    return;
  }

  if (ENABLE_LOG) {
    Serial.print("SGP4 position (km): ");
    Serial.print(ro[0], 3);
    Serial.print(", ");
    Serial.print(ro[1], 3);
    Serial.print(", ");
    Serial.println(ro[2], 3);

    // Convert ISS TEME position to lat/lon for comparison
    double issLatLonAlt[3];
    double recef[3], vecef[3];
    teme2ecef(ro, vo, jdnow, recef, vecef);
    ijk2ll(recef, issLatLonAlt);

    Serial.print("ISS calculated lat: ");
    Serial.println(degrees(issLatLonAlt[0]), 6);
    Serial.print("ISS calculated lon: ");
    Serial.println(degrees(issLatLonAlt[1]), 6);
    Serial.print("ISS calculated alt (km): ");
    Serial.println(issLatLonAlt[2], 3);
  }

  double razel[3], razelrates[3];

  if (ENABLE_LOG) {
    Serial.print("Observer lat (deg): ");
    Serial.println(observerLatitude, 6);
    Serial.print("Observer lon (deg): ");
    Serial.println(observerLongitude, 6);
    Serial.print("Observer alt (km): ");
    Serial.println(observerAltitude, 6);
    Serial.print("Observer lat (rad): ");
    Serial.println(radians(observerLatitude), 6);
    Serial.print("Observer lon (rad): ");
    Serial.println(radians(observerLongitude), 6);
  }

  rv2azel(ro, vo, radians(observerLatitude), radians(observerLongitude), observerAltitude, jdnow, razel, razelrates);

  if (ENABLE_LOG) {
    Serial.print("Raw razel: ");
    Serial.print(razel[0], 3);
    Serial.print(" km, ");
    Serial.print(degrees(razel[1]), 3);
    Serial.print("° az, ");
    Serial.print(degrees(razel[2]), 3);
    Serial.println("° el");
  }

  double azimuth_deg = degrees(razel[1]);
  double elevation_deg = degrees(razel[2]);
  double range_km = razel[0];
  if (azimuth_deg < 0) {
    azimuth_deg += 360.0;
  }

  // Test with actual SGP4 ISS position instead of hardcoded coordinates
  if (ENABLE_LOG) {
    // Get ISS lat/lon from SGP4 for manual calculation
    double issLatLonAlt[3];
    double recef[3], vecef[3];
    teme2ecef(ro, vo, jdnow, recef, vecef);
    ijk2ll(recef, issLatLonAlt);

    double issLatDeg = degrees(issLatLonAlt[0]);
    double issLonDeg = degrees(issLatLonAlt[1]);

    testAzimuthCalculation(issLatDeg, issLonDeg);
  }

  if (ENABLE_LOG) {
    Serial.print("FINAL: Azimuth: ");
    Serial.print(azimuth_deg, 2);
    Serial.print("° (");
    // Add cardinal direction interpretation
    if (azimuth_deg >= 337.5 || azimuth_deg < 22.5)
      Serial.print("N");
    else if (azimuth_deg >= 22.5 && azimuth_deg < 67.5)
      Serial.print("NE");
    else if (azimuth_deg >= 67.5 && azimuth_deg < 112.5)
      Serial.print("E");
    else if (azimuth_deg >= 112.5 && azimuth_deg < 157.5)
      Serial.print("SE");
    else if (azimuth_deg >= 157.5 && azimuth_deg < 202.5)
      Serial.print("S");
    else if (azimuth_deg >= 202.5 && azimuth_deg < 247.5)
      Serial.print("SW");
    else if (azimuth_deg >= 247.5 && azimuth_deg < 292.5)
      Serial.print("W");
    else if (azimuth_deg >= 292.5 && azimuth_deg < 337.5)
      Serial.print("NW");
    Serial.print("), Elevation: ");
    Serial.print(elevation_deg, 2);

    // Add visibility status
    if (elevation_deg > 0) {
      Serial.print("° (VISIBLE)");
    } else {
      Serial.print("° (BELOW HORIZON - ISS NOT VISIBLE)");
    }

    Serial.print(", Range: ");
    Serial.print(range_km, 2);
    Serial.println(" km");

    // Add compass vs SGP4 comparison
    float currentCompass = getCompassHeading();
    Serial.print("Current compass heading: ");
    Serial.print(currentCompass, 2);
    Serial.print("° vs SGP4 azimuth: ");
    Serial.print(azimuth_deg, 2);
    Serial.print("° (diff: ");
    Serial.print(azimuth_deg - currentCompass, 2);
    Serial.println("°)");

    Serial.println("==================");
  }

  moveElevationTo((float)elevation_deg);
  moveAzimuthTo((float)azimuth_deg);

  // Update LCD less frequently to reduce I2C interference
  if (millis() - lastLcdUpdateTime >= LCD_UPDATE_INTERVAL) {
    updateLCD(azimuth_deg, elevation_deg, range_km);
    lastLcdUpdateTime = millis();
  }
}