DIY OLED Planetarium
in Your Palm
Turn a knob and zoom through the Solar System — Earth, Moon, Mars, Jupiter — all rendered on a tiny 1.3" OLED screen. It's the most mesmerising gadget you'll ever build. 🪐✨
🪐 Watch the Planetarium in Action
Rotate the virtual knob below and watch the planets zoom into view — exactly what your OLED display will show.
What Are We Building?
Imagine holding the entire Solar System in the palm of your hand — rotating a small knob and watching Earth melt away into the distance as the Moon fills your screen, then Mars rushes toward you in fiery red, then Jupiter's massive storm bands swirl across the pixels. That's exactly what this gadget does.
The OLED Planetarium is three components working together:
- A KY-040 Rotary Encoder — your navigation dial that detects clockwise and counter-clockwise turns
- An ESP32 microcontroller — the brain that reads the encoder and decides which planet to draw
- A 1.3" SH1106 OLED display — the tiny 128×64 screen that renders each planet with hand-crafted pixel art, smooth zooming, stars, craters, and ring systems
The whole device fits in a small enclosure the size of a TV remote and runs for hours on a USB power bank. It's one of the most impressive-looking builds you can make in a single weekend.
What You'll See at Each Knob Step
Each clockwise click of the rotary encoder zooms you to the next destination with a smooth scale animation.
🌍 Earth
Blue oceans, green continents, swirling white cloud bands, and a soft atmospheric halo — your home planet rendered in 128×64 pixels.
🌕 Moon
Grey cratered surface, large impact basins, a dark terminator line, and the gentle gradient of sunlight across the regolith.
🔴 Mars
Rust-red dust plains, the Valles Marineris canyon system, a white polar ice cap, and the shadow of Olympus Mons volcano.
🪐 Jupiter
Alternating dark and light cloud bands, the iconic Great Red Spot, a faint ring system drawn around the equator, and a hint of moon Io nearby.
Turn the encoder counter-clockwise to zoom back out — Earth ← Moon ← Mars ← Jupiter — in the reverse order.
Everything You Need
Just 5 main components — all common and inexpensive. You can often find them as a bundle kit online.
ESP32 Dev Board (WROOM-32)
The brain — fast dual-core processor, perfect for smooth graphics calculations on the OLED.
1.3" SH1106 OLED Display (I²C)
128×64 monochrome pixels. The SH1106 (not SSD1306) gives a slightly sharper image on 1.3" screens.
KY-040 Rotary Encoder Module
The navigation dial. Has built-in click (press down). Comes with a nice knob cap already attached.
Mini Breadboard + Jumper Wires
For connecting everything without soldering during testing. Go permanent with a PCB after confirming it works!
Small Enclosure (Optional)
A small project box or 3D-printed case makes this look incredibly professional. Or use the breadboard as-is for demos.
Arduino IDE + Libraries
Free to download. You'll need the U8g2 library for OLED drawing and the ESP32 board package.
Full Wiring Diagram
Just 7 wires total — this is one of the simplest circuits in this whole series of projects.
Total wires: 4 for OLED (SDA, SCL, VCC, GND) + 5 for encoder (CLK, DT, SW, V+, GND) = 9 connections. Use 3.3V (NOT 5V) for both components — the ESP32 is a 3.3V device and both the OLED and encoder run at 3.3V. The SW pin of the encoder has a 10kΩ pull-up resistor to 3.3V to prevent floating reads.
Step-by-Step Build
Install Arduino IDE and ESP32 Board Support
Open Arduino IDE → Preferences → paste this URL into "Additional Board Manager URLs":
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
Go to Tools → Board → Board Manager → search "esp32" → Install "esp32 by Espressif Systems". Then select: Tools → Board → ESP32 Arduino → ESP32 Dev Module.
Install the U8g2 Graphics Library
Open Library Manager (Tools → Manage Libraries), search "U8g2" by oliver, install it. This is the most powerful OLED graphics library for ESP32 — it handles the SH1106 perfectly and gives us full drawing functions for circles, filled shapes, and custom fonts.
Wire Up the OLED Display
- OLED SDA → ESP32 GPIO 21
- OLED SCL → ESP32 GPIO 22
- OLED VCC → ESP32 3.3V
- OLED GND → ESP32 GND
Wire Up the Rotary Encoder
- Encoder CLK → ESP32 GPIO 34
- Encoder DT → ESP32 GPIO 35
- Encoder SW → ESP32 GPIO 32 + 10kΩ resistor to 3.3V (pullup)
- Encoder V+ → ESP32 3.3V
- Encoder GND → ESP32 GND
Upload the Code and Test
Copy the full code from the next section into a new Arduino sketch. Connect the ESP32 to your computer with a USB-C cable. Select the correct port under Tools → Port. Click Upload. After uploading, the OLED should show Earth. Turn the encoder clockwise — Moon, Mars, Jupiter!
encoderDir = -1 in the code.Optional: Build an Enclosure
For the most impressive presentation, 3D print a small case (search Thingiverse for "ESP32 OLED encoder case") or use a small project box. Cut holes for the OLED window and encoder knob shaft. Mount the breadboard inside with double-sided tape or M2 screws.
The Complete Planetarium Sketch
This code reads the rotary encoder, smoothly transitions between planet zoom levels, and draws each planet scene entirely from scratch using U8g2 drawing primitives. No images, no external files — pure code.
/* OLED Planetarium — ESP32 + SH1106 1.3" + KY-040 Encoder Turn the knob: Earth → Moon → Mars → Jupiter (and back!) Uses U8g2 library for smooth OLED graphics. */ #include <U8g2lib.h> #include <Wire.h> #include <math.h> // ── OLED: SH1106 128×64 I²C ───────────────────────────────────── U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE, 22, 21); // If using SSD1306 (common 0.96" board) swap the line above for: // U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0,U8X8_PIN_NONE,22,21); // ── Encoder pins ───────────────────────────────────────────────── #define ENC_CLK 34 #define ENC_DT 35 #define ENC_SW 32 volatile int encPos = 0; int lastEncPos = 0; int currentPlanet = 0; // 0=Earth 1=Moon 2=Mars 3=Jupiter // Zoom animation float displayScale = 1.0f; float targetScale = 1.0f; int displayPlanet = 0; const char* PLANET_NAMES[] = {"EARTH", "MOON", "MARS", "JUPITER"}; const char* PLANET_DESC[] = { "Our Blue Planet", "384,400 km away", "The Red Planet", "Gas Giant" }; // Frame counter for simple animation uint32_t frameCount = 0; // ── Encoder ISR ────────────────────────────────────────────────── void IRAM_ATTR encoderISR() { static int lastCLK = HIGH; int clk = digitalRead(ENC_CLK); if (clk != lastCLK && clk == LOW) { if (digitalRead(ENC_DT) != clk) encPos++; else encPos--; } lastCLK = clk; } // ── DRAWING FUNCTIONS ──────────────────────────────────────────── // Draw scattered stars void drawStars(int seed) { srand(seed); for (int i = 0; i < 30; i++) { int sx = rand() % 128; int sy = rand() % 64; if ((frameCount / 8 + i) % 5 == 0) u8g2.drawPixel(sx, sy); // twinkle: skip some stars some frames else u8g2.drawPixel(sx, sy); } } // Draw filled circle outline + disc (helper) void drawPlanetDisc(int cx, int cy, int r) { u8g2.drawDisc(cx, cy, r, U8G2_DRAW_ALL); } // ── EARTH ──────────────────────────────────────────────────────── void drawEarth(int cx, int cy, int r) { drawStars(42); // Ocean (full disc) u8g2.setDrawColor(1); u8g2.drawDisc(cx, cy, r, U8G2_DRAW_ALL); // Continents (XOR pattern = dark on white disc) u8g2.setDrawColor(0); // Europe/Africa blob u8g2.drawEllipse(cx-r/5, cy, r/5, r/3, U8G2_DRAW_ALL); u8g2.drawDisc(cx-r/5, cy+r/3, r/6, U8G2_DRAW_ALL); // Americas blob u8g2.drawEllipse(cx+r/4, cy-r/5, r/6, r/4, U8G2_DRAW_ALL); // Asia blob u8g2.drawEllipse(cx+r/7, cy-r/3, r/4, r/6, U8G2_DRAW_ALL); // Cloud bands (white streaks) u8g2.setDrawColor(1); u8g2.drawLine(cx-r+3, cy-r/3, cx+r-3, cy-r/3-2); u8g2.drawLine(cx-r+5, cy+r/4, cx+r-5, cy+r/4+2); // Planet border u8g2.setDrawColor(1); u8g2.drawCircle(cx, cy, r, U8G2_DRAW_ALL); } // ── MOON ───────────────────────────────────────────────────────── void drawMoon(int cx, int cy, int r) { drawStars(99); u8g2.setDrawColor(1); u8g2.drawDisc(cx, cy, r, U8G2_DRAW_ALL); // Craters (inverse circles) u8g2.setDrawColor(0); u8g2.drawDisc(cx-r/3, cy+r/4, r/6, U8G2_DRAW_ALL); u8g2.drawDisc(cx+r/4, cy-r/4, r/8, U8G2_DRAW_ALL); u8g2.drawDisc(cx+r/5, cy+r/3, r/10, U8G2_DRAW_ALL); u8g2.drawDisc(cx-r/6, cy-r/3, r/9, U8G2_DRAW_ALL); u8g2.drawDisc(cx+r/2-2, cy, r/12, U8G2_DRAW_ALL); // Highlight rings around main craters u8g2.setDrawColor(1); u8g2.drawCircle(cx-r/3, cy+r/4, r/6+1, U8G2_DRAW_ALL); u8g2.drawCircle(cx+r/4, cy-r/4, r/8+1, U8G2_DRAW_ALL); // Dark limb shadow (right side) u8g2.setDrawColor(0); for (int i = 0; i < r/3; i++) { int sx = cx + r - i; int dy2 = (int)sqrt((float)(r*r - (sx-cx)*(sx-cx))); if (dy2 > 0) u8g2.drawLine(sx, cy-dy2, sx, cy+dy2); } u8g2.setDrawColor(1); u8g2.drawCircle(cx, cy, r, U8G2_DRAW_ALL); } // ── MARS ───────────────────────────────────────────────────────── void drawMars(int cx, int cy, int r) { drawStars(77); u8g2.setDrawColor(1); u8g2.drawDisc(cx, cy, r, U8G2_DRAW_ALL); // Surface terrain (darker patches = inverse) u8g2.setDrawColor(0); u8g2.drawEllipse(cx+r/5, cy+r/4, r/4, r/7, U8G2_DRAW_ALL); u8g2.drawEllipse(cx-r/4, cy-r/6, r/6, r/10, U8G2_DRAW_ALL); // Valles Marineris canyon u8g2.drawLine(cx-r/2, cy+r/6, cx+r/2-2, cy+r/7); u8g2.drawLine(cx-r/2+1, cy+r/6+1, cx+r/2-3, cy+r/7+1); // Polar ice cap (white disc at top) u8g2.setDrawColor(1); u8g2.drawEllipse(cx, cy-r+r/5, r/5, r/8, U8G2_DRAW_ALL); // Olympus Mons volcano hint u8g2.drawCircle(cx-r/3, cy-r/4, r/8, U8G2_DRAW_ALL); // Planet border u8g2.drawCircle(cx, cy, r, U8G2_DRAW_ALL); } // ── JUPITER ────────────────────────────────────────────────────── void drawJupiter(int cx, int cy, int r) { drawStars(55); u8g2.setDrawColor(1); u8g2.drawDisc(cx, cy, r, U8G2_DRAW_ALL); // Cloud bands (alternating dark/light strips) u8g2.setDrawColor(0); int bands[] = {-r*4/7, -r*2/7, -r/12, r*2/7, r*4/7}; int bh[] = {2, 3, 3, 2, 2}; for (int i = 0; i < 5; i++) { int by = cy + bands[i]; int hw = (int)sqrt(max(0, r*r - bands[i]*bands[i])); for (int j = 0; j < bh[i]; j++) u8g2.drawLine(cx-hw, by+j, cx+hw, by+j); } // Great Red Spot u8g2.setDrawColor(0); u8g2.drawEllipse(cx-r/5, cy+r/8, r/7, r/10, U8G2_DRAW_ALL); // Ring system (thin horizontal lines above & below equator) u8g2.setDrawColor(1); u8g2.drawLine(cx-r-8, cy-1, cx-r-1, cy-1); u8g2.drawLine(cx+r+1, cy-1, cx+r+8, cy-1); u8g2.drawLine(cx-r-6, cy+1, cx-r-1, cy+1); u8g2.drawLine(cx+r+1, cy+1, cx+r+6, cy+1); // Io moon u8g2.drawDisc(cx+r+12, cy-6, 3, U8G2_DRAW_ALL); // Planet border u8g2.drawCircle(cx, cy, r, U8G2_DRAW_ALL); } // ── UI OVERLAY ─────────────────────────────────────────────────── void drawHUD(int planet, float scale) { // Top bar u8g2.setDrawColor(1); u8g2.setFont(u8g2_font_5x7_tf); u8g2.drawStr(0, 6, PLANET_NAMES[planet]); // Step indicators (top right) for (int i = 0; i < 4; i++) { int sx = 128 - (4-i)*9 + 2; if (i == planet) u8g2.drawBox(sx, 1, 6, 6); else u8g2.drawFrame(sx, 1, 6, 6); } // Bottom description u8g2.setFont(u8g2_font_4x6_tf); u8g2.drawStr(0, 63, PLANET_DESC[planet]); // Zoom percentage char zbuf[12]; snprintf(zbuf, sizeof(zbuf), "ZOOM %d%%", (int)(scale*100)); u8g2.drawStr(80, 63, zbuf); } // ── SETUP ──────────────────────────────────────────────────────── void setup() { Serial.begin(115200); u8g2.begin(); u8g2.enableUTF8Print(); pinMode(ENC_CLK, INPUT_PULLUP); pinMode(ENC_DT, INPUT_PULLUP); pinMode(ENC_SW, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENC_CLK), encoderISR, CHANGE); // Intro splash u8g2.clearBuffer(); u8g2.setFont(u8g2_font_helvB08_tf); u8g2.drawStr(10, 20, "OLED PLANETARIUM"); u8g2.setFont(u8g2_font_5x7_tf); u8g2.drawStr(18, 36, "Turn knob to explore"); u8g2.drawStr(8, 50, "Earth . Moon . Mars . Jupiter"); u8g2.sendBuffer(); delay(2400); } // ── MAIN LOOP ──────────────────────────────────────────────────── void loop() { // Read encoder (clamp 0–3) int pos = constrain(encPos, 0, 3); if (pos != currentPlanet) { currentPlanet = pos; displayScale = 0.2f; // start zoom-in animation } // Smooth zoom easing (lerp toward 1.0) displayScale += (1.0f - displayScale) * 0.18f; if (displayScale > 0.99f) displayScale = 1.0f; // Planet radius based on zoom and OLED size int maxR = 26; // max planet radius in pixels int r = (int)(maxR * displayScale); int cx = 64, cy = 36; // planet centre (slightly above HUD) u8g2.clearBuffer(); switch (currentPlanet) { case 0: if (r > 4) drawEarth(cx, cy, r); break; case 1: if (r > 4) drawMoon(cx, cy, r); break; case 2: if (r > 4) drawMars(cx, cy, r); break; case 3: if (r > 4) drawJupiter(cx, cy, r); break; } drawHUD(currentPlanet, displayScale); u8g2.sendBuffer(); frameCount++; delay(22); // ~45 FPS }
setup(): const int ENC_DIR = -1; and multiply all encPos changes by ENC_DIR. Or simply swap the CLK and DT wires.Your First Planetary Session
Power On — Watch the Splash Screen
The OLED displays "OLED PLANETARIUM" with "Turn knob to explore · Earth · Moon · Mars · Jupiter" for 2.4 seconds. If you don't see this, check the I²C address — most SH1106 boards are at 0x3C. Try 0x3D if it doesn't work.
Earth Appears — Turn Clockwise
Earth zooms into view with its oceans and continents. Give the encoder one click clockwise — the Moon should zoom in from the centre outward. Try it again — Mars appears, then Jupiter with its bands and Great Red Spot.
Counter-Clockwise Returns
Clicking left reverses back through the planets. The little square indicators in the top-right corner of the screen track your current position (1 filled box per planet).
Show It Off!
This device is a conversation starter at any science fair or maker event. Challenge visitors to "find the Great Red Spot" on Jupiter or "spot the Valles Marineris canyon" on Mars. People are always amazed that a ₹1,000 gadget can render this.
🛑 Safety Rules
- Adult supervision recommended for first-time builders, especially when wiring the breadboard.
- Use only 3.3V for the OLED and encoder — connecting 5V to these components can permanently damage them.
- GPIO34 and GPIO35 on the ESP32 are input-only — never try to write output through them.
- If the OLED screen stays blank, always power off before changing any wires — hot-swapping I²C devices can corrupt addresses.
- Keep the USB cable away from the OLED screen — the cable can flex and introduce electrical noise in the I²C line if it runs alongside signal wires.
- When soldering (for permanent mounting), ensure good ventilation and wear eye protection.
Troubleshooting
The OLED stays completely blank.
Check the I²C address: most 1.3" SH1106 boards use 0x3C, but some use 0x3D. In the code, try both U8G2_SH1106_128X64_NONAME_F_HW_I2C constructors with each address. Run an I²C scanner sketch to find the actual address your board reports.
The encoder doesn't change planets when I turn it.
Verify GPIO34 and GPIO35 are wired to CLK and DT respectively. Open Serial Monitor at 115200 baud and add Serial.println(encPos); in loop() to see if the encoder position is changing. If not, the ISR may not be triggering — try swapping CLK and DT pins.
The planets appear but the zoom animation is jerky or frozen.
Reduce the delay(22) value to make animations faster, or increase it to reduce CPU usage. The ESP32 at 240MHz is fast enough to run this smoothly — if it's lagging, check that no other heavy computation is in the loop.
The encoder position jumps by 2 or 4 instead of 1 per click.
The KY-040 encoder generates 4 pulses per detent (mechanical click). Change the ISR to only count every 2nd or 4th pulse: add a counter and only increment encPos when counter % 2 == 0.
I want SSD1306 instead of SH1106 — what changes?
Just swap one line in the code: replace U8G2_SH1106_128X64_NONAME_F_HW_I2C with U8G2_SSD1306_128X64_NONAME_F_HW_I2C. All drawing code stays exactly the same.
The stars twinkle too fast / not enough.
Adjust the frameCount / 8 divisor in the drawStars() function — higher numbers = slower twinkling, lower numbers = faster. Use frameCount / 20 for very slow, dreamlike twinkling.
Fun Space Facts for Your Project Log
Pixels on your OLED — yet all four planets are drawn from pure maths, no images stored anywhere.
Distance from Earth to Jupiter — you just simulated that entire journey in one click of a ₹80 encoder.
Frame rate of the planetarium animation — faster than many video games run on game consoles from 20 years ago.
Screen size in inches — smaller than your thumbnail, yet visible from across a room when lit in a dark environment.
Level Up Your Planetarium
You just wrote the code that draws an entire solar system in 128×64 pixels using nothing but circles, ellipses, lines, and maths. That's the same fundamental skill behind every game, every film visual effect, and every scientific simulation in the world. 🪐

Comments
Post a Comment