Astra - a Smart Telescope That Streams to Your Phone

by Makestreme in Design > 3D Design

6154 Views, 33 Favorites, 0 Comments

Astra - a Smart Telescope That Streams to Your Phone

I Built a Smart Telescope That Streams to Your Smartphone
Screenshot 2026-07-05 122805.png
Screenshot 2026-07-05 114012.png
Screenshot 2026-07-05 114201.png
Screenshot 2026-07-05 114045.png
Screenshot 2026-07-05 114149.png

I always wanted to own a telescope. Just the thought of being able to see the celestial objects I’d seen on TV or read about in books with my own eyes fascinated me. I'd been saving up to buy one for some time. But good telescopes are not affordable by any means. For the budget I had, I could only get cheap refractor telescopes with about a 50 mm aperture. So, I decided to build one myself.

Now I wanted my telescope to solve two of the biggest issues with standard telescopes:

  1. Having to squish your eye against a small eyepiece while you try to focus and point the telescope at the right direction.
  2. Not having the ability to easily save a digital copy of the image you’re seeing.

The most popular open-source telescopes I could find online was the Hadley Telescope and the PiKon telescope project. The thing that attracted to the Pikon me was its use of a cheap PVC pipe for the main body and a Raspberry Pi camera mounted directly at the focal plane instead of using an eyepiece. The only downside I could see was the fact that you had to connect it physically to a PC and a power cable to view the image.

So, I decided to make it battery powered and add wireless streaming to any smartphone so that I can carry it anywhere and have freedom to move it around however I like. Plus, the original Pikon design is a bit old so I made quite a few design and software upgrades to make it the ultimate digital telescope. I call it Astra (because it not only sounds like “Astro”, but also, in Sanskrit, Astra means something that is launched at a target).

Supplies

  1. 114/900 Telescope Primary Mirror: Amazon international seller, Indian seller
  2. PVC tube of 5-inch inner diameter and 750-950mm length


3D prints (Find them here)

  1. Mirror mount
  2. Mirror Base
  3. Spider
  4. Camera holder
  5. Cog
  6. Electronics mount
  7. Tripod mount
  8. Smartphone mount


Electronics

  1. Raspberry Pi Zero 2 W — https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/
  2. Raspberry Pi Camera Module (Camera Module 3 recommended) — https://www.raspberrypi.com/products/camera-module-3/
  3. Raspberry Pi Zero Camera Cable (22-pin to 15-pin CSI cable) — https://www.raspberrypi.com/products/raspberry-pi-zero-camera-cable/
  4. Micro SD Card (minimum 4 GB; 16–32 GB Class 10 recommended) — https://www.amazon.com/s?k=micro+sd+card+32gb
  5. 18650 Li-ion Battery — https://www.digikey.com/en/products/filter/batteries-rechargeable-secondary/91
  6. 5 V Battery Boost/Charge Module (TP4056 + 5 V boost) — https://www.amazon.com/s?k=5v+boost+charger+module+18650
  7. Momentary Push Switch — https://www.digikey.com/en/products/filter/pushbutton-switches/199
  8. Spare Micro-USB Cable — https://www.amazon.com/s?k=micro+usb+cable


Hardware

  1. 10× M3.5 Self-Tapping Screws — https://www.mcmaster.com/self-tapping-screws/
  2. 3× 8 mm Nuts and Bolts — https://www.mcmaster.com/metric-screws/
  3. 3× 10 mm Compression Springs — https://www.mcmaster.com/compression-springs/
  4. 4× M2 Nuts and Bolts — https://www.mcmaster.com/m2-screws/
  5. M5 Threaded Rod (minimum 100 mm length) — https://www.mcmaster.com/threaded-rods/
  6. 2× M4 Nuts and Bolts — https://www.mcmaster.com/m4-screws/
  7. Strong Double-Sided Mounting Tape (e.g. 3M VHB) — https://www.amazon.com/s?k=3m+vhb+tape



Tools

  1. Screw driver
  2. Nose pliers
  3. Soldering iron

How It Works

telescopeexplainer1-ezgif.com-added-text.gif
telescopeexplainer2-ezgif.com-added-text.gif

Before starting any project, understanding how it works makes everything much easier later on.

A reflecting telescope uses a concave mirror to collect incoming light and focus it into a tiny image. Then a secondary mirror and eyepiece is used to make the image viewable. Two things mainly determine how capable a telescope is.

The first is the aperture: the diameter of the primary mirror. A larger aperture collects more light, making faint objects brighter and revealing finer details.

The second is the focal length: the distance between the mirror and where it focuses all the incoming light. A longer focal length produces a larger image at the focal plane.

Instead of using a secondary mirror and an eyepiece, we're placing a Raspberry Pi camera directly at the focal plane. The camera sensor simply captures the image that the telescope forms. There's just one problem. The camera already has its own lens that's designed to focus light onto the sensor. But our telescope has already done that job. So we remove the camera lens entirely, exposing only the image sensor. Then all we have to do is position that sensor exactly at the telescope's focal point.

The Primary Mirror

Screenshot 2026-07-05 114412.png
Screenshot 2026-07-05 114355.png

The heart of the telescope is the primary mirror. For this design, we want a 114mm diameter and 700 to 900mm focal length mirror.

Finding reliable sellers online isn't easy, especially because damage to the reflective coating or imperfections in the mirror's surface can noticeably affect the final image. I spent weeks researching and eventually found a few reliable sellers. I've attached links to both Indian and international suppliers in the supplies section.

After the mirror arrived, I gave it a quick visual inspection to rule out any major scratches, chips, or coating damage.

And whatever you do, do not touch the reflective surface! Even if there's a little dust on it, it's usually better to leave it alone or gently blow it off. Improper cleaning is far more likely to damage the mirror than a few specks of dust.

PVC Tube

Screenshot 2026-07-05 114605.png
Screenshot 2026-07-05 114523.png

I first purchased a standard 5-inch PVC pipe. Make sure the length of the tube is at least as long as the focal length of your primary mirror. Mine had a focal length of 700 mm, so I cut the tube to 750 mm just to be safe. Then I measured the tube's actual inner diameter and realized it was 5.25 inches, not 5 inches. That's because PVC pipes are usually sold by their nominal size, not their actual dimensions. So the standard Pikon files wouldn’t fit.

3D Printing

Screenshot 2026-07-05 114705.png
Screenshot 2026-07-05 114722.png
Screenshot 2026-07-05 114741.png
Screenshot 2026-07-05 114755.png

I used Fusion 360 to make and update the parts of the telescope. I used the spider module and mirror assembly designs from the original pikon design, but modified them slightly to match my tube. While I was at it, I made a few design upgrades too. I used a remix of the camera mount that included a 3D-printed rack-and-pinion focuser.

I also redesigned the Raspberry Pi mount to hold my electronics. This is because the original design used a Raspberry Pi 3, but we’re using a cluster of electronics along with a Pi zero 2w.

Plus, I designed a simpler universal tripod mount and a smartphone mount for a neat smart feature I'll talk about later.

All my 3D files are attached below. I've also included a link to the original PiKon files here. Just make sure you measure your PVC tube first and use the version that matches its diameter.

Once all the designs were ready, I sent them off for 3D printing. I recommend using PETG or ABS instead of PLA because they're much more resistant to heat. Telescopes rely on precise alignment, and if the printed parts warp in the heat, it can throw the optics out of alignment.


Note: I'm having trouble uploading some stl files here. Find all the required files in my Github repo

Focus and Collimation

focus_coll1-ezgif.com-added-text.gif
focus_coll2-ezgif.com-added-text.gif
Screenshot 2026-07-05 114909.png
Screenshot 2026-07-05 114919.png

Now there are two more concepts we'll need to understand before we start assembling: focus and collimation.

Let's start with focus. The primary mirror forms an image at a specific distance. By moving the camera sensor slightly forward or backward, we can place it exactly where that image forms. This is called focusing.

The second concept is collimation. All the light from the mirror needs to be pointing in exactly the right direction so that every bit of light ends up at the same point. If they're even slightly misaligned, the image becomes softer and loses detail, even if the telescope is perfectly focused. This alignment of the mirror is called collimation.

Assemble the Optical System

Recording2026-07-05115052-ezgif.com-optimize.gif
Recording2026-07-05115238-ezgif.com-optimize.gif
Screenshot 2026-07-05 114950.png
Screenshot 2026-07-05 115012.png
Screenshot 2026-07-05 115142.png
Screenshot 2026-07-05 115202.png

There are two main optical assemblies.

The first is the primary mirror cell. This consists of a mirror mount, which the primary mirror will later be attached to, and a mirror base, to which the mount will be connected. This assembly helps with mirror collimation using three M8 nuts and bolts. Pass each bolt through the holes in both the base and the mount, then insert the hex nuts into the hexagonal cutouts on the mount. Make sure to place a 10 mm diameter compression spring over each of the three bolts between the two parts. These springs keep constant tension on the mirror mount, so the mirror can stay securely locked in its adjusted position after collimation. Do not mount the mirror yet. The mirror goes in last.

The second optical assembly is the camera focusing system. This consists of a spider assembly, to which I've already attached the pinion using a long M5 threaded bolt. The camera mount has a matching rack that slides smoothly through the center cutout in the spider and meshes with the pinion. Turning the long bolt should now move the camera mount smoothly back and forth.

Prepare the Pvc Tube

Screenshot 2026-07-05 114523.png
Screenshot 2026-07-05 114605.png

I lined the inside of the tube with matte black chart paper to reduce internal reflections. While matte black spray paint would work better, using a sheet of black paper is much easy and still gives good results for a budget build.

The outside was spray-painted white, partly for aesthetics and partly to keep the telescope a little cooler in the sun. As you can see, I didn't do a great job with the finish, but we'll sort that out later.

Prepare the Camera

Screenshot 2026-07-05 115457.png
Screenshot 2026-07-05 115516.png
Screenshot 2026-07-05 115604.png
Screenshot 2026-07-05 115614.png
Screenshot 2026-07-05 115746.png
Screenshot 2026-07-05 115723.png
Screenshot 2026-07-05 115736.png

For receiving the image, we are using a 5mp Raspberry Pi camera module. Like I mentioned earlier, the lens needs to be removed to expose the bare sensor.

First, we need to unplug the camera from the circuit board. Then we can carefully use some pliers to twist and unscrew the lens module. The module is attached with a bit of glue so it might take some effort initially.

After removing the lens, don't throw it away! You can place it on the smartphone camera for some amazing micro photography (see attached images!)

Install the Optical Assembly

Screenshot 2026-07-05 115905.png
Screenshot 2026-07-05 115935.png
Screenshot 2026-07-05 115855.png
Screenshot 2026-07-05 115958.png
Screenshot 2026-07-05 120006.png

Now we can assemble the spider module. I first screwed the spider to one end of the PVC tube with M3.5 self-tapping screws. I also screwed in the electronics holder plate while I was at it. Then we can use tiny M2 nuts and bolts to mount the camera module to the camera mount.

Now we use double sided foam tape and carefully stick the mirror on the mirror mount.

Install the Tripod Mount

Screenshot 2026-07-05 120049.png
Screenshot 2026-07-05 120101.png
Screenshot 2026-07-05 120113.png
Screenshot 2026-07-05 120131.png
Screenshot 2026-07-05 120156.png
Screenshot 2026-07-05 120147.png

Before screwing in the tripod mount, we need to know the center of gravity of the whole assembly. So we first screw in the mirror assembly to the other side of the PVC tube. And then place the whole thing on a fulcrum and mark the center of gravity.

Remove the mirror assembly and screw the tripod mount and the smartphone holder on the CG line. I also added a nice focus wheel to the spider.

Electronics

Screenshot 2026-07-05 120411.png
Screenshot 2026-07-05 120250.png
Screenshot 2026-07-05 120313.png

With that, we've completed assembling the telescope hardware. Next, let's move on to the electronics and software.

Instead of using a Raspberry Pi 3, we're going to use a Raspberry Pi Zero 2 W. It's much cheaper, more compact, and has built-in Wi-Fi, which you'll soon see is a really useful feature. One thing to note is that the Pi Zero uses a different camera connector, so you'll need the correct camera ribbon cable. The standard Raspberry Pi camera cable won't fit.

Let's start by installing the software. First, download Raspberry Pi Imager from the official Raspberry Pi website. Then insert a microSD card. Since storage prices have gotten pretty outrageous lately, I'm actually using a recycled flash drive with a USB adapter. But whether you use a microSD card or a USB drive, the process is exactly the same.

I've put together a detailed step-by-step PDF guide that covers the entire software setup. However, in the next step, I’ll cover the whole setup in detail. Feel free to skip it if you’re referring to the PDF guide.

Software Setup

Astra Raspberry Pi Setup Guide


1) Hardware

  1. Raspberry Pi Zero 2 W
  2. microSD card, 4 GB minimum, 8 GB or larger recommended
  3. Raspberry Pi Camera
  4. Power supply


2) Flash Raspberry Pi OS Lite


Open Raspberry Pi Imager and configure it as follows:

  1. Device: Raspberry Pi Zero 2 W
  2. Operating System: Raspberry Pi OS Lite (32-bit)
  3. Storage: Select your microSD card


Before clicking Write, open the advanced options (⚙️ or Edit Settings) and configure:

  1. Hostname: astra
  2. Enable SSH
  3. Set a username and password
  4. Configure your Wi-Fi SSID and password
  5. Set your locale, keyboard layout, and timezone


Click Write, wait for the flashing and verification process to finish, then safely eject the card.


Insert the card into the Raspberry Pi, power it on, and wait about 1–2 minutes for the first boot.


You are now ready to continue with the setup below.


3) First boot and update


SSH into the Pi from your pc:


ssh <username>@astra.local


And run:


sudo apt update
sudo apt full-upgrade -y
sudo reboot


4) Install packages


Run:


sudo apt install python3-flask python3-waitress avahi-daemon


5) Create the project folder


mkdir -p ~/astra
cd ~/astra


6) Create app.py


Create the file with your editor:


nano ~/astra/app.py


Paste the following code:


import atexit
import subprocess
import threading
import time
from datetime import datetime
from io import BytesIO

from flask import Flask, Response, redirect, render_template_string, request, send_file, url_for
from waitress import serve

app = Flask(__name__)

WIDTH = 1920
HEIGHT = 1080
FPS = 15

state_lock = threading.Lock()
frame_cond = threading.Condition()

# Default settings (Moon mode)
DEFAULTS = {
"hflip": False,
"vflip": True,
"exposure_us": 1000,
"gain": 1.0,
}

camera_settings = DEFAULTS.copy()

latest_frame = None
latest_frame_id = 0

camera_proc = None
stop_event = threading.Event()
restart_event = threading.Event()

def clamp_int(value, low, high, default):
try:
value = int(value)
except (TypeError, ValueError):
return default
return max(low, min(high, value))

def clamp_float(value, low, high, default):
try:
value = float(value)
except (TypeError, ValueError):
return default
return max(low, min(high, value))

def build_camera_cmd():
with state_lock:
hflip = camera_settings["hflip"]
vflip = camera_settings["vflip"]
exposure_us = camera_settings["exposure_us"]
gain = camera_settings["gain"]

cmd = [
"rpicam-vid",
"-n",
"-t", "0",
"--width", str(WIDTH),
"--height", str(HEIGHT),
"--framerate", str(FPS),
"--codec", "mjpeg",
"--gain", f"{gain}",
"--shutter", str(exposure_us),
"-o", "-",
]

if hflip:
cmd.append("--hflip")
if vflip:
cmd.append("--vflip")

return cmd

def camera_worker():
global latest_frame, latest_frame_id, camera_proc

while not stop_event.is_set():
cmd = build_camera_cmd()

camera_proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
bufsize=0,
)

buffer = bytearray()

try:
while not stop_event.is_set() and not restart_event.is_set():
chunk = camera_proc.stdout.read(4096)
if not chunk:
break

buffer.extend(chunk)

while True:
start = buffer.find(b"\xff\xd8")
if start == -1:
if len(buffer) > 2_000_000:
del buffer[:-2_000_000]
break

end = buffer.find(b"\xff\xd9", start + 2)
if end == -1:
if start > 0:
del buffer[:start]
break

frame = bytes(buffer[start:end + 2])
del buffer[:end + 2]

with frame_cond:
latest_frame = frame
latest_frame_id += 1
frame_cond.notify_all()

finally:
try:
if camera_proc and camera_proc.poll() is None:
camera_proc.terminate()
camera_proc.wait(timeout=2)
except Exception:
pass

restart_event.clear()
time.sleep(0.2)

def mjpeg_generator():
boundary = b"--frame\r\nContent-Type: image/jpeg\r\n\r\n"
last_seen_id = -1

while not stop_event.is_set():
with frame_cond:
frame_cond.wait_for(
lambda: latest_frame_id != last_seen_id or stop_event.is_set(),
timeout=5,
)

if stop_event.is_set():
break

if latest_frame is None:
continue

frame = latest_frame
last_seen_id = latest_frame_id

yield boundary + frame + b"\r\n"

INDEX_HTML = """
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Astra</title>
<style>
* {
box-sizing: border-box;
}

body {
margin: 0;
background: #141414;
color: #e5e5e5;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

header {
height: 56px;
display: flex;
align-items: center;
padding: 0 20px;
background: #1b1b1b;
border-bottom: 1px solid #2f2f2f;
font-size: 22px;
font-weight: 600;
letter-spacing: 1px;
}

.container {
max-width: 1100px;
margin: 24px auto;
padding: 0 20px 24px 20px;
}

.viewer {
background: #000;
border: 1px solid #333;
overflow: hidden;
border-radius: 8px;
}

.viewer img {
display: block;
width: 100%;
height: auto;
transform-origin: center center;
transition: transform 0.15s ease-out;
}

.panel {
margin-top: 18px;
padding: 16px 0 0 0;
}

.controls {
display: grid;
grid-template-columns: repeat(2, minmax(240px, 1fr));
gap: 18px 28px;
align-items: end;
}

.control-group {
min-width: 0;
}

.control-label {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 8px;
font-size: 14px;
color: #d8d8d8;
}

.control-value {
color: #a9a9a9;
font-variant-numeric: tabular-nums;
}

.slider {
width: 100%;
margin: 0;
}

.toggle-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
font-size: 15px;
color: #e5e5e5;
}

.switch {
position: relative;
display: inline-block;
width: 52px;
height: 30px;
flex: 0 0 auto;
}

.switch input {
opacity: 0;
width: 0;
height: 0;
}

.slider-ui {
position: absolute;
cursor: pointer;
inset: 0;
background: #444;
transition: 0.2s;
border-radius: 999px;
border: 1px solid #555;
}

.slider-ui:before {
position: absolute;
content: "";
height: 22px;
width: 22px;
left: 3px;
top: 3px;
background: #ddd;
transition: 0.2s;
border-radius: 50%;
}

.switch input:checked + .slider-ui {
background: #2f6feb;
border-color: #2f6feb;
}

.switch input:checked + .slider-ui:before {
transform: translateX(22px);
}

.modes-bar {
display: flex;
gap: 10px;
margin-bottom: 24px;
flex-wrap: wrap;
padding-bottom: 16px;
border-bottom: 1px solid #333;
}

.buttons {
display: flex;
gap: 12px;
align-items: center;
margin-top: 24px;
flex-wrap: wrap;
}

button, a.capture {
appearance: none;
border: 1px solid #444;
border-radius: 8px;
padding: 10px 18px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
color: white;
background: #2d2d2d;
text-decoration: none;
}

button:hover, a.capture:hover {
background: #3b3b3b;
}

.btn-apply { background: #2f6feb; border-color: #2f6feb; }
.btn-apply:hover { background: #2458c2; }

.btn-capture { border-color: #059669; background: #059669; }
.btn-capture:hover { background: #047857; }

.btn-reset { margin-left: auto; border-color: #7f1d1d; background: #450a0a; color: #fecaca; }
.btn-reset:hover { background: #7f1d1d; }

.footer {
margin-top: 18px;
color: #999;
font-size: 13px;
font-variant-numeric: tabular-nums;
}

@media (max-width: 720px) {
.controls {
grid-template-columns: 1fr;
}
.btn-reset { margin-left: 0; width: 100%; text-align: center; }
}
</style>
</head>
<body>
<header>ASTRA</header>

<div class="container">
<div class="viewer">
<img id="liveStream" src="{{ url_for('video_feed') }}" alt="Astra live feed">
</div>

<form class="panel" method="post" action="{{ url_for('settings') }}">

<div class="modes-bar">
<span style="display:flex; align-items:center; margin-right:8px; color:#aaa; font-size:14px;">Presets:</span>
<button type="submit" formaction="{{ url_for('preset', mode='moon') }}">🌕 Moon</button>
<button type="submit" formaction="{{ url_for('preset', mode='planets') }}">🪐 Planets</button>
<button type="submit" formaction="{{ url_for('preset', mode='stars') }}">🌟 Deep Sky</button>
</div>

<div class="controls">
<div class="control-group">
<div class="toggle-row">
<span>Horizontal Flip</span>
<label class="switch">
<input type="checkbox" name="hflip" {% if hflip %}checked{% endif %}>
<span class="slider-ui"></span>
</label>
</div>
</div>

<div class="control-group">
<div class="toggle-row">
<span>Vertical Flip</span>
<label class="switch">
<input type="checkbox" name="vflip" {% if vflip %}checked{% endif %}>
<span class="slider-ui"></span>
</label>
</div>
</div>

<div class="control-group">
<div class="control-label">
<span>Exposure (µs)</span>
<span class="control-value">{{ exposure_us }}</span>
</div>
<input
class="slider"
type="range"
name="exposure_us"
min="100"
max="2000000"
step="500"
value="{{ exposure_us }}"
>
</div>

<div class="control-group">
<div class="control-label">
<span>Gain</span>
<span class="control-value">{{ "%.1f"|format(gain) }}</span>
</div>
<input
class="slider"
type="range"
name="gain"
min="1.0"
max="16.0"
step="0.1"
value="{{ "%.1f"|format(gain) }}"
>
</div>

<div class="control-group">
<div class="control-label">
<span>Digital Focus Zoom</span>
<span class="control-value" id="zoomVal">0x</span>
</div>
<input class="slider" type="range" id="zoomSlider" min="0" max="3" step="0.5" value="0">
</div>
</div>

<div class="buttons">
<button type="submit" class="btn-apply">Apply Adjustments</button>
<a class="capture btn-capture" href="{{ url_for('capture') }}" target="_blank" download>⛶ Capture Frame</a>
<button type="submit" formaction="{{ url_for('preset', mode='reset') }}" class="btn-reset">Reset Defaults</button>
</div>

<div class="footer">
1920 × 1080 &nbsp;&nbsp;•&nbsp;&nbsp; 15 FPS
</div>
</form>
</div>

<script>
// Digital Zoom Frontend Handler
const zoomSlider = document.getElementById('zoomSlider');
const liveStream = document.getElementById('liveStream');
const zoomVal = document.getElementById('zoomVal');

zoomSlider.addEventListener('input', function() {
// A slider value of 0 means scale(1). A value of 3 means scale(4).
const scaleLevel = 1 + parseFloat(this.value);
liveStream.style.transform = `scale(${scaleLevel})`;
zoomVal.innerText = `${this.value}x`;
});
</script>
</body>
</html>
"""

@app.route("/")
def index():
with state_lock:
hflip = camera_settings["hflip"]
vflip = camera_settings["vflip"]
exposure_us = camera_settings["exposure_us"]
gain = camera_settings["gain"]

return render_template_string(
INDEX_HTML,
hflip=hflip,
vflip=vflip,
exposure_us=exposure_us,
gain=gain,
)

@app.route("/video_feed")
def video_feed():
return Response(
mjpeg_generator(),
mimetype="multipart/x-mixed-replace; boundary=frame",
)

@app.route("/settings", methods=["POST"])
def settings():
hflip = "hflip" in request.form
vflip = "vflip" in request.form
exposure_us = clamp_int(request.form.get("exposure_us"), 100, 2000000, 1000)
gain = clamp_float(request.form.get("gain"), 1.0, 16.0, 1.0)

with state_lock:
camera_settings["hflip"] = hflip
camera_settings["vflip"] = vflip
camera_settings["exposure_us"] = exposure_us
camera_settings["gain"] = gain

restart_event.set()
return redirect(url_for("index"))

@app.route("/preset/<mode>", methods=["POST"])
def preset(mode):
# Keep flip settings as they are, but update exposure and gain
with state_lock:
if mode == 'moon' or mode == 'reset':
camera_settings["exposure_us"] = 1000
camera_settings["gain"] = 1.0
elif mode == 'planets':
camera_settings["exposure_us"] = 3000
camera_settings["gain"] = 1.5
elif mode == 'stars':
camera_settings["exposure_us"] = 500000
camera_settings["gain"] = 10.0

restart_event.set()
return redirect(url_for("index"))

@app.route("/capture")
def capture():
with frame_cond:
frame = latest_frame

if frame is None:
return "No frame available yet. Wait a second and try again.", 503

filename = datetime.now().strftime("astra_%Y%m%d_%H%M%S.jpg")
fileobj = BytesIO(frame)
fileobj.seek(0)

return send_file(
fileobj,
mimetype="image/jpeg",
as_attachment=True,
download_name=filename,
)

def cleanup():
stop_event.set()
restart_event.set()

try:
if camera_proc and camera_proc.poll() is None:
camera_proc.terminate()
except Exception:
pass

atexit.register(cleanup)

threading.Thread(target=camera_worker, daemon=True).start()

if __name__ == "__main__":
serve(app, host="0.0.0.0", port=80)


7) Create the systemd service


Create the file:


sudo nano /etc/systemd/system/astra.service


Paste this service file, replacing YOUR_USER with your Linux username:


[Unit]
Description=Astra Telescope Server
After=network-online.target avahi-daemon.service
Wants=network-online.target

[Service]
Type=simple
User=YOUR_USER
WorkingDirectory=/home/YOUR_USER/astra
ExecStart=/usr/bin/python3 /home/YOUR_USER/astra/app.py
Restart=always
RestartSec=2
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target


Then run:


sudo systemctl daemon-reload
sudo systemctl enable astra.service
sudo systemctl restart astra.service
sudo systemctl status astra.service


8) Verify the web server


Check that Astra is listening on port 80:


sudo ss -tlnp | grep :80


Open the page from another device on the same network:

  1. http://astra.local
  2. or http://<pi-ip> if mDNS is not available


9) Create the Astra hotspot


Run these commands when you are ready to switch Astra from home Wi‑Fi to hotspot mode. Replace YOUR_STRONG_PASSWORD with your own password.


sudo nmcli connection add type wifi ifname wlan0 con-name Astra autoconnect yes ssid Astra

sudo nmcli connection modify Astra 802-11-wireless.mode ap 802-11-wireless.band bg

sudo nmcli connection modify Astra wifi-sec.key-mgmt wpa-psk wifi-sec.psk "YOUR_STRONG_PASSWORD"

sudo nmcli connection modify Astra ipv4.method shared ipv4.addresses 192.168.4.1/24

sudo nmcli connection modify Astra connection.autoconnect yes


If you want Astra to stop joining the home Wi‑Fi network automatically, disable that profile’s autoconnect setting:


sudo nmcli connection modify YOUR_HOME_WIFI connection.autoconnect no


Activate the hotspot only when you are ready to disconnect from the current network:


sudo nmcli connection up Astra


10) Use Astra


Connect a phone, tablet, or laptop to the Wi‑Fi network named Astra.

Then open one of these URLs:

  1. http://astra.local
  2. http://192.168.4.1


The page shows the live feed, flip toggles, exposure, gain, brightness, and the capture button.


Note: If both of those URLs don’t work, try http://10.42.0.1


Final Assembly

Screenshot 2026-07-05 120517.png
Screenshot 2026-07-05 120538.png
Screenshot 2026-07-05 120708.png
Screenshot 2026-07-05 120639.png
Screenshot 2026-07-05 120649.png
Screenshot 2026-07-05 120605.png

To power the telescope, I'm using a single 18650 lithium-ion battery along with a 5 V, 2 A boost converter. This makes the entire telescope completely portable, with no external power cables required. The circuit connections are straightforward. Refer to the attached image.

I mounted all the electronics to the electronics holder plate using double-sided tape. Next, slide the camera mount into the spider assembly and connect the camera ribbon cable to the Raspberry Pi.

Graphics

Screenshot 2026-07-05 120855.png
Screenshot 2026-07-05 120922.png
Screenshot 2026-07-05 120914.png

Only after everything else is installed do we reinstall the primary mirror assembly. This greatly reduces the risk of accidentally scratching or damaging it during assembly. Finally, I added a protective cover for the electronics, along with some graphics and a QR code on the side of the telescope. That QR code will come in handy very soon.

How to Use the Telescope

Screenshot 2026-07-05 121047.png
Screenshot 2026-07-05 121107.png
Screenshot 2026-07-05 121122.png
Screenshot 2026-07-05 121146.png
Screenshot 2026-07-05 121159.png
Screenshot 2026-07-05 121222.png
Screenshot 2026-07-05 121239.png
Screenshot 2026-07-05 121331.png
Screenshot 2026-07-05 121320.png

Now it's finally time to use the telescope. Attach the tripod mount to any standard photography tripod. Depending on the weight of your telescope and the stiffness of your tripod, it may be a little wobbly. So I definitely recommend using a sturdy tripod if you have one.

Power on the Raspberry Pi and wait for about a minute. You should see a new Wi-Fi network named Astra. Connect your phone to it, then open http://astra.local on a web browser. I added a QR code to the side of the telescope so I can open it instantly.

A web interface should appear with a live camera feed and a few controls. The Exposure and Gain sliders are there for more advanced users, but I've already created presets for the Moon, planets, and deep-sky objects that should work well in most situations. Whenever you want to save an image, just tap the Capture button and it'll download straight to your phone.

Now we need to point the telescope at something. Download an astronomy app like Sky Map or SkEye. Allow location access, then attach your phone to the phone mount. Search for the object you want to observe. I highly recommend starting with the Moon since it's the easiest target. The app will guide you until the telescope is pointing in the right direction.

At this point, you'll probably see either a black screen or a blurry white blob. That's completely normal. This is where the focus knob comes in. Slowly rotate it back and forth until the image becomes sharp. If you still can't get a crisp image, you may need to make some small collimation adjustments using the three bolts on the mirror cell. But if you mounted the mirror flat and close to the center, you probably won't need to adjust it much.

First Images

Screenshot 2026-07-05 114012.png
Screenshot_20260624_201655.jpeg
Shotcut_00_00_11_233.jpeg
Shotcut_00_00_23_250.jpeg
Screenshot 2026-07-05 114201.png

Unfortunately, because it's currently monsoon season here in Bangalore, I couldn't capture any planets. But one night, the Moon finally showed up clearly, so I quickly took the telescope out onto my balcony. My tripod is a little shaky, but once everything settled down, I managed to capture some nice shots of the Moon and its craters. I did have to increase the exposure and gain a little.

The images aren't the sharpest, but considering they were taken from the middle of a city, through humidity and light pollution, I'm honestly pretty happy with the results. I may have to adjust the collimation a bit though. You might also notice a slight pink tint around the edges of some images. That's because removing the Raspberry Pi camera lens also removes its built-in infrared filter, allowing the sensor to detect infrared light that our eyes normally can't see.

So, was it worth it? Absolutely. Not only is it incredibly difficult to find a commercial telescope at this price that offers similar capabilities, but building it taught me a lot about optics, astronomy, and telescope design than simply buying one ever could.

Now all that's left is to wait for the monsoon skies to clear. As soon as I manage to capture Jupiter, Saturn, or maybe even a few deep-sky objects, I'll update the Instructable with those images. Until then, I hope you have fun building your own telescope!