Right now, about 400 km above your head, seven people are living in the International Space Station (ISS). It flies at 28 000 km/h and goes once round the Earth every 92 minutes. A free web service knows exactly where it is, so your ESP32 can ask every five seconds. With a bit of school geometry it works out how far the station is from your home and which direction it is in. A servo needle shows the distance on a printed dial, and when the station comes within 2000 km, a light comes on and the buzzer beeps. Watch long enough and you will see the needle sweep towards "Overhead" and away again, once every orbit.
What you'll learn
- How to read JSON from a web API over HTTPS
- How to work out the distance between two places on a globe (the haversine formula)
- How to work out the compass direction from one place to another
- How to turn a number into a needle position on a dial
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Servo motor SG90 with horns and screws (ESP32 IoT Starter Kit)
- White LED and 1 resistor, 220 Ω (ESP32 IoT Starter Kit)
- Passive buzzer (ESP32 IoT Starter Kit)
- The printed parts from the 3D printing box below (dial and needle), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| Servo signal (orange) | ESP32 pin 13 |
| Servo + (red) | 5V |
| Servo − (brown) | GND |
| ESP32 pin 25 | 220 Ω resistor, then the LED's long leg |
| LED short leg | GND |
| Passive buzzer + | ESP32 pin 26 |
| Passive buzzer − | GND |
Find your home on the map
- Open a map, for example Google Maps, and right-click on your home.
- The first line of the menu shows two numbers, such as
59.437, 24.754. The first is the latitude, the second the longitude. - Put them into
HOME_LATandHOME_LONin the sketch. Two decimals are plenty: the gauge is about thousands of kilometres.
The sketch
In the Arduino IDE, open Library Manager and install ArduinoJson (by Benoit Blanchon, version 7) and ESP32Servo (by Kevin Harrington).
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const float HOME_LAT = 59.44; // Tallinn; use your own place
const float HOME_LON = 24.75;
const float FAR_KM = 20000; // right end of the dial: the other side of the Earth
const float NEAR_KM = 2000; // closer than this: the light comes on and it beeps
const bool TEST_PARK = false; // true: park the needle in the middle for assembly
const int SERVO_PIN = 13, LED_PIN = 25, BUZZER_PIN = 26;
const unsigned long EVERY = 5000; // ask every 5 seconds
const float EARTH_RADIUS = 6371.0; // km
Servo needle;
bool wasNear = false;
unsigned long lastAsk = 0;
float rad(float degrees) { return degrees * PI / 180.0; }
// Distance along the Earth's surface between two places (the haversine formula)
float distanceKm(float lat1, float lon1, float lat2, float lon2) {
float dLat = rad(lat2 - lat1), dLon = rad(lon2 - lon1);
float a = sin(dLat / 2) * sin(dLat / 2) + cos(rad(lat1)) * cos(rad(lat2)) * sin(dLon / 2) * sin(dLon / 2);
return 2 * EARTH_RADIUS * atan2(sqrt(a), sqrt(1 - a));
}
// Compass direction from place 1 to place 2, 0 = north, 90 = east
float bearing(float lat1, float lon1, float lat2, float lon2) {
float dLon = rad(lon2 - lon1);
float y = sin(dLon) * cos(rad(lat2));
float x = cos(rad(lat1)) * sin(rad(lat2)) - sin(rad(lat1)) * cos(rad(lat2)) * cos(dLon);
float b = atan2(y, x) * 180.0 / PI;
return b < 0 ? b + 360 : b;
}
const char* compassPoint(float b) {
const char* const POINTS[8] = {"north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west"};
return POINTS[(int)((b + 22.5) / 45) % 8];
}
// 0 km = 180 degrees (Overhead, left), FAR_KM = 0 degrees (right)
void showDistance(float km) {
float k = constrain(km, 0, FAR_KM);
needle.write(180 - (int)(k / FAR_KM * 180));
}
bool askWhereIss(float& lat, float& lon, float& height, float& speed, String& light) {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, "https://api.wheretheiss.at/v1/satellites/25544");
int status = http.GET();
if (status != 200) {
Serial.printf("The ISS service answered %d\n", status);
http.end();
return false;
}
String body = http.getString();
http.end();
JsonDocument doc;
if (deserializeJson(doc, body)) return false;
lat = doc["latitude"];
lon = doc["longitude"];
height = doc["altitude"];
speed = doc["velocity"];
light = doc["visibility"].as<String>(); // "daylight" or "eclipsed" (in the Earth's shadow)
return true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
needle.attach(SERVO_PIN);
needle.write(90);
if (TEST_PARK) {
Serial.println("Needle parked at 90 degrees. Fit the needle pointing straight up.");
while (true) delay(1000);
}
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println(" connected");
}
void loop() {
if (millis() - lastAsk < EVERY && lastAsk != 0) return;
lastAsk = millis();
float lat, lon, height, speed;
String light;
if (!askWhereIss(lat, lon, height, speed, light)) return;
float km = distanceKm(HOME_LAT, HOME_LON, lat, lon);
float dir = bearing(HOME_LAT, HOME_LON, lat, lon);
showDistance(km);
bool near = km < NEAR_KM;
digitalWrite(LED_PIN, near);
if (near && !wasNear) { // it just came close: beep three times
for (int i = 0; i < 3; i++) {
tone(BUZZER_PIN, 1760, 120);
delay(250);
}
}
wasNear = near;
Serial.printf("ISS over %.1f, %.1f | %.0f km away, towards the %s | %.0f km up, %.0f km/h | %s\n",
lat, lon, km, compassPoint(dir), height, speed, light.c_str());
}
client.setInsecure() keeps the connection encrypted but skips checking the server's certificate. That keeps the sketch short and is fine for a learning project. For a device you leave running for years, load the server's root certificate with client.setCACert() instead.
Try it
- Open the Serial Monitor at 115200 baud. Every five seconds a line appears, such as
ISS over 12.4, 96.1 | 6820 km away, towards the east | 418 km up, 27580 km/h | daylight. - Check the needle: at 6820 km it points a third of the way from Overhead. It creeps along as the station moves, about 40 km every five seconds.
- Leave it running. Several times a day the station passes within 2000 km of you: the needle swings towards Overhead, the light comes on and the buzzer beeps three times.
- If the line says
eclipsed, the station is in the Earth's shadow. If it saysdaylightand it is dark where you are, and it is close, look up: that is when people see the ISS as a bright, fast-moving star.
How it works
The web service wheretheiss.at calculates the station's position from its orbit and sends it back as JSON, a text format that looks like {"latitude": 12.4, "longitude": 96.1, "altitude": 418, ...}. ArduinoJson turns that text into values the sketch can use, just like in the weather dial.
The Earth is round, so you can't use a ruler on a flat map to find the distance. The haversine formula works on a sphere: it uses the difference in latitude and longitude between your home and the point under the station, and gives the distance along the Earth's surface. The farthest anything can be is halfway round the Earth, about 20 000 km, which is why the dial ends there.
bearing() uses similar trigonometry to find the compass direction to the station, from 0 (north) round to 359. compassPoint() cuts the circle into eight slices of 45 degrees and names them. Finally, showDistance() turns the distance into a servo angle: 0 km is 180 degrees, 20 000 km is 0 degrees, and everything in between is shared out evenly.
The beep only sounds when the station comes close. wasNear remembers the last answer, so the buzzer stays quiet for the rest of the pass.
Try this next
- Add the 1602 LCD and show the distance, the direction and "daylight" or "shadow".
- Make the LED blink faster the closer the station gets.
- Track another satellite: the Hubble Space Telescope has the number 20580 on the same service.
Troubleshooting
- "The ISS service answered -1": the ESP32 couldn't reach the internet. Check the Wi-Fi, and try again in a minute; the free service sometimes has a short break.
-
"answered 429": the sketch asked too often. Keep
EVERYat 5000 or more. -
The needle moves the wrong way: the servo is mounted the other way round. Change the line in
showDistance()toneedle.write((int)(k / FAR_KM * 180));. - The distance seems wrong: check that latitude and longitude aren't swapped, and that places west of Greenwich or south of the equator have a minus sign.
- It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz.
- The board restarts when the servo moves: servos draw bursts of current. Use a good USB port or a powered hub, and check that the servo's red wire is on 5V.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.