Berlin STVID setup: Pi 5 8GB, Pi HQ camera, ~35mm lens

After messing around with an IP camera with OpenIPC for a bit, I decided to upgrade to a Pi HQ camera.

For now, this is running @cgbsat’s stvid software. But maybe soon it will run SatNOGS Optical instead? I’m holding out for a standalone/dockerized installation for now.

Hardware

We still had a Pi 5 8GB and a 6–60mm varifocal f/1.6 lens, so that’s what I used.

For the housing, I went with a 110mm drainage pipe (this one, except they only had the HT version, and these covers) and drilled holes for cable glands + the view opening (which I closed with some plexiglass + epoxy). I then 3D printed some parts for mounting inside the pipe, as well as a clamshell clamp with a 1/4’’ thread insert so I can mount it on a camera mount.

I also added a small servo + shutter, a 12V → 5V DC/DC for the power supply, a 12V fan, and a little AHT20 breakout board for temperature/humidity measurements.

This is what the hardware stack looks like (notice how you can’t see the fan at all? I’m sure that’s great for airflow):

And this is what it looks like fully assembled:

Like I said, I wouldn’t recommend using this housing. But if you do want to for some reason, here’s the CAD.

Software

I went with Ubuntu 26.04 Server. I didn’t take notes during setup, so the following is reconstructed from my bash history and might be missing some steps.

System dependencies

I don’t think all of these are necessary for stvid, some were required for other experiments. But it shouldn’t hurt to install them either way.

sudo apt install rpicam-apps-core \
  git make g++ libeigen3-dev source-extractor astrometry.net \
  python-is-python3 python3-pip python3.14-venv python3-libcamera python3-picamera2 \
  libcap-dev libpython3-dev libopenh264-dev libgl1
sudo cp /usr/bin/source-extractor /usr/local/bin/sextractor

Local dependencies

mkdir -p ~/software && cd ~/software
git clone https://gitlab.com/pierros/hough3d-code.git
git clone https://github.com/cbassa/satpredict.git
cd ~/software/hough3d-code
make -j4
sudo cp hough3dlines /usr/local/bin
cd ~/software/satpredict
make -j4
sudo make install

Installing stvid

@pe2bz noted in his stvid setup post that the original cbassa/stvid does not work on a Pi 5. @EelkeVisser created a branch with picamera2 support. It’s a bit older, so I merged cbassa/main into EelkeVisser/picamera2. It turned out that stvid was selecting the 10-bit mode on the HQ camera (maybe because I was using a different OS?), so I added one more commit to select 12-bit mode. The result is now in jazzpi/picamera2, so we can install it like this:

cd ~/software
git clone https://github.com/jazzpi/stvid -b picamera2
cd ~/software/stvid
python -m venv --system-site-packages .venv
source .venv/bin/activate
pip install -r requirements.txt

NOTE: We install the Python requirements into a virtual environment. That means, before running any of the stvid scripts, we always have to activate the virtual environment (cd ~/software/stvid && source .venv/bin/activate). It’s also very important to create the virtual environment with --system-site-packages. Otherwise, you have to build python3-libcamera and python3-picamera2 locally, and that is not fun.

System configuration

There’s a couple more steps before acquire.py can run:

Access rights

We need to give our user access rights to the camera + DMA:

sudo usermod -aG video $USER

CMA pool size

I ran into Cannot allocate memory errors because the CMA pool was too small. So we need to increase the pool size.

First, I added cma=512M to /boot/firmware/cmdline.txt and rebooted, but this didn’t seem to have any effect. So I also edited /boot/firmware/config.txt and edited the dtoverlay=vc4-kms-v3d line (in the [all] section) to say

dtoverlay=vc4-kms-v3d,cma-512

and rebooted. Afterwards the pool size was finally increased:

$ grep -i cma /proc/meminfo
CmaTotal:         524288 kB
CmaFree:          137288 kB

So maybe you can skip the cmdline.txt edit but it doesn’t seem to hurt.

NAS mount

I have a 64GB SD card, so that could probably store quite a few observations. But I don’t want to wear it out. Since the Pi is connected to Gigabit ethernet, I decided to store the observations on a SMB share. That has the added benefit that I can easily process them from a (more powerful) VM rather than having to run everything on the Pi.

So, we need to edit /etc/fstab to add the SMB mount:

//10.1.2.3/visual-sat-tracking  /mnt  cifs  defaults,uid=1000,gid=1000  0  0

Afterwards, run sudo mount -a && sudo systemctl daemon-reload to make the system pick up the new mount.

stvid configuration

I put the configuration on the NAS so I can share it between the acquire.py Pi and the process.py VM. Without the NAS you can skip this of course.

cd ~/software/stvid
mkdir -p /mnt/stvid
cp configuration.ini-dist /mnt/stvid/configuration.ini
ln -s /mnt/stvid/configuration.ini

Then, edit it in your favorite editor and modify the following values:

  • [Observer] → everything. For COSPAR, if you don’t have an ID, it’s recommended to pick a number between 9900 and 9999.
  • [Setup]
    • camera_type = PI
    • observations_path = /mnt/stvid/obs (or whereever you want to store them)
  • [Credentials] → your space-track.org username/password, used for the update_tle.py script
  • [PI]
    • exposure should match framerate so you get as much light as possible
    • nframes / framerate gives you the length of time for each .fits stack. It’s recommended to set this so the fastest possible satellite will not enter & exit the FOV within one stack. I just went with 10s stacks for starters.
    • analog_gain goes up to 22.26, afterwards the camera starts compensating with digital gain which won’t help. I believe digital_gain is not used at all.
    • I have the following configuration (which I’m still testing, there might be better configurations):
[PI]
device_id = 0           # Device ID
nx =  800               # Camera horizontal pixel count
ny =  600               # Camera vertical pixel count
nframes = 100           # Number of frames for each image
framerate = 10		# Take 10 frames per second
exposure = 100000	# Exposure time in us
awb_gain_red = 2	# Gain for red.
awb_gain_blue = 2.3	# Gain for blue
analog_gain = 23	# Analog gain
digital_gain = 1	# Digital gain
  • [Elements] → tlepath: I set it to /home/$USER/tle
  • I commented out [Shutter] and [Aimpoint] because I wasn’t sure how they would affect my setup

Observing

Turning the rings

Before closing up the housing (in my case, before pushing the camera into the housing at all), we need to configure the lens. Aperture ring goes wide open, of course.

For the zoom ring, the #satnogs-optical consensus seemed to be that zooming all the way in (60mm) would lead to a too narrow FOV. So I zoomed out a little bit. Turns out I went to ~35mm (we will see this in the results).

Now the focus ring: for this, I ran

rpicam-vid -t 0 --codec mjpeg --listen -o tcp://0.0.0.0:8888 --framerate 25

on the Pi, and ffplay tcp://10.3.2.1:8888 on a laptop that I had with me. Then I pointed the camera at some distant buildings and turned the focus ring until it looked decently focused. Maybe it would be better to do this at night and focus on the stars directly, but I didn’t want to stay on the roof until nighttime.

Unfortunately, the video stream had quite a bit of delay (I think 5–10s) which made adjusting the focus very painful. Ben’s post has instructions for getting VNC running, that might provide a video stream with less delay…

Pointing the camera

After closing up the housing, the question is: where should the camera look? As per Ben’s recommendation, I tried to aim for AZ west and EL 50°. IIUC this should be a good pointing because there’s lots of satellites (Starlinks) in an ~53° inclination, which should pass roughly west-to-east over Berlin (which is at ~52.5°N). We will see how well I pointed it in the results.

Starting the observation

Now it is just a matter of starting the observation. For a quick test (even at daytime) you can run

cd ~/software/stvid
source .venv/bin/activate
./acquire.py -t 120

which will immediately start acquiring for 120s. So with the 10s stacks, you should see 12 .fits files coming out of this. You can inspect the .fits with AstroImageJ for example (on Wayland, I had to run export _JAVA_AWT_WM_NONREPARENTING=1 before starting it to make it behave correctly).

Once you’re happy with your configuration, you can just run

cd ~/software/stvid
source .venv/bin/activate
./acquire.py

and it will wait until dusk before starting the acquisition, then stop acquisition and exit at dawn. So you have to restart it every day. Probably I will set up a cronjob/timer for this, but for now I’m just running it manually.

Results

I’ve had it running for two (partial) nights now. Unfortunately it’s been pretty cloudy (and the middle of a big city isn’t ideal wrt. light pollution), but still I caught a bunch of Starlinks and some other satellites :slight_smile:

Keograms


(took me until ~01:30 local to set up stvid…)



(~21:15 local I discovered the camera was in 10-bit mode, after fixing it I restarted acquire.py so there’s two keograms for the second night)

Numbers

  • Night of 09-30 to 10-01:
    • 14 ID’d tracks
    • 9 ID’d satellites
      • 3x rocket body
      • 2x spy satellite
      • 2x communications satellite (no Starlinks!)
      • 2x science satellite
    • 2 unID’d tracks (after cleaning up junk)
    • all tracks (ID’d and unID’d) between 02:42 and 03:15 UTC
  • Night of 10-01 to 10-02:
    • 54 ID’d tracks
    • 28 ID’d satellites
      • 4x rocket body
      • 1x geodetic satellite
      • 16x communications satellite (15x Starlink)
      • 3x science satellite
      • 1x meteorology satellite
      • 2x SAR satellite
    • 17 unID’d tracks (after cleaning up junk)
    • all tracks (ID’d and unID’d) between 18:02 and 21:07 UTC
  • Night of 10-02 to 10-03:
    • 12 ID’d tracks
    • 8 ID’d satellites
      • 1x rocket body
      • 6x communications satellite (5x Starlink, 1x BlueBird)
      • 1x SAR satellite
    • 3 unID’d tracks (after cleaning up junk)
      • all of them seem to be Starlinks
    • all tracks (ID’d and unID’d) between 18:33 and 20:11 UTC

The first night I had stvid configured for 5fps/200ms exposures at 100 nframes, so each stack was 20s long.

The first night I only started acquisition long after dusk, and the second night I think there were too many clouds during dawn. So that probably explains the different timeframes of the detected tracks.

Highlights

19460 USA 32, a late cold-war era SIGINT satellite which recently made the news because it experienced a fragmentation event (of course I don’t think there’s any fragments visible with this setup):

Two Starlinks at once:


(for some reason stvid did not ID the left track, but it’s 52464 Starlink-3904 and 58041 Starlink-30542).

unID trailing just behind 69587 OBJECT F:


and if you check the GP history:

69587 is in the process of raising its orbit. Since that means a longer period, you would expect it to trail behind the predicted position :slight_smile:

Of course that doesn’t mean the track is actually of 69587, could just be a coincidence. Next step is figuring out how to do that analysis… Maybe I can generate a TLE from the IOD and see if it lines up with the next Space-Track TLE?

Pointing analysis

stvid helpfully puts RA/Dec and FOV in each image (from its astrometry solve). Let’s take the 69587 one as an example.

If we plug the RA, Dec, time, and Berlin’s coordinates (52.52°N/13.41°E/50m) into this calculator, we get Az 280.0°, Alt 41.8°. So my eyeballed pointing was off by ~10° in both azimuth and elevation.

From the FOV + sensor size, we can also calculate the focal length of the lens:

tan(a/2) = (H/2)/f
f = H/(2 tan(a/2))

where a = FOV, H = sensor size, and f = focal length.

stvid reports a FOV of 9.94° x 7.46°, so a diagonal FOV of

sqrt(9.94² + 7.46²) = 12.43°

The Pi HQ camera uses an IMX477 sensor, which has a diagonal of 7.857mm. So we get

f = 7.857mm/(2 tan(12.43°/2))
f = 36.07mm

6 Likes

maybe you can share more dot / iod of the object. ussualy inside file with name : *_noradid_catalog_m.dat

IOD / TLE input:

58041 23 156P   9914 G 20261001184006000 17 25 1537840+355823 37 S

0 STARLINK-30542
1 58041U 23156P   26275.86803669  .00001066  00000-0  42697-4 0  9998
2 58041  53.1589 193.5450 0001381  87.0846 273.0315 15.34394626168069

Sure:
2026-10-01T20-28-19.194_90000_unid_m.dat (268 Bytes)

And also the two Starlinks for good measure:
2026-10-01T18-40-00.043_58041_catalog_m.dat (536 Bytes)
2026-10-01T18-40-00.043_90000_unid_m.dat (469 Bytes)

Tonight was finally relatively clear:

Unfortunately I forgot to start acquire.py in time, but still stvid caught some satellites at dusk. I’ve now scheduled the servo control and stvid scripts through at for the next week. But something automated would be nice…

Now in the keogram we can see that dawn was pretty clear. But after removing junk unids, there were no tracks at all after 20:04. I saw in the process.py log that all the lines at dawn were red, i.e. no calibration matched. Plenty of .fits had 30+ stars so I suspected the camera moved.

I vibecoded a script for performing a plate solve for a given .fits file. And indeed:

Pointing at 2026-10-03T20:45:06.880 UTC:
  RA/Dec:     269.8717  +37.4088 deg
  Az/Alt:     279.6215  +42.1006 deg
  Rotation:   -126.819 deg (+y axis, E of N)
Pointing at 2026-10-04T03:54:11.757 UTC:
  RA/Dec:      17.4109  +37.3344 deg
  Az/Alt:     279.5991  +42.2237 deg
  Rotation:   -127.032 deg (+y axis, E of N)

Az/Alt moved by ~0.1-0.2°, which is apparently enough to throw off the stvid calibration step. I made process.py re-plate-solve if the calibration failed and reran process.py:

./process.py -d /mnt/stvid/obs/20261003_0/184847/ -m 600000 -C 8 -r

-r makes it delete all the intermediate results and re-process everything. -m 600000 stops it from quitting before processing all the files because no new files appear.

And indeed, there are many more tracks detected at dawn! I vibecoded another script to automate the satellite counting I did for the previous post to compare the two runs. Before the re-solve:

Time range:        2026-10-03 18:49:18 .. 2026-10-03 20:04:52 (UTC)
Observing clusters (1):
  2026-10-03 18:49:18 .. 2026-10-03 20:04:52  (1:15:34, 11 frames)
Frames:            11
Passes:            7 (6 identified, 1 unidentified)
Unique ID'd sats:  6 (0 classified)

After the re-solve:

Time range:        2026-10-03 18:49:18 .. 2026-10-04 04:12:42 (UTC)
Observing clusters (2):
  2026-10-03 18:49:18 .. 2026-10-03 20:04:52  (1:15:34, 11 frames)
  2026-10-04 02:43:09 .. 2026-10-04 04:12:42  (1:29:33, 42 frames)
Frames:            53
Passes:            27 (24 identified, 3 unidentified)
Unique ID'd sats:  24 (1 classified)

and I am rewarded with my first classified satellite!


Here is a much more detailed view of this satellite: AstroBin

2 Likes

The scheduled servo and acquire, update_tle worked. process didn’t for some reason so I had to start that manually, but there the timing is not really important fortunately.

Tonight was unexpectedly even clearer:

There were many frames with 50–100 stars, when previously I rarely saw more than 30. Calibration also worked pretty much all the time until 04:20 UTC. And that meant by far the best results yet:

Time range:        2026-10-04 17:29:37 .. 2026-10-05 04:24:26 (UTC)
Observing clusters (3):
  2026-10-04 17:29:37 .. 2026-10-04 21:06:36  (3:36:58, 166 frames)
  2026-10-05 00:13:27 .. 2026-10-05 00:13:27  (0:00:00, 1 frames)
  2026-10-05 02:37:02 .. 2026-10-05 04:24:26  (1:47:23, 137 frames)
Frames:            304
Passes:            172 (159 identified, 13 unidentified)
Unique ID'd sats:  158 (1 classified)

Almost 6x as many satellites as the previous highscore! And you can clearly see the two clusters at dusk and dawn.

Some highlights:


Yaogan-40 02 A/B/C in formation flight. Gunter says “Yaogan 40 is a series of Chinese military satellites of unknown purpose (possibly SIGINT).”

STARLINK-2519 showing up in two separate passes over Berlin.

DMSP 5D-2 F11 (USA 73) tumbling/flaring. This is an old military meteorological satellite and apparently its tumbling is distinctive enough that you can find light curves of it in research papers.

1 Like

Clear again:

Time range:        2026-10-05 17:31:42 .. 2026-10-06 04:22:48 (UTC)
Observing clusters (3):
  2026-10-05 17:31:42 .. 2026-10-05 21:10:52  (3:39:10, 304 frames)
  2026-10-05 22:22:04 .. 2026-10-05 22:22:14  (0:00:10, 2 frames)
  2026-10-06 03:01:20 .. 2026-10-06 04:22:48  (1:21:28, 193 frames)
Frames:            499
Passes:            215 (191 identified, 24 unidentified)
Unique ID'd sats:  191 (7 classified)

>200 satellites, including 7 classifieds!

Almost all of the unID’d tracks seem to be Starlinks that are ahead/behind their predictions. Especially in the dawn cluster there’s many of these, and often their TLE was >1 day old at that point. I think it’s time to set up Celestrak’s supplemental Starlink elements.

Some highlights:

Two NOSS pairs:

00703 Thor-AbleStar from Dec 1963:

57289 coming out of penumbra:

1 Like

Clear in the evening, not so clear in the morning:

But somehow, still a new highscore:

Time range:        2026-10-06 17:23:10 .. 2026-10-07 04:32:02 (UTC)
Observing clusters (3):
  2026-10-06 17:23:10 .. 2026-10-06 20:35:35  (3:12:24, 268 frames)
  2026-10-06 22:52:36 .. 2026-10-06 22:52:36  (0:00:00, 1 frames)
  2026-10-07 03:08:32 .. 2026-10-07 04:32:02  (1:23:30, 147 frames)
Frames:            416
Passes:            247 (232 identified, 15 unidentified)
Unique ID'd sats:  232 (3 classified)

For some reason tonight it picked up lots of junk tracks. In several .fits it detected 40+ junk tracks. So maybe it was just more sensitive in general tonight? But the only config change I performed was adding Starlink + OneWeb supplemental catalogs.

I find these results promising, compared to what I got.

Awaiting another Pi to check the HQ camera without the filter.

I’m also pleasantly surprised, especially considering this is with Bortle 8–9 skies, I still have the filter installed, and the optical path isn’t exactly high-end. I think the 35mm vs 50mm lens makes a big difference in how many satellites get picked up. But I doubt I would be able to pick up a LEMUR-2.

However I’m also pointing west and the large majority of what I pick up are Starlinks. For example yesterday it was 142/191 ID’d satellites. Today it was 201/232. I wonder what results I would get with a more northerly pointing.

1 Like