Automatic Weather Satellite
Image Downlink Program

By: Tianhao Zhang (tz373), Zeyu Zhang (zz648)
Publish Date: Dec. 14, 2019


Project Promo Video


Introduction

In the beginning of the autumn semester, we came across some videos from Youtube introducing the SDR (Software Defined Radio). We found it very interesting because it has a very broad range of frequency detections and leaves all the filtering & processing stuff to the software. Because of that, it can be applied to receive signals from FM radios, HAM communications, and even satellite signals. Thererfore, we wanted to do some experiments with it. In specific, we were trying to develop and test a design for incorporating this SDR dongle into the Raspberry Pi ecosystem and developing a nice yet simple graphical interface for receiving the live weather image from the NOAA satellites. This system is capable of displaying the time remaining for the next satellite overhead event and recording the signals autonomously for decoding. The final outcome of this program is to process/decode the weather images and display them on the touchscreen as they become ready. The tools we used are Raspberry Pi, piTFT touchscreen, SDR USB dongle and satellite signal receiving (QFH) antenna. The figure below shows the components of our design.


Project Objective:

  • Build a Quadrifilar Helix Antenna for Weather Satellite Image Receptions
  • Develop Shell and Python scripts for recording, decoding, and displaying the acquired images automatically
  • Research possibilities on receiving signals from other weather satellites

QFH Antenna

In the beginning, we did some research and considered three different kinds of antenna for receiving signals from NOAA satellites: V-dipole Antenna, Double Cross Antenna and QFH (Quadrifiliar Helix) Antenna.

V-dipole antenna is the most trivial antenna during the construction. It is based on a very simple linearly polarized dipole. To receive signals from NOAA, we need to set two dipoles with leg length 53.4cm and spread apart 120 degrees. We also have to move the antenna direction based on the location of satellites.

Double cross antenna contains four dipoles. Each dipole should have length of 38.25 inches and 21.5 inches spacing. Each of the four dipole supports is tilted 30 degrees from vertical. This antenna style has a safety concern: every dipole is sharp-pointed to outside.

• Unlike previous two styles, Quadrifilar Helix (QFH) antenna contains a single wire that wound in the form of a helix. This antenna is omni-directional and we don’t need to move it based on the location of satellites. Based on the calculation, we need to design the antenna with the following dimension (Picture below on the left)

We finally settled down with building the QFH Antenna because of the its easy-to-build characteristics and that beautiful helix shape. On Nov. 18, 2019, I sent the custom-made STL files to the Rapid Prototype Lab for 3D printing. This included the parts for the top, middle, and bottom mounting brackets. The next day, we picked up those printed parts and headed to the Home Depot for buying suitable pipes and coax cable. On Thursday, we did some initial build by cutting the PVC pipes and then went to Home Depot again for purchasing additional PVC pipe. On Nov. 23, 2019, we met at the ECE Maker Club 3pm in the afternoon and worked on building the antenna. We worked six and a half hours non-stop. In the process, we cut the additional PVC pipe, sticked the 3D printed parts to PVC pipes, measured dimensions, soldered wires, and put it all together. Later that evening, we did an initial test since there's a satellite pass-by at 10:30pm. The antenna worked properly. The picture above on the right is our QFH antenna.

 


Software Program

In this section, I will discuss the software we implemented for this project. First of all, the driver for the RTL dongle has been installed to raspberry pi. We also installed the sox audio toolkit for post processing the audio stream from satellite. To know when the satellites pass overhead, we installed predict software. In addition, we installed the wxtoimg for audio decoding and weather map generation. After all the software installation process, we implemented scripts to schedule the satellite signal receiving. The first script we implemented is schedule_all.sh, it update satellite information from celestrak and creates a TLE file for prediction use. We also implemented schedule_satellite.sh to calculate each pass event and store the information into atqlog.sat and goodluck.sat, two files that contains the information of each pass event. Then, we impelemented the receive_and_process_satellite.sh, which received the signal from satellites and process it into a wav file. This is the low level software of our Raspberry Pi. As for the interface, we developed the autosat.py with pygame to show the satellites pass events and images received. We also used autosat.py to call receive_and_process_satellite.sh when satellite passing our head. We used autorun.sh to schedule the satellite and start the interface.


Testing

Unlike traditional projects, our project can only be tested outside when the satellites passing overhead. When the satellites passing overhead, we connected our QFH antenna to Raspberry Pi and Raspberry Pi will start recording when the elevation of the satellites and us is greater than 20 degrees. However, during the test, the recording process cannot start automatically though we scheduled the recording using 'at' commands. There are two possible factors might cause this problem. The first is the connection between RTL dongle and Rpi is not reliable. The second factor is that our python file uses most of the CPU so the power is not enough to start recording. So, we removed the 'at' command and used subprocess.Popen to run the recording commands.


Results, Conclusion & Future Work

Our system worked as planned after the modification from 'at' command to python subprocess. Our system achieved to predict the satellite pass time, record the signal and decode the signal to weather image, and all of the above processes is shown on the PiTFT screen. Here is the sample image we received on 12/04/2019 with our system. In the future work, receiving and decoding Russian Meteor M2 satellite can be added to the system. Meteor M2 has a different way to decode and the image should be clearer than NOAA satellites. Also, this system can also add a feature to listen FM radios. We have successfully received FM radios such as FM 91.7MHz during the testing stage. The signal is strong and clear. In addition, we can also implement a interface to show the sound wave during recording.

Work Distribution

Project Group Picture

Tianhao Zhang

tz373@cornell.edu

Assembled QFH Antenna with Zeyu and Developed Software
Produced Promo Video for the project

Generic placeholder image

Zeyu Zhang

zz638@cornell.edu

Assembled QFH Antenna with Tianhao and Developed Software
Tested the overall system


Parts List

Total: $58.42


References

R-Pi GPIO Document
R-Pi NOAA Weather Satellite Receiver
QFH Antenna Calculator
QFH Antenna 3D Printed Parts
QFH Antenna Build
WXTOIMG Signal Decode
Pygame Tutorial

Code Appendix


// autorun.sh
# Team Member: Tianhao Zhang (tz373), Zeyu Zhang (zz648)
# Final Project, 12/05/2019
#!/bin/bash

date >> /home/pi/systemlog.sat
/home/pi/weather/predict/schedule_all.sh >> /home/pi/systemlog.sat 2>&1
sudo python /home/pi/weather/predict/autosat.py >> /home/pi/systemlog.sat
echo "" >> /home/pi/systemlog.sat
// receive_and_process_satellite.sh
# Team Member: Tianhao Zhang (tz373), Zeyu Zhang (zz648)
# Final Project, 12/05/2019
#!/bin/sh

# $1 = Satellite Name
# $2 = Frequency
# $3 = FileName base
# $4 = TLE File
# $5 = EPOC start time
# $6 = Time to capture

echo ${1},${6} > /home/pi/weather/predict/recording.sat
sudo timeout ${6} rtl_fm -f ${2}M -s 170k -g 45 -p 55 -E wav -E deemp -F 9 - | sox -t wav - $3.wav rate 11025

rm /home/pi/weather/predict/recording.sat
echo ${1} > /home/pi/weather/predict/processing.sat

PassStart=`expr $5 + 90`

if [ -e $3.wav ]
  then
    /usr/local/bin/wxmap -T "${1}" -H $4 -p 0 -l 0 -o $PassStart ${3}-map.png

    /usr/local/bin/wxtoimg -m ${3}-map.png -e MSA $3.wav $3.png
fi

rm /home/pi/weather/predict/processing.sat
// autosat.py
# Team Member: Tianhao Zhang (tz373), Zeyu Zhang (zz648)
# Final Project, 12/05/2019

import RPi.GPIO as GPIO
import time
import math
import pygame
from pygame.locals import *
import os
import sys
import subprocess
import pandas as pd
import datetime as dt
from time import gmtime, strftime,localtime
import subprocess

os.putenv('SDL_VIDEODRIVER','fbcon')
os.putenv('SDL_FBDEV','/dev/fb1')
os.putenv('SDL_MOUSEDRV','TSLIB')
os.putenv('SDL_MOUSEDEV','/dev/input/touchscreen')

GPIO.setmode(GPIO.BCM)
GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP) 
GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP) 
GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)

ball1 = pygame.image.load("/home/pi/weather/predict/cornellnew.png")
#ball1rect = ball1.get_rect()


def recording_status():
    return os.path.exists('/home/pi/weather/predict/recording.sat')
    
    
     
def processing_status():
    return os.path.exists('/home/pi/weather/predict/processing.sat')
    
def png_exist():
    global png_name
    return os.path.exists(png_name)

def count_down():
    global status
    global stime
    global sat_name
    global viewResult
    global df_sort
    global png_name
    skip = False
    
    start_time = dt.datetime.strptime(df_sort.iloc[0,1],"%Y%m%d-%H%M%S")
    stime = str(start_time)
    FMT = '%Y%m%d-%H%M%S'
    tdelta = start_time-dt.datetime.now()
    hours = tdelta.seconds//3600
    minutes = (tdelta.seconds//60)%60
    seconds = tdelta.seconds-hours*3600 - minutes*60
    sat_name = df_sort.iloc[0,0]
    clock = pygame.time.Clock()
    if tdelta.days < 0:
	png_name = df_sort.iloc[0,0:2].values.tolist()
	png_name = ''.join(png_name)
	png_name = png_name.replace(" ","")
	png_name = png_name + ".png"
	png_name = "/home/pi/weather/"+png_name
        print(png_name)
	df_sort = df_sort.iloc[1:]
	df_sort = df_sort.reset_index(drop=True)
	skip = True
	
    counter, text = seconds, str(seconds).rjust(3)
    counterM, textM = minutes, str(minutes).rjust(3)
    counterH, textH = hours, str(hours).rjust(3)
    pygame.time.set_timer(pygame.USEREVENT, 1000)
    font = pygame.font.SysFont('Consolas', 30)    
    smallfont = pygame.font.SysFont('Consolas', 15)


    while codeRun and not aboutpage and not details and not recording_status() and not processing_status() and not viewResult and not skip:
	for e in pygame.event.get():
	    if e.type == pygame.USEREVENT: 
		counter -= 1
		
		if counter < 0 and counterM == 0 and counterH == 0:
		    
		    with open('/home/pi/weather/predict/goodluck.sat','r') as f:
			goodcommands = f.read().splitlines()
		    pd.set_option('display.max_colwidth',10000)
		    good_df = pd.DataFrame(goodcommands)
		    print (good_df)
		    run_goodcommands = good_df.iloc[:,0].str.contains(df_sort.iloc[0,1])
		    my_cmd = good_df[run_goodcommands].values
		    print (my_cmd)
		    subprocess.Popen(str(my_cmd[0,0]), shell=True)
		    
		    
		    
		    
		    skip = True
		if counter >= 0:
		    text = str(counter).rjust(3) 
		elif counterM == 0:
		    counter = 59
		    counterM = 59
		    counterH -= 1
		    text = str(counter).rjust(3)
		    textM = str(counterM).rjust(3)
		    textH = str(counterH).rjust(3)
		else:
		    counter = 59
		    counterM -=1
		    text = str(counter).rjust(3)
		    textM = str(counterM).rjust(3)
	    if e.type == pygame.QUIT: break
	else:
	    screen.fill((255, 255, 255))
	    screen.blit(ball1, (10,5))
	    screen.blit(font.render("Next Satellite Overhead Event", True, (0, 0, 0)), (20, 50))
	    screen.blit(font.render(textH, True, (0, 0, 0)), (100, 90))
	    screen.blit(font.render("Hours", True, (0, 0, 0)), (130, 90))
	    screen.blit(font.render(textM, True, (0, 0, 0)), (100, 130))
	    screen.blit(font.render("Minutes", True, (0, 0, 0)), (130, 130))
	    screen.blit(font.render(text, True, (0, 0, 0)), (100, 170))
	    screen.blit(font.render("Seconds", True, (0, 0, 0)), (130, 170))
	    screen.blit(smallfont.render("Details", True, (0, 0, 0)), (280, 35))
	    screen.blit(smallfont.render("About", True, (0, 0, 0)), (285, 170))
	    screen.blit(smallfont.render("Exit", True, (0, 0, 0)), (295, 220))
	    if png_exist():
		screen.blit(smallfont.render("Image", True, (0, 0, 0)), (285, 100))
	    else:
		viewResult = False
	    pygame.display.flip()
	    clock.tick(60)
	    continue
	break

def detailscreen():
    global aboutpage
    global viewResult
    while details and not recording_status() and not processing_status() and not viewResult:
	screen.fill((255, 255, 255))
	screen.blit(font.render(status, True, (0, 0, 0)), (40, 70))
	screen.blit(font.render(sat_name, True, (0, 0, 0)), (115, 110))
	screen.blit(font.render(stime, True, (0, 0, 0)), (60, 150))
	screen.blit(smallfont.render("Back", True, (0, 0, 0)), (290, 35))
	pygame.display.flip()
	codeRun = True
	aboutpage = False
	viewResult = False
	
	
def aboutscreen():
    global details
    global viewResult
    while aboutpage and not recording_status() and not processing_status() and not viewResult:
	screen.fill((255, 255, 255))
	screen.blit(font.render("Developed By", True, (0, 0, 0)), (105, 70))
	screen.blit(font.render("Tianhao Zhang", True, (0, 0, 0)), (100, 110))
	screen.blit(font.render("Zeyu Zhang", True, (0, 0, 0)), (115, 150))
	screen.blit(smallfont.render("Back", True, (0, 0, 0)), (290, 170))
	pygame.display.flip()
	codeRun = True
	details = False
	viewResult = False






def recordingscreen():
    global codeRun
    global details
    global aboutpage
    global viewResult

    #sat_name = df_sort.iloc[0,0]
    #recording_time =df_sort.iloc[0,2]
    #recording_time = int(recording_time)
    recording_time = 0
    if recording_status():
	satfile = open('/home/pi/weather/predict/recording.sat','r')
	sat_name_time = satfile.read().split(',')
	recording_time = int(sat_name_time[1])
	minutes = (recording_time //60)%60
	seconds = recording_time - minutes*60
	clock = pygame.time.Clock()

	counter, text = seconds, str(seconds).rjust(3)
	counterM, textM = minutes, str(minutes).rjust(3)
	pygame.time.set_timer(pygame.USEREVENT, 1000)
	font = pygame.font.SysFont('Consolas', 30)    
	smallfont = pygame.font.SysFont('Consolas', 15)

    while recording_status() and not processing_status():
	for e in pygame.event.get():
	    if e.type == pygame.USEREVENT: 
		counter -= 1
		if counter >= 0:
		    text = str(counter).rjust(3) 
		elif counter < 0 and counterM <=0:
		    counter = 0
		    counterM = 0 
		    text = str(counter).rjust(3)
		    textM = str(counterM).rjust(3)
		elif counterM == 0:
		    counter = 59
		    counterM = 59
		    text = str(counter).rjust(3)
		    textM = str(counterM).rjust(3)
		else:
		    counter = 59
		    counterM -=1
		    text = str(counter).rjust(3)
		    textM = str(counterM).rjust(3)
	    if e.type == pygame.QUIT: break
	else:
	    screen.fill((255, 255, 255))
	    screen.blit(ball1, (10,5))
	    screen.blit(font.render("Recording...", True, (255, 0, 0)), (105, 50))
	    screen.blit(font.render(sat_name_time[0], True, (255, 0, 0)), (115, 90))
	    screen.blit(font.render(textM, True, (255, 0, 0)), (100, 130))
	    screen.blit(font.render("Minutes", True, (255, 0, 0)), (130, 130))
	    screen.blit(font.render(text, True, (255, 0, 0)), (100, 170))
	    screen.blit(font.render("Seconds", True, (255, 0, 0)), (130, 170))
	    
	    pygame.display.flip()
	    clock.tick(60)
	    continue
	break
    codeRun = True
    details = False
    aboutpage = False
    viewResult = False



def processingscreen():
    global details
    global aboutpage
    global codeRun
    global viewResult
    if processing_status():
	satfile = open('/home/pi/weather/predict/processing.sat','r')
	sat_name_process = satfile.read().splitlines()
    while processing_status() and not recording_status():
	screen.fill((255, 255, 255))
	screen.blit(font.render("Processing...", True, (0, 0, 255)), (105, 70))
	screen.blit(font.render(sat_name_process[0], True, (0, 0, 255)), (120, 110))
	pygame.display.flip()
	details = False
	aboutpage = False
	codeRun = True
	viewResult = False
	


def resultscreen():
    global details
    global viewResult
    global aboutpage
    global codeRun
    
    if not png_exist():
	viewResult = False
    while viewResult and not aboutpage and not details and not recording_status() and not processing_status():
	screen.fill((255, 255, 255))
	my_image = pygame.image.load(png_name)
	my_image = pygame.transform.scale(my_image,(320,240))
	screen.blit(my_image, (0,0))
	screen.blit(smallfont.render("Back", True, (255, 255, 255)), (285, 100))
	pygame.display.flip()
	codeRun = True
	details = False
	aboutpage = False




pygame.init()
pygame.mouse.set_visible(False)

size = width, height = 320, 240
BLACK = 0, 0, 0
WHITE = 255,255,255
RED = 255,0,0
GREEN = 0,255,0

screen = pygame.display.set_mode(size)
font = pygame.font.SysFont('Consolas', 30)    
smallfont = pygame.font.SysFont('Consolas', 15)

#my_font = pygame.font.Font(None,20)
#my_buttons = {(240,220):'quit',(160,120):'STOP', (50,20):'Left History', (250,20): 'Right History'}
codeRun = True
details = False
aboutpage = False
status = ""
stime = ""
sat_name = ""
png_name = "/home/pi/weather/zzyzth.png"
viewResult = False

def GPIO27_CB(channel):
    global codeRun
    codeRun = False 

 
def GPIO17_CB(channel):
    global details
    if details:
	details = False
    else:
	details = True

def GPIO22_CB(channel):
    global viewResult
    if viewResult:
        viewResult = False 
    else:
	viewResult = True

def GPIO23_CB(channel):
    global aboutpage
    if aboutpage:
        aboutpage = False 
    else:
	aboutpage = True

GPIO.add_event_detect(27, GPIO.FALLING, callback = GPIO27_CB, bouncetime=300)
GPIO.add_event_detect(22, GPIO.FALLING, callback = GPIO22_CB, bouncetime=300)
GPIO.add_event_detect(23, GPIO.FALLING, callback = GPIO23_CB, bouncetime=300)
GPIO.add_event_detect(17, GPIO.FALLING, callback = GPIO17_CB, bouncetime=300)
	
try:
	file = open('/home/pi/weather/predict/atqlog.sat','r')
	satatq = []
	with open('/home/pi/weather/predict/atqlog.sat','r') as f:
	    satatq = f.read().splitlines()
		
	status = satatq[0]
	df = pd.DataFrame(satatq)
	df_split = df.iloc[1:,0].str.split(',',expand=True)
	df_sort = df_split.sort_values(by=[1])
	df_sort = df_sort.reset_index(drop=True)
	
	while codeRun:
	    aboutscreen()
	    detailscreen()
	    recordingscreen()
	    processingscreen()
	    count_down()
	    resultscreen()
	    
	
	   

	GPIO.cleanup()
except KeyboardInterrupt:
	GPIO.cleanup()
//schedule_satellite.sh
# Team Member: Tianhao Zhang (tz373), Zeyu Zhang (zz648)
# Final Project, 12/05/2019
#!/bin/bash
PREDICTION_START=`/usr/bin/predict -t /home/pi/weather/predict/weather.tle -p "${1}" | head -1`
PREDICTION_END=`/usr/bin/predict -t /home/pi/weather/predict/weather.tle -p "${1}" | tail -1`


var2=`echo $PREDICTION_END | cut -d " " -f 1`

MAXELEV=`/usr/bin/predict -t /home/pi/weather/predict/weather.tle -p "${1}" | awk -v max=0 '{if($5>max){max=$5}}END{print max}'`

while [ `date --date="TZ=\"UTC\" @${var2}" +%D` == `date +%D` ] || [ `date --date="TZ=\"UTC\" @${var2}" +%D` == `date -d "+1 day" +%D` ]; do

START_TIME=`echo $PREDICTION_START | cut -d " " -f 3-4`

var1=`echo $PREDICTION_START | cut -d " " -f 1`

var3=`echo $START_TIME | cut -d " " -f 2 | cut -d ":" -f 3`
TIMER=`expr $var2 - $var1 + $var3`

OUTDATE=`date --date="TZ=\"UTC\" $START_TIME" +%Y%m%d-%H%M%S`

if [ $MAXELEV -gt 19 ]
  then
    echo ${1//" "}${OUTDATE} $MAXELEV
    echo "/home/pi/weather/predict/receive_and_process_satellite.sh \"${1}\" $2 /home/pi/weather/${1//" "}${OUTDATE} /home/pi/weather/predict/weather.tle $var1 $TIMER" >> /home/pi/weather/predict/goodluck.sat
    echo "${1},${OUTDATE},${TIMER}" >> /home/pi/weather/predict/atqlog.sat
fi

nextpredict=`expr $var2 + 60`

PREDICTION_START=`/usr/bin/predict -t /home/pi/weather/predict/weather.tle -p "${1}" $nextpredict | head -1`
PREDICTION_END=`/usr/bin/predict -t /home/pi/weather/predict/weather.tle -p "${1}"  $nextpredict | tail -1`

MAXELEV=`/usr/bin/predict -t /home/pi/weather/predict/weather.tle -p "${1}" $nextpredict | awk -v max=0 '{if($5>max){max=$5}}END{print max}'`

var2=`echo $PREDICTION_END | cut -d " " -f 1`

done
// schedule_all.sh
# Team Member: Tianhao Zhang (tz373), Zeyu Zhang (zz648)
# Final Project, 12/05/2019
#!/bin/bash

sudo rm /home/pi/weather/predict/atqlog.sat
sudo rm /home/pi/weather/predict/goodluck.sat
# Update Satellite Information

wget -qr https://www.celestrak.com/NORAD/elements/weather.txt -O /home/pi/weather/predict/weather_copy.txt


if [ $? -eq 0 ]
then
  sudo mv /home/pi/weather/predict/weather_copy.txt /home/pi/weather/predict/weather.txt
  echo "Internet Connected Mode" > /home/pi/weather/predict/atqlog.sat
else
  rm /home/pi/weather/predict/weather_copy.txt
  echo "Internet Offline Mode" > /home/pi/weather/predict/atqlog.sat
fi

grep "NOAA 15" /home/pi/weather/predict/weather.txt -A 2 > /home/pi/weather/predict/weather.tle
grep "NOAA 18" /home/pi/weather/predict/weather.txt -A 2 >> /home/pi/weather/predict/weather.tle
grep "NOAA 19" /home/pi/weather/predict/weather.txt -A 2 >> /home/pi/weather/predict/weather.tle
grep "METEOR-M 2" /home/pi/weather/predict/weather.txt -A 2 >> /home/pi/weather/predict/weather.tle


#Schedule Satellite Passes:

/home/pi/weather/predict/schedule_satellite.sh "NOAA 19" 137.1000
/home/pi/weather/predict/schedule_satellite.sh "NOAA 18" 137.9125
/home/pi/weather/predict/schedule_satellite.sh "NOAA 15" 137.6200