A satellite tracker that asks for help

I built a satellite tracker using an ESP32 and two SG90 servos, then hit a hardware wall. Here is how SGP4 propagation and a manual rotation workaround solved it, and why I open sourced the whole thing.

I built a satellite tracker out of an ESP32, two SG90 servos, and a 3D printed pan-tilt mount. It works. It also has a hardware limitation I did not fully think through until I was deep into the build, and the fix for that limitation ended up being the most interesting part of the whole project.

I am open sourcing it as Passtrace. This post is about the constraint, the workaround, and what I learned deciding how to structure and share the project.

The idea

The goal was simple on paper: enter a satellite's TLE, point a camera or antenna at it as it moves across the sky, and keep it pointed there automatically. Two axes of motion, azimuth and elevation, driven by a Python script running SGP4 orbital propagation and sending commands to an ESP32 over WiFi.

The ESP32 firmware itself is deliberately dumb. It exposes one HTTP endpoint that takes an axis and an angle, and moves the corresponding servo there. All the actual tracking intelligence, the orbital math, the coordinate conversion, lives in the Python controller. I like this split. It means the firmware is reusable for anything that needs two-axis servo control over HTTP, not just satellite tracking, and it means the interesting logic is easy to read and modify without touching embedded code.

Where it broke

SG90 servos rotate 180 degrees, not 360. That is fine for elevation, since nothing needs to point below the horizon. It is a real problem for azimuth, since a satellite pass can start in the north and end in the south, crossing through the entire compass.

With the mount fixed in one position, the azimuth servo can only cover half the sky at any given time. Depending on how the mount is oriented, that might be north through east to south, leaving south through west back to north completely unreachable.

I considered a few standard fixes. A continuous rotation servo or a stepper motor with a slip ring would solve this properly, but that means new hardware, new wiring, and more firmware complexity for a project I built mostly to learn SGP4 tracking, not to build a production ground station.

The workaround

Instead, Passtrace asks for help. The Python controller continuously computes the satellite's true azimuth. When that azimuth falls outside what the servo can currently reach, the script knows the satellite has moved into the half of the sky the mount is not facing. At that point it parks the elevation servo at a safe angle, prints a countdown, and waits ten seconds for a human to manually rotate the base 180 degrees.

Once the countdown ends, the script flips an internal mode flag and recalculates azimuth relative to the new orientation. Tracking resumes as if nothing happened, just from the other side.

There is no sensor confirming the rotation actually happened. The script trusts that you did it. That is an honest limitation, not something I tried to hide once I decided to publish the code. If you skip the rotation, tracking will simply be wrong until the next flip cycle corrects itself.

This is the kind of decision that looks obvious in hindsight and was not obvious while building it. The temptation is to solve everything in hardware. Sometimes the better call is to solve half of it in hardware and hand the rest to the person standing next to the device.

Deciding to open source it

I almost did not publish this one. It felt small compared to shipping production systems, and the manual rotation step felt like an admission that I had not fully solved the problem.

Two things changed my mind. First, the constraint and the workaround are the actual engineering story here, and engineers reading a project like this care more about how you handled a real limitation than whether the final result is polished. Second, most of my public work has been services and apps. A hardware and firmware project rounds that out and shows a different kind of thinking, closer to the metal, without a framework doing the heavy lifting.

Once I decided to publish it, I went back through both the firmware and the Python script with fresh eyes, since I was writing this for future me as much as for anyone else. That meant:

  • Moving all WiFi credentials, coordinates, and TLE data into gitignored config files, with example files committed in their place
  • Renaming variables from generic servo1 and servo2 to azimuth and elevation so the code reads the way I actually think about the problem
  • Writing a separate docs page explaining the servo gap and the manual flip, instead of burying that reasoning in code comments where it is easy to miss
  • Being upfront in the README about what the project does not do yet, rather than presenting it as more finished than it is

What is next

The obvious next step is replacing the manual rotation with a continuous rotation servo or stepper motor, which would remove the human step entirely and make this closer to an unattended ground station. I am leaving that as documented future work rather than blocking the release on it, since the current version already does what it set out to do.

If you are building something similar or have thoughts on the approach, the repo is at github.com/ishitraj/passtrace.

← all posts