Satellite Tracking Antenna

  • Student: Razvan-Andrei Stoica
  • Master: AAC2

1. Introduction


The National Oceanic and Atmospheric Administration (abbreviated as NOAA /ˈnoʊ.ə/ NOH-ə) is a scientific and regulatory agency within the United States Department of Commerce that forecasts weather, monitors oceanic and atmospheric conditions, charts the seas, conducts deep sea exploration, and manages fishing and protection of marine mammals and endangered species in the U.S. exclusive economic zone.

For years, NOAA’s Polar-orbiting Operational Environmental Satellites (POES) satellites have provided the backbone of the global observing system. Their current operational POES satellites include NOAA-15, NOAA-18, and NOAA-19. Today, they operate in various primary and secondary roles, providing additional full global data coverage for a broad range of weather and environmental applications, supporting both short-term weather forecasting and long-term climate and environmental data records.


Automatic Picture Transmission (APT), also known as NOAA-GEOSAT, is an analog image transmission mode used to by the NOAA weather satellites and formerly some Russian weather satellites to transmit satellite weather photos.

In order to receive those pictures, different types of antennas can be used. Some of them, such as the V-Dipole, must be oriented towards the true north due to satellites' trajectories.


The initial goal of this project was to make a device that is able to orient an antenna towards one of the 3 active NOAA satellites. Due to the fact that orienting the antenna towards satellites themselves is not necessary and due to other technical circumstances, the final project resumes to a device that collects data necessary for calibrating the software and also allows its user to align an antenna to specific angles. In the next chapters we'll talk about the used hardware, the hardly working software and how to set the device up.

2. Hardware

Mechanical Elements

  • The largest part of the project is the chassis. This allows the movement of an antenna on two axis and also hosts all the electronics. It uses 4 radial bearings and one axial bearing. Everything (except for the bearings, axes and screws) was 3D printed with PETG.

  • The antenna is one of the most important parts of this project. I chose to go with the default antenna that came with the RTL-SDR Kit. It is a V-Dipole adjustable one. In order to make this project work, I had to adapt it and print a piece that keep the two poles at a 120 degree angle. According to an article from the RTL-SDR forum, each pole's size must be 53.4cm.

  • A dedicated ground plane had to be built for the GPS antenna. It is a 70x70mm PCB with a hole where the cable of the antenna passes.

Electronic Components

The following components have been used:

  • Raspberry Pi 4B
  • Arduino UNO
  • GRB Shield
  • 2x A4988 Motor Driver
  • BNO055 IMU
  • NEO6MV2 GPS Module + DI(wh)Y antenna
  • 2x 17HS8401S Stepper Motor (EMIs go brrrr)

The components' connections can be seen in the following diagram:

3. Software

The software chapter can be split into 3 parts

Raspberry Pi

1st Functionality: Exchange Data

The RPi runs a python script that gathers data from the GPS and from Arduino UNO's IMU and uploads it to the database, alongside its own IP in the LAN. Various libraries had to be installed and a setup for GPS was needed.

#!/usr/bin/env python
import pyrebase
import time
from gps import *
import netifaces as ni
import signal
import serial
firebaseConfig = {
  "apiKey": "REDACTED",
  "authDomain": "REDACTED",
  "databaseURL": "REDACTED",
  "projectId": "REDACTED",
  "storageBucket": "REDACTED",
  "messagingSenderId": "REDACTED",
  "appId": "REDACTED"
def configDatabase():
    firebase = pyrebase.initialize_app(firebaseConfig)
    auth = firebase.auth()
    login = auth.sign_in_with_email_and_password(USER_EMAIL, USER_PASSWORD)
    db = firebase.database()
    return (db, auth, login)
def configArduino():
    mySerial = serial.Serial('/dev/ttyACM0', 115200, timeout = 1)
    return mySerial
def getIP():
    ip_addr = 'Not Available'
        ip_addr = ni.ifaddresses('wlan0')[ni.AF_INET][0]['addr']
            ip_addr = ni.ifaddresses('eth0')[ni.AF_INET][0]['addr']
    return ip_addr    
def getPositionData(gpsd):
    nx =
    while (nx['class'] != 'TPV'):
        nx =
    latitude = getattr(nx,'lat', 'Not Available')
    longitude = getattr(nx,'lon', 'Not Available')
    return (str(longitude), str(latitude))
def getGPSPosition():
    gpsd = gps(mode=WATCH_ENABLE|WATCH_NEWSTYLE)
        (longitude, latitude) = getPositionData(gpsd)
        return (longitude, latitude)
        return ('Not Available', 'Not Available')
def getANTPosition(arduino_serial):
    head_x = 'Not Available'
    head_y = 'Not Available'
    if arduino_serial.in_waiting > 0:
        line = arduino_serial.readline().decode('utf-8').rstrip()
        headings = line.split(',')
        for heading in headings:
            if heading.startswith('X'):
                head_x = heading.strip('X')
            elif heading.startswith('Y'):
                head_y = heading.strip('Y')
    return (head_x, head_y)
def gatherData(arduino_serial):
    ip_addr = getIP()
    (longitude, latitude) = getGPSPosition()
    (head_x, head_y) = getANTPosition(arduino_serial)
    return (ip_addr, longitude, latitude, head_x, head_y)
def main():
    print("Client has started!")
    (db, auth, login) = configDatabase()
    mySerial = configArduino()
    while True:
        new_data = gatherData(mySerial)
        data = {}
        if (new_data[0] != 'Not Available'):
            data['IP'] = new_data[0]
        if (new_data[1] != 'Not Available'):
            data['LONGITUDE'] = new_data[1]
        if (new_data[2] != 'Not Available'):
            data['LATITUDE'] = new_data[2]
        if (new_data[3] != 'Not Available'):
            data['HEADING_X'] = new_data[3]
        if (new_data[4] != 'Not Available'):
            data['HEADING_Y'] = new_data[4]
        # Reading from DB
        read_data = db.child('UsersData').child(login['localId']).child('Data').child('MOVEMENT').get(login['idToken'])
        movement_x = read_data.val()
        if (movement_x != 0):
            data['MOVEMENT'] = 0
            message_to_send = str(movement_x) + '\n'
            message_to_send = message_to_send.encode('ASCII')
        # Writing to DB
        db.child('UsersData').child(login['localId']).child('Data').update(data, login['idToken'])
    return 0
if __name__ == "__main__":
    raise SystemExit(main())
2nd Functionality: TCP Server for RTL-SDR

RTL-SDR can now be used remotely in the LAN (hence why its IP is needed). This was done by installing and configuring rtl_tcp.

Arduino UNO

The Arduino UNO reads data from the IMU and from Serial and outputs data to the Serial and to the motors (makes them spin). This is the code it runs:

// Include the AccelStepper Library
#include <AccelStepper.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BNO055.h>
// Define pin connections
const int dirPin = 6;
const int stepPin = 3;
const int enablePin = 8;
// Define motor interface type
#define motorInterfaceType 1
// Creates an instance
AccelStepper myStepper(motorInterfaceType, stepPin, dirPin);
double xPos = 0, yPos = 0, headingVel = 0;
uint16_t BNO055_SAMPLERATE_DELAY_MS = 10; //how often to read data from the board
uint16_t PRINT_DELAY_MS = 100; // how often to print the data
uint16_t printCount = 0; //counter to avoid printing every 10MS sample
//velocity = accel*dt (dt in seconds)
//position = 0.5*accel*dt^2
double ACCEL_VEL_TRANSITION =  (double)(BNO055_SAMPLERATE_DELAY_MS) / 1000.0;
double DEG_2_RAD = 0.01745329251; //trig functions require radians, BNO055 outputs degrees
// Check I2C device address and correct line below (by default address is 0x29 or 0x28)
//                                   id, address
Adafruit_BNO055 bno = Adafruit_BNO055(55, 0x28);
unsigned long tStart;
void setup() {
  while (!Serial) delay(10);  // wait for serial port to open!
  if (!bno.begin())
    Serial.print("No BNO055 detected");
    while (1);
  // set the maximum speed, acceleration factor,
  // initial speed and the target position
  pinMode(enablePin, OUTPUT);
  digitalWrite(enablePin, LOW);
  tStart = micros();
void loop() {
  if (Serial.available() > 0){
    String myData = Serial.readStringUntil('\n');
  // Change direction once the motor reaches target position
  if (myStepper.distanceToGo() != 0);
  if ((micros() - tStart) > (BNO055_SAMPLERATE_DELAY_MS * 1000)) {
    tStart = micros();
    sensors_event_t orientationData , linearAccelData;
    bno.getEvent(&orientationData, Adafruit_BNO055::VECTOR_EULER);
    //  bno.getEvent(&angVelData, Adafruit_BNO055::VECTOR_GYROSCOPE);
    bno.getEvent(&linearAccelData, Adafruit_BNO055::VECTOR_LINEARACCEL);
    xPos = xPos + ACCEL_POS_TRANSITION * linearAccelData.acceleration.x;
    yPos = yPos + ACCEL_POS_TRANSITION * linearAccelData.acceleration.y;
    // velocity of sensor in the direction it's facing
    headingVel = ACCEL_VEL_TRANSITION * linearAccelData.acceleration.x / cos(DEG_2_RAD * orientationData.orientation.x);
    if (printCount * BNO055_SAMPLERATE_DELAY_MS >= PRINT_DELAY_MS) {
      //enough iterations have passed that we can print the latest data
      printCount = 0;
    else {
      printCount = printCount + 1;


Code and Configuration

The database has been configured the same way it was done at school, but the field in the database are rather rewritten than appended. A firebase-hosted web app has also been added as the GUI for the project (again, same as at school). It uses a simple html page and the following javascript code that deals with database communication:

  const setupUI = (user) => {
    if (user) {
      //toggle UI elements = 'none'; = 'block'; ='block'; ='block';
      userDetailsElement.innerHTML =;
      // get user UID to get data from database
      var uid = user.uid;
      // Database paths (with user UID)
      var dbPath = 'UsersData/' + uid.toString();
      // Database references
      var dbRef = firebase.database().ref(dbPath);
      // Checbox (cards for sensor readings)
      cardsCheckboxElement.addEventListener('change', (e) =>{
        if (cardsCheckboxElement.checked) {
 = 'block';
 = 'none';
      var intervalId = window.setInterval(function(){
        // CARDS
        // Get the latest readings and display on cards
        dbRef.orderByKey().on('child_added', snapshot =>{
          var jsonData = snapshot.toJSON();
          var ip = jsonData.IP;
          var heading_x = jsonData.HEADING_X;
          var heading_y = jsonData.HEADING_Y;
          var latitude = jsonData.LATITUDE;
          var longitude = jsonData.LONGITUDE;
          // Update DOM elements
          ipElement.innerHTML = ip;
          yawElement.innerHTML = heading_x;
          pitchElement.innerHTML = heading_y;
          latElement.innerHTML = latitude;
          longElement.innerHTML = longitude;
      }, 2000);
      turnCWButtonElement.addEventListener('click', (e) =>{
        var updates = {};
        updates['/UsersData/' + uid.toString() + '/Data/MOVEMENT'] = "10";
      turnCCWButtonElement.addEventListener('click', (e) => {
        var updates = {};
        updates['/UsersData/' + uid.toString() + '/Data/MOVEMENT'] = "-10";

The GUI can be seen in the picture below. The web app outputs antenna's current position, the longitude and latitude of the device and the RPi's IP and expects one of the two buttons pressed as input (those will move the antenna on one of the axis).

The address of the web app is:

4. Using the Device

In order to use the device, one should follow those steps:

  1. Connect the Raspberry Pi to the LAN via Wi-Fi or Ethernet (the later might not be possible do to the rotating nature of the device);
  2. Install SDR#, GPredict, SDRSharp.GpredictConnector, WXtoIMG and VBAudio Cable (Internal Audio Routing);
  3. Start SDR#
  4. Select RTL-SDR TCP as source → Click on Gear Icon → Add the IP address of the RPi (that can be seen in the GUI) in the IP field;
  5. Select Settings (3 lines) → go to Plugins → GPredictConnect → Check the enable checkbox;
  6. Select Settings (3 lines) → go to Audio → click on Output → Select [Windows DirectSound] Cable Input (VB-Audio Cable)
  7. Select Settings (3 lines) → go to Radio → check WFM radio button (how funny) and set the Bandwidth to 40KHz
  8. Start GPredict
  9. Go to Edit → Preferences → Ground Station and add a new ground station using the information from the GUI (latitude and longitude)
  10. Go to Edit → Preferences → Interfaces → Add new Interface (SDRSharp with default port)
  11. Go to Edit → Update TLE data from network
  12. Go to Edit → Update transponder data
  13. Go to File → New module → choose a name; choose your ground station name; choose the 3 NOAA satellites (15 18 and 19) and add them to the right → Click OK
  14. Go to the little triangle to the right (Module options / shortcuts) → Radio Control → Select your Radio defined earlier and click Engage
  15. From the Radio Control panel → Choose a satellite that's in your range → Choose APT Downlink → Click Track and T buttons
  16. Go to the little triangle and also check the “Autotrack” checkbox
  17. Open WXtoImg
  18. Go to Options → Ground Station Location → Set latitude and longitude → Click Ok
  19. Go to Options → Recording Options → Make sure the soundcard is the VB-Audio Virtual Cable → Click Ok
  20. Go to File → Update Keplers (deprecated, must find another way)
  21. Go to File → Record → Auto Record
  22. Wait & Hope

But how does it work?

  • The SDR#, is, as the name says, a Software Defined Radio. It is used to receive and filter radio waves. This tool uses RTL_TCP as a device (the RTL_SDR connected to the RPi.
  • GPredict is a software that tracks satellites and predicts their future positions. This tool will adjust the frequency to the desired satellite (closest one when auto-tracking).
  • WXtoImg takes the analogic data and converts it into images. This software receives the data from SDR# via a virtual “audio cable”.

5. Challenges

Here is a list of past and current problems

  • Many plastic parts had to be redone multiple times and some still do
  • The design is really bad
  • The device needs a power supply
  • The device needs a way to receive current without cables (maybe brushes or batteries)
  • The device has 3 antennas near 2 big motors, what could go wrong
  • RTL-SDR is getting hot, please don't burn
  • GPS doesn't work indoors (mostly works near windows)
  • The main antenna is not optimal
  • The IMU loses its calibration quickly, doesn't have an EPROM and is very hard to calibrate
  • The IMU doesn't even reach 90 degrees when vertical, for some reason
iothings/proiecte/2022/razvanandreistoica.txt · Last modified: 2023/01/20 03:01 by razvan.stoica0211
CC Attribution-Share Alike 3.0 Unported Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0