By: Tianhao Zhang (tz373), Zeyu Zhang (zz648)
Publish Date: Dec. 14, 2019
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.
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.
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.
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.
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.
tz373@cornell.edu
Assembled QFH Antenna with Zeyu and Developed Software
Produced Promo Video for the project
zz638@cornell.edu
Assembled QFH Antenna with Tianhao and Developed Software
Tested the overall system
// 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