WhatsUp? a GPS Keychain That Shows Overhead Planets

by mars91 in Circuits > Sensors

40 Views, 0 Favorites, 0 Comments

WhatsUp? a GPS Keychain That Shows Overhead Planets

check out my circuits at instructables under mars91
PIA01341~large.jpg
dayTime.png
Screenshot 2026-01-28 at 11.53.55 AM.png
IMG_0145.png

This small circuit uses a GPS module to determine its location on Earth by getting the exact latitude, longitude, and time (to atomic-clock accuracy). With a bit of physics, it calculates the positions of all eight planets (and Pluto). Nine RGB LEDs light up when Mercury, Venus, our Moon, Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto are overhead. If a planet is below the horizon, its LED remains off.

Supplies

Screenshot 2026-01-28 at 11.56.12 AM.png
Screenshot 2026-01-28 at 11.46.55 AM.png
Screenshot 2026-01-28 at 11.53.32 AM.png
Screenshot 2026-01-28 at 11.54.06 AM.png

Parts


9 RGB LEDs - WS2812B

2 100k resistors - 0603

4 10uF capacitors - 0603

1 LED - any color 0603

4 10k resistors - 0603

1 ATtiny3216 - MCU

1 220uF capacitor - 3528 tantalum

1 2.2k resistors - 0603

1 SAM-M10Q - Low powered GPS

1 TPS7A0230 - 3.0v LDO SOT-23-5

4 .1uF capacitors - 0603

1 AO3413 - P-Channel Mosfet SOT-23-3

1 MMBT3904 - NPN Transistor SOT-23-3

2 metal buttons

1 47k resistors - 0603

1 IRLML6402 - P-Channel Mosfet SOT-23-3

1 LS14250 case - battery holder

1 LS14250 - battery

1 custom PCB - more on this below


The circuit has a few parts: the ATtiny3216; a latching system to save power; the GPS; the RGB LEDs; and a battery. Many of these parts have drop-in replacements, and the small (0603) capacitors/resistors are generic.


I went with the ATtiny3216 because I wanted to solve the planet problem using only a sparse lookup table; some physics; and GPS provided latitude, longitude, and time. This MCU has a lot of community support, is cheap, easy to flash, and has I2C (the communication protocol needed for the GPS).

I chose the bigger ATtiny3216 over the more common ATtiny1616 (tho same footprint) because I was worried about memory and RAM space. After building it, I do think the ATtiny1616 would have been enough.


The AO3413 and MMBT3904 build the latching circuit. They provide a quick flow of power when the ON button is pressed, then the MCU latches on and keeps itself powered until the job is done.


The 220 µF capacitor and SAM-M10Q are for the GPS module and handling inrush current when the GPS turns on. The SAM-M10Q is expensive (about $15 USD) and is often sold out, but I usually can find it by checking most big-name distributors.


The LS14250 is an odd 3.6 volt battery because i googled "what is the smallest, most powerful battery". The IRLML6402 helps protect the circuit if the battery snaps in backwards. The GPS data sheet says it works best at 3.0 volts, so I used a TPS7A0230 - a 3.0v LDO. There are many drop-in replacements for the TPS7A0230.


The 9 RGB LEDs can get bright and use a lot of current. The open-source library tinyNeoPixel.h lets you choose brightness levels from 0 to 255. I usually keep brightness levels under 50 to keep the current down. Don't turn all 9 LEDs at (R=255, G=255, B=255), instead white should be (R=20, G=20, B=20).


Tools


110V 850W Soldering Hot Plate (this one is big - i'm sure a small one would work)

Solder Paste

Solder Flux

UPDI programmer

Stencil Squeegee


Order Board Here


link to PCB

Or ask me for the Gerber files, I'll try to get them to you

SAM-M10Q Vs the ATtiny3216

hacks-c.jpg

The Struggle and I²C Code


I searched for the lowest-power, high-quality GPS module and selected the SAM-M10Q. Then discovered SparkFun had example code for the SAM-M10Q (major win), and designed the entire circuit around this GPS. Built the board and uploaded SparkFun’s code from: https://github.com/sparkfun/SparkFun_u-blox_SAM-M10Q

And ran into problems: the code is far too big for the ATtiny3216. I spent weeks (maybe months?) trying to adapt SparkFun’s open-source code so it would run on my ATtiny3216. I probably would have given up if the GPS module wasn't 15 dollars.

What made this worse was that there was no serial communication between my board and my computer. I could not "When in doubt, print it out" and only had nine blinking RGB LEDs to talk with. I even created my own messy diagnostic language using the RGB LEDs. Blink red twice if the data packet arrived, blink green if the ring buffer is full, etc...

I finally got a version that I was confident would work, and it still didn’t. Gen AI was not much help either. Finally, I found that adding small delays to the ATtiny3216 I2C communication protocol functions made the system work.

I now have an extremely lightweight C++ implementation ~100 lines of code - that successfully communicates with a SAM-M10Q GPS module over I²C on an ATtiny3216!


The I2C code can be found at this file at the bottom, SAMM10Q.h


Physics and the Ephemeris (space Look Up Table)

dreamstime_m_175599472-scaled.jpg
IMG_0141.png
download.png

The board needs a lightweight, low-memory lookup table (ephemeris) to reference when finding the overhead planets. The ATtiny3216 is not going to run anything big. The method I detail below saves all 9 planets, the Moon, and the Sun in a 10 KB ephemeris for one year with ~99% accuracy (supported by the chip running some physics approximations). The world’s smartest humans have been creating accurate lightweight ephemerides forever, and I am by every definition a newb, but this is my attempt.

I also had no space background until I picked up the book Fundamentals of Astrodynamics by Bate, Mueller, and White (2nd edition) and read it cover to cover which really helped.


Reference Frames

Basically an agreement on where the zero position is — in outer space, the (0,0,0) point.

Using python's Skyfield, grab the planets’ coined GCRS reference frame positions and velocities, known as state vectors. A state vector for a planet has six values: [x, y, z, vx, vy, vz] (position and speed in each axis)

The GCRS frame is very useful because its center position is Earth-centered and it does not spin.

Spinning matters because Earth spins (fast, one rotation every ~24 hours). Using a spinning reference frame can be super useful since we’re also stuck on this spinning rock. However, Mars's position doesn’t care that we spin. Mercury's position doesn’t care that we spin. For example when you look at the Moon, it orbits us about once per month, but we’re spinning while looking at it and seeing the combination of our spin and its movement in space. Kind of like driving on the highway and other cars look like they’re only moving a little because you’re moving too.

Adding Earth’s spin into the planet motion and state vectors adds unnecessary complexity (and can make the physics harder). What is needed is to find where the planets are relative to us first, then worry about our current rotation.


99% Accurate Solution goes like this:

  1. Use python to get the planet’s, the Sun’s, and the Moon’s GCRS state vector and the timestamp.
  2. Save the planets, Moon’s, and Sun’s state vectors as our sparse ephemeris.
  3. The circuit gets an incoming latitude, longitude, and timestamp. It finds the nearest saved state vector and timestamp. (This physics works forward or backward in time, so the nearest timestamp can be in the past or future.)
  4. Using the time difference between the GPS timestamp and the ephemeris timestamp, propagate the planet around the Sun using Newton’s F = MA and Euler’s method of approximation for a second-order differential equation. Yes, Newton’s F = MA for gravity is a second-order (non-linear, non-homogeneous) differential equation :| but Euler makes it manageable.
  5. Propagate the Earth around the Sun too, then subtract Earth’s new position from the planet’s new position. This effectively moves our reference frame back to Earth center(0,0,0). This estimated value is where chip thinks you are in space!
  6. Take the estimated planet position and rotate it to match Earth’s current spin. For the accuracy we need, Earth spins very predictably: once every 86,164.1 seconds. Using the timestamp, we rotate the planet’s position to align with Earth’s current rotation.
  7. The newly rotated vector (known as ECEF) can be projected onto Earth like a map (treating Earth as a decent sphere approximation). You can use the dot product (from calculus) to determine whether the object is overhead or not.
  8. Neat to think this 1 dollar MCU has a fairly accurate idea where we are in space and where you are on Earth!


The Moon...


Ironically, the one thing we most easily see was the hardest to predict...

For example, Pluto and Neptune honestly don’t even need to move with F=MA for my overhead calculations to work. They move so slowly - and are so far away relative to Earth that you get ~100% accuracy whether you propagate them or not (with the given ephemeris) . Mars and Venus, however, only get < 50% accuracy without the Euler improvements. Stepping them forward once using Euler jumps that to ~99%.

There are really two motions that dominate the calculations. Without question, the biggest factor is Earth’s rotation. When you look up in the night sky and see Jupiter in different locations over time, you’re mostly not seeing Jupiter move, you’re seeing the Earth spin. The small amount of motion you do see from Jupiter, Mars, etc. comes from them orbiting the Sun, so we take that into account, mentioned above.

But the Moon is different. Its biggest source of gravity is us (we beat the Sun since we’re much, much closer). And the Moon moves fast compared to outer planets as seen from Earth.

For our sparse data set, the moon is handled by using much smaller time steps and more aggressive F = MA approximations, specifically Runge–Kutta 4 (RK4). On my laptop, solving for the RK4 estimated Moon position is about 30 times slower than for the Euler estimated planets. I can't imagine how much slower it is for the ATtiny3216, so we call this RK4 only once for the moon, getting the over 99% moon estimation accuracy.


looking at the C++ you'll see the Moon's own special function propagate_moon_grcs_fine

propagate_moon_grcs_fine(idx, dt_sec_i64, moon_sv, r_geo);
normalize3(r_geo);
eci_to_ecef_target_timestamp(r_geo, r_ecef, earth_theta);
setLEDOverhead(8, flashState * is_visible_est(r_ecef, currentLat, currentLon),
MOON_R, MOON_G, MOON_B);


And take a look at the Python code — it’ll hopefully help a lot :)

Create the Tables With Python


  1. Run the build_ephemeris_tables.py script first.
  2. It uses Skyfield to sample planet positions every ~6 weeks for a year and saves them as .npy files and a C header (ephemeris_tables.h). That header is what your C++ project includes.
  3. Drop the generated header into your Arduino project.
  4. Now you’ve got static lookup tables for planet position + velocity that your code can access instantly.
  5. Use the test script to sanity-check accuracy.
  6. It randomly picks times, propagates the planet from the nearest table entry, rotates into Earth coordinates, and compares the result against skyfield truth.

run it -> get a header full of ephemeris data -> include it in C++ -> optionally run the tester to make sure nothing’s crazy.

Assembly

612Rz+kdW8L._AC_SL1500_.jpg
IMG_0154.png

Tips


Align the stencil over your board. Spread solder paste over the holes and use a squeegee to push the paste through. Make sure the stencil is flush against the board and everything is nice and clean.

Next, carefully place all the components in their correct spots.

Watch the diode orientation(s) and for the 9 RGB LEDs (the WS2812B). On the PCB, there are two lines around the WS2812B's area. One line has an extra 90-degree bent tip. The side with the bent line should face the “dot” side of the WS2812B. See included pictures.

Reflowing with a Hot Plate

Here’s my process:

  1. I place the board, with all components and solder paste, on the hot plate while it's off.
  2. Then I turn it on and wait for the temperature to reach 215°C.
  3. Around 200°C, you'll see the solder start to melt.
  4. Once it hits 215°C, turn the hot plate off.
  5. Carefully move the board (I use needle-nose pliers) onto a cooling surface (like a cookie sheet).


Post-Reflow Cleanup

Check for solder bridges, unintended blobs connecting pads. If you spot any:

  1. Dab some flux on the bridge.
  2. Use a hot soldering iron and gently drag across the area.
  3. Clean off the flux using isopropyl alcohol and a Q-tip. Burnt flux is sticky and turns yellow-brown.

Also, keep your soldering iron tip clean by wiping it with some solder and a damp sponge.


Soldering the PCB's Back Components

The components on the back need to be soldered by hand. Here’s a great video if you need a refresher.

Programming

IMG_0140.png

This project is a good example of a bare chip that is powerful and easy to program.

This blank chip can be programmed using a UPDI programmer in the Arduino IDE environment. Follow the excellent steps provided by Adafruit.

On the back of this board, there are three pads: Vin, Gnd, and UPDI programmer. I solder three separate male headers onto these pads. I use clay to help align the headers when I solder. Look at the pics above. Soldering and Assembly step below for more detailed build instructions.


Programming


Follow the directions from Adafruit, but I will summarize:

  1. In Arduino, add the following URL to your Boards Manager preferences: http://drazzy.com/package_drazzy.com_index.json
  2. Install megaTinyCore from the Boards Manager.
  3. Under Tools, select:
  4. Board: megaTinyCore: ATtiny3216
  5. Chip: ATtiny3216
  6. Clock: 10 MHz
  7. Programmer: SerialUPDI - SLOW: 57600 baud
  8. Finally, go to Sketch > Upload Using Programmer. Upload a blank sketch to confirm it works.


You should see your UPDI friend blinking and the code being successfully uploaded to the board.

Try It Out

IMG_0157.png
IMG_0150.png

Simply press the ON button. Wait for the GPS to register (this can first take a few minutes). It will flash red while it’s finding the needed satellites. Once locked in, the 9 LEDs will light up to show the planets directly overhead, with over 99% accuracy.

The GPS also has a flashing LED (the small 0603 LED mentioned in the parts list). When that’s flashing, you’re cooking — it means you have a strong connection to the overhead satellites.

Press the adjacent button to turn the circuit off.


Latch

The circuit uses a high-side MOSFET latch, which is just a fancy way of saying that when you press the button, it lets power from the battery reach the MCU. Once the MCU boots up, it uses one of its pins to hold the line low (basically mimicking the button being held down). Pressing the second button turns the circuit off by telling the MCU to release its latch and cut power to itself. If you peek into the code, it might seem weird that a button press shows up as a LOW signal, while the MCU sets the latching pin HIGH to keep itself on. That’s because there’s an NPN transistor in between. To turn on an NPN transistor (which pulls the line to ground and sends the LOW signal), its base has to be driven high by the MCU. This entire latching system is built using a P-channel MOSFET (AO3413), an NPN transistor (MMBT3904), a few resistors, and one button.


I believe since the latch circuit has a 100k pull-up but no direct path to ground, it uses an extremely small amount of current (microamps, or 10⁻⁶ amps) when idle. Maybe someone can correct me? Regardless, in a worst-case scenario this system still uses very little current.


GPS

The GPS does not work well indoors, in concrete buildings, tunnels, etc. When you’re indoors, your iPhone is using Bluetooth, WiFi, cellular triangulation, and a few other tricks to find you — this board does not ¯\_(ツ)_/¯ That said, I can get it to work inside my dense city apartment; it just takes a while near a window.

Once you’re outside and connected to satellites, the GPS has a low-power system that stays connected to the battery all the time and helps prevent a cold start. So if you get one good lock outside, future connections shouldn’t take as long, they should be almost instantaneous.


Code

The code is best explained above. Running the Python script build_ephemeris_tables.py will create an ephemeris_tables.h file that you use in your Arduino project. Remember that this data is only good for a certain amount of time, and you can probably experiment with the limits by changing the sparsity and size of the ephemeris tables.

The C++ code mainly grabs latitude, longitude, and time from the GPS, then uses the physics equations talked about above to estimate the planets’ positions relative to our spinning Earth.

The main C++ loop basically goes like this: read incoming GPS data, if more than three satellites are detected then solve planet positions and update the LEDs, and finally check for a button press to turn the power off.


Warning

The LS14250 is not meant to supply 100+ mA. With 9 RGB LEDs, you could easily exceed that if you run them all at full white (R=255, G=255, B=255). Don’t do that. I keep my LEDs around |RGB| ~ 30.


Thats it!

Thanks for checking out this instructables and hopefully you get to check out some planets.