Two Radiosonde Recoveries in San Francisco

The prevailing winds here in California blow from west to east, from the Pacific Ocean towards the Sierra Nevada mountains. Radiosondes launched from Oakland International Airport float in these winds, landing east of Oakland in the Central Valley, or south in the hills east of San Jose. I never recover the ones that land in the Central Valley, as driving 2 hours each direction during rush hour to recover a balloon is a bit too far for me. Only rarely do they land in populated areas in the Bay Area, and almost never on the Peninsula or in the city of San Francisco.

This map (code below) shows the landing locations of all the radiosondes launched from Oakland since November 2020, when I started receiving them. Each red dot is the last position received by my two receiving stations, and is typically less than 1,000 meters altitude. This map shows 337 radiosondes, and I have removed the radiosondes launched up north by UCSD during atmospheric river events. As you can see, they are mostly south and east of the launch location (green dot).

Radiosonde landing locations

When I saw the morning radiosonde on June 1st 2021 land just off the coast of Half Moon Bay, I thought the winds might be shifting favorably. I ran some predictions from Habhub, and those showed that the afternoon balloon might land in San Bruno, which is only 7 miles south of my apartment. The hunt was on!

San Bruno landing prediction

My mobile receiving station is very simple, just a mag-mount antenna, RTL-SDR Blog v3, and laptop running Ubuntu. I use my phone's hotspot to view the Sondehub predicted landing location, and also upload decoded telemetry back to Sondehub. A navigator is useful to watch the screen and give directions.

Mobile radiosonde receiving station block diagram

I use the radiosonde_auto_rx program to track the radiosondes, running entirely in a Docker container. Telemetry is automatically uploaded to Sondehub, so other people can watch the progress of the balloon. Sondehub also does a real-time prediction, updated as telemetry comes in, which is helpful when you are out in the field tracking the balloon. Radiosondes are launched twice daily at 1100 and 2300 UTC, and take about 90 minutes to ascend to 30,000 meters (~98,000 ft). Then the balloon bursts, and it takes only 30 minutes to fall back to the ground.


This radiosonde was launched just after 2300 UTC on Tuesday June 1st 2021, which is 4pm Pacific time. It reached an altitude of 32,473 meters about 93 minutes after launch, and landed 20 minutes later.

S4140428 flight trajectory

With my mobile station, I tracked it all the way to the ground and pulled up approximately 3 minutes after it landed. The tracker was in the street gutter, with the string draped over a streetlight and over a roof, and the actual balloon was hanging on the side of the house. I carefully pulled the balloon off the roof, cut the cable, and was gone before anybody knew what happened. Easy!

S4140428 landing location


The predictions for the Wednesday morning radiosonde had it landing in the middle of the bay, halfway between Oakland and San Francisco. Not much chance for a recovery I thought, so I didn't even set my alarm. Launch was at 1100 UTC on June 2nd 2021, which is 4am. But I woke up anyways at 6am, checked Sondehub, and saw that the balloon had just landed on Bernal Hill, only 0.9 miles from my house.

S4140438 flight trajectory

I jumped on my bike and was there in 25 minutes or so. The morning was very foggy and cool. The latex balloon was in the middle of Folsom St on the north side of the park, and the tracker electronics was behind a bush about 30 ft from the road. Thanks to all the road infrastructure we have built everywhere, recovery was very easy.

S4140438 landing

After getting back to the house, I took a look at the telemetry. I could see where the balloon had hit the ground at 1301 UTC, and slowly rolled (or got dragged by the balloon) down the hill over the course of 3 minutes, until signal was lost. When I picked the tracker up off the ground 28.5 minutes later, my station at home started receiving telemetry again.

On the bike ride home, I was treated to an amazing sunrise over our beautiful city.

San Francisco cityscape


The winds had shifted northerly for the 2300 UTC June 2nd 2021 launch, and the predictions had it landing in the hills east of Berkeley. I had meetings that evening, so I wasn't really paying attention. The radiosonde actually landed on the UC Berkeley campus, in the Stern Hall dorms only 300 feet northwest of the Greek Theatre. Hopefully some student was curious and grabbed it!

S3850298 flight trajectory

The winds have grown stronger, and the radiosondes are now landing in the Delta and Central Valley. Back to our regularly scheduled programming.


The python3 code used to generate the plot at the top is based on earlier work plotting balloon locations. It basically grabs the last line (last decoded position, which is probably pretty close to the ground) of all the saved telemetry log files, and plots those on a map.

Jupyter notebook, using Jupyter 6.1.5, Python 3.8.5, Matplotlib 3.3.3, Pandas 1.1.5, and Geopandas 0.8.1.

Raw python3 code

Code, with the jupyter notebook comments:

#!/usr/bin/env python
# coding: utf-8

# In[1]:

import pandas as pd
import matplotlib.pyplot as plt
import descartes
import geopandas as gpd
import glob, os
import matplotlib as mpl
from shapely.geometry import Point, Polygon

# In[2]:

# Generate the census map diagram, and plot it
census_map = gpd.read_file('../TG00CAZCTA.shp')
census_map = census_map.to_crs('EPSG:4326')
fig,ax = plt.subplots(figsize = (15,15))
ax.set_ylim([37, 38.25])
ax.set_xlim([-122.75, -121.5])
census_map.plot(ax = ax, linewidth=1, edgecolor='black')

# In[3]:

# Make a list of all the log filenames in the folder.
all_files = glob.glob("*.log")

li = []

# Iterate thru each file and grab the last line. Append each last line to li list.
for filename in all_files:
    df = pd.read_csv(filename)
    df2 = df.tail(1)

# Concat the list into a single dataframe.
dataframe = pd.concat(li, axis=0, ignore_index=True)


# In[4]:

# Create geometry points based on the lat/long from each row in the dataframe
geometry = [Point(xy) for xy in zip( dataframe["lon"], dataframe["lat"]) ]

# In[5]:

# Make a GeoPandas dataframe from the dataframe and the geometry points
geo_frame = gpd.GeoDataFrame(dataframe,            #specify data
                             crs = 'EPSG:4326',    #specify coordinate ref system
                             geometry = geometry)  #specify geometry list created

# In[6]:

# Plot the census map, then plot the GeoPandas dataframe on top.
fig,ax = plt.subplots(figsize = (15,15))
ax.set_ylim([37.0, 38.0])    # This doesn't do anything because aspect ratio is 1, set by longitude
ax.set_xlim([-123.5, -120.0])
ax.set_ylabel('WGS84 Latitude', fontsize=14)
ax.set_xlabel('WGS84 Longitude', fontsize=14)
ax.set_title('Oakland Airport Radiosonde Landing Locations', fontsize=18)

census_map.plot(ax = ax, alpha = 0.4, color="grey", linewidth=1, edgecolor='black')

geo_frame.plot(ax = ax, markersize = 6, color = "red", marker = "o")

# Save the plot locally
plt.savefig('last-position.jpg', bbox_inches='tight')

# In[ ]: