Satellite Tracking Ham Radio Antenna Rotator

Table of contents

Satellite Tracking Ham Radio Antenna Rotator

Abstract

I've been working on several different antenna rotor control systems for ham radio and this kit will be used to develop a small satellite tracking system integrating with what I already have built.

This project is a mini version of my automatic satellite tracking system antenna rotor controller, The system consists of several subsystems.  There are many satellites in orbit that transmit telemetry, CW (morse) code, and some satellites have radio repeaters that anyone can use to talk to people around the world.  Some satellites are stationary with relative to the earth's rotation, but many travel around in various orbits and at various heights above the ground. 

The satellite tracking network I am currently using is called SATNOGS.  The SATNOGS network is composed of hundreds of ground stations downloading telemetry data from dozens of satellites and uploading the data into an observation database.

The SATNOGS wiki page can be found here:

SatNOGS Wiki

My ground station database for KD9QOG can be found here:

SatNOGS Network - Ground Station KD9QOG

I've been working with this type of system for about three years and have built two rotor controllers so far.  My previous information can be found here:

SatNOGS automatic satellite tracking antenna rotator - element14 Community

The basic SATNOGS ground station consists of a client system, a radio, and antenna.  Some antennas are omnidirectional and are stationary, while others are directional and require a rotor system with elevation and azimuth control.

image 

The purpose of this project is to construct a mini antenna rotator that will interface with the satellite tracking software and automatically point a directional antenna at a chosen satellite to record an observation for upload into the international database.

The SATNOGS client is a raspberry pi 4 with a RTL-SDR radio.

image

The antenna rotor system is an Arduino micro attached to the TCM5272 Trinamic motor control board, along with two NEMA 17 stepper motors and some custom 3d printed parts.

image

The pinout for the Arduino micro and TMC5272 is as follows:

image

Using the TMC5272_TMC_API simple rotation C code example as a starting base, here's a video of what a 2.4GHz antenna automatically tracking a satellite would look like:

The code snippet for this demo is below:

image

To interface the AD Trinamic TMC5272 motor controller to the SATNOGS client, the commands to control the antenna rotator based on the satellite orbit in relation to the observer ground station need to be converted to the motor control commands to move each motor in the elevation and azimuth directions.  The list of supported antenna rotators does not currently support the AD Trinamic board, so some sort of conversion must be realized in the Arduino firmware.

The SatNOGS Client uses hamlib to speak to a rotator.

Rotators - SatNOGS Wiki

SatNOGS Client Setup - SatNOGS Wiki

The protocol for the EasyComm is as follows:

mustbeart.com/software/easycomm.txt

EASYCOMM II Standard
--------------------

The EasyComm 2 standard is an enhanced protocol to allow full station
control
and also feedback from external systems.

The host PC issues commands to the controller by sending a 2 character
command identifier followed by the command value.  Commands are
separated by either a space or carriage return or linefeed.
Not all commands need to be implemented, and the most basic system
may only decode the rotator control commands.

The Host PC can issue the following commands -:

Command		Meaning			Perameters
-------		-------			----------
AZ		Azimuth			number - 1 decimal place
EL		Elevation		number - 1 decimal place
UP		Uplink freq		in Hertz
DN		Downlink freq		in Hertz
DM		Downlink Mode		ascii, eg SSB, FM
UM		Uplink Mode		ascii, eg SSB, FM
DR		Downlink Radio		number
UR		Uplink Radio		number
ML		Move Left
MR		Move Right
MU		Move Up
MD		Move Down
SA		Stop azimuth moving
SE		Stop elevation moving
AO		AOS
LO		LOS
OP		Set output		number
IP		Read an input		number
AN		Read analogue input	number
ST		Set time		YY:MM:DD:HH:MM:SS
VE		Request Version

One method for completing this project is to define the protocol for EasyComm in my arduino connected to the TMC5272 and implement it through the USB or serial port.
The firmware for the DIY SATNOGS rotor controller can be found here:
SatNOGS Rotator v3 - SatNOGS Wiki
SatNOGS Rotator Controller - SatNOGS Wiki
Files · master · librespacefoundation / SatNOGS / satnogs-rotator-firmware · GitLab

The SATNOGS rotor controller incorporates two end-stop limit switches and optical motor position encoders for each axis. I will need to address this in my design.


Another method for this project is to use the SATNOGS antenna rotor controller firmware (RS485) as a base and incorporate the AD Trinamic SPI commands for the TMC5272 motor controller kit.
Each method has its challenges.
A third method is to use a separate tracking program to send commands to the antenna rotator and leave the SATNOGS client as is. One of these third-party software packages is called SATPC32
SATPC32 uses a RS232 or USB interface for commercial antenna rotor control, and it might be easier to use.
image
image
image

So my current dilemma is the choice of using the AD Trinamic code as a base and add the EasyComm protocol, or use the SATNOGS rotator firmware as a base and add the AD Trinamic SPI code to it.

This is the firmware code to decode the serial commands.

 // Read from serial
           while (Serial.available() > 0) {
               incomingByte = Serial.read();
               // Read new data, '\n' means new pacakage
               if (incomingByte == '\n' || incomingByte == '\r') {
                   buffer[BufferCnt] = 0;
                   if (buffer[0] == 'A' && buffer[1] == 'Z') {
                       if (buffer[2] == ' ' && buffer[3] == 'E' &&
                           buffer[4] == 'L') {
                           // Send current absolute position in deg
                           str1 = String("AZ");
                           str2 = String(control_az.input, 1);
                           str3 = String(" EL");
                           str4 = String(control_el.input, 1);
                           str5 = String("\n");
                           Serial.print(str1 + str2 + str3 + str4 + str5);
                       } else {
                           // Get the absolute position in deg for azimuth
                           rotator.control_mode = position;
                           rawData = strtok_r(Data, " ", &Data);
                           strncpy(data, rawData + 2, 10);
                           if (isNumber(data)) {
                               control_az.setpoint = atof(data);
                           }
                           // Get the absolute position in deg for elevation
                           rawData = strtok_r(Data, " ", &Data);
                           if (rawData[0] == 'E' && rawData[1] == 'L') {
                               strncpy(data, rawData + 2, 10);
                               if (isNumber(data)) {
                                   control_el.setpoint = atof(data);
                               }
                           }
                       }
                   } else if (buffer[0] == 'E' && buffer[1] == 'L') {
                           // Get the absolute position in deg for elevation
                           rotator.control_mode = position;
                           rawData = strtok_r(Data, " ", &Data);
                           if (rawData[0] == 'E' && rawData[1] == 'L') {
                               strncpy(data, rawData + 2, 10);
                               if (isNumber(data)) {
                                   control_el.setpoint = atof(data);
                               }
                           }
                   } else if (buffer[0] == 'V' && buffer[1] == 'U') {
                       // Elevation increase speed in mdeg/s
                       rotator.control_mode = speed;
                       strncpy(data, Data + 2, 10);
                       if (isNumber(data)) {
                           // Convert to deg/s
                           control_el.setpoint_speed = atof(data) / 1000;
                       }
                   } else if (buffer[0] == 'V' && buffer[1] == 'D') {
                       // Elevation decrease speed in mdeg/s
                       rotator.control_mode = speed;
                       strncpy(data, Data + 2, 10);
                       if (isNumber(data)) {
                           // Convert to deg/s
                           control_el.setpoint_speed = - atof(data) / 1000;
                       }
                   } else if (buffer[0] == 'V' && buffer[1] == 'L') {
                       // Azimuth increase speed in mdeg/s
                       rotator.control_mode = speed;
                       strncpy(data, Data + 2, 10);
                       if (isNumber(data)) {
                           // Convert to deg/s
                           control_az.setpoint_speed = atof(data) / 1000;
                       }
                   } else if (buffer[0] == 'V' && buffer[1] == 'R') {
                       // Azimuth decrease speed in mdeg/s
                       rotator.control_mode = speed;
                       strncpy(data, Data + 2, 10);
                       if (isNumber(data)) {
                           // Convert to deg/s
                           control_az.setpoint_speed = - atof(data) / 1000;
                       }
                   } else if (buffer[0] == 'S' && buffer[1] == 'A' &&
                              buffer[2] == ' ' && buffer[3] == 'S' &&
                              buffer[4] == 'E') {
                       // Stop Moving
                       rotator.control_mode = position;
                       str1 = String("AZ");
                       str2 = String(control_az.input, 1);
                       str3 = String(" EL");
                       str4 = String(control_el.input, 1);
                       str5 = String("\n");
                       Serial.print(str1 + str2 + str3 + str4 + str5);
                       control_az.setpoint = control_az.input;
                       control_el.setpoint = control_el.input;
                   } else if (buffer[0] == 'R' && buffer[1] == 'E' &&
                              buffer[2] == 'S' && buffer[3] == 'E' &&
                              buffer[4] == 'T') {
                       // Reset the rotator, go to home position
                       str1 = String("AZ");
                       str2 = String(control_az.input, 1);
                       str3 = String(" EL");
                       str4 = String(control_el.input, 1);
                       str5 = String("\n");
                       Serial.print(str1 + str2 + str3 + str4 + str5);
                       rotator.homing_flag = false;
                   } else if (buffer[0] == 'P' && buffer[1] == 'A' &&
                              buffer[2] == 'R' && buffer[3] == 'K' ) {
                       // Park the rotator
                       rotator.control_mode = position;
                       str1 = String("AZ");
                       str2 = String(control_az.input, 1);
                       str3 = String(" EL");
                       str4 = String(control_el.input, 1);
                       str5 = String("\n");
                       Serial.print(str1 + str2 + str3 + str4 + str5);
                       control_az.setpoint = rotator.park_az;
                       control_el.setpoint = rotator.park_el;
                   } else if (buffer[0] == 'V' && buffer[1] == 'E') {
                       // Get the version if rotator controller
                       str1 = String("VE");
                       str2 = String("SatNOGS-v2.2");
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '0') {
                       // Get the inside temperature
                       str1 = String("IP0,");
                       str2 = String(rotator.inside_temperature, DEC);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '1') {
                       // Get the status of end-stop, azimuth
                       str1 = String("IP1,");
                       str2 = String(rotator.switch_az, DEC);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '2') {
                       // Get the status of end-stop, elevation
                       str1 = String("IP2,");
                       str2 = String(rotator.switch_el, DEC);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '3') {
                       // Get the current position of azimuth in deg
                       str1 = String("IP3,");
                       str2 = String(control_az.input, 2);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '4') {
                       // Get the current position of elevation in deg
                       str1 = String("IP4,");
                       str2 = String(control_el.input, 2);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '5') {
                       // Get the load of azimuth, in range of 0-1023
                       str1 = String("IP5,");
                       str2 = String(control_az.load, DEC);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '6') {
                       // Get the load of elevation, in range of 0-1023
                       str1 = String("IP6,");
                       str2 = String(control_el.load, DEC);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '7') {
                       // Get the speed of azimuth in deg/s
                       str1 = String("IP7,");
                       str2 = String(control_az.speed, 2);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'I' && buffer[1] == 'P' &&
                              buffer[2] == '8') {
                       // Get the speed of elevation in deg/s
                       str1 = String("IP8,");
                       str2 = String(control_el.speed, 2);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'G' && buffer[1] == 'S') {
                       // Get the status of rotator
                       str1 = String("GS");
                       str2 = String(rotator.rotator_status, DEC);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if (buffer[0] == 'G' && buffer[1] == 'E') {
                       // Get the error of rotator
                       str1 = String("GE");
                       str2 = String(rotator.rotator_error, DEC);
                       str3 = String("\n");
                       Serial.print(str1 + str2 + str3);
                   } else if(buffer[0] == 'C' && buffer[1] == 'R') {
                       // Get Configuration of rotator
                       if (buffer[3] == '1') {
                           // Get Kp Azimuth gain
                           str1 = String("1,");
                           str2 = String(control_az.p, 2);
                           str3 = String("\n");
                           Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '2') {
                           // Get Ki Azimuth gain
                           str1 = String("2,");
                            str2 = String(control_az.i, 2);
                            str3 = String("\n");
                            Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '3') {
                           // Get Kd Azimuth gain
                           str1 = String("3,");
                           str2 = String(control_az.d, 2);
                           str3 = String("\n");
                           Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '4') {
                           // Get Kp Elevation gain
                           str1 = String("4,");
                            str2 = String(control_el.p, 2);
                            str3 = String("\n");
                            Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '5') {
                           // Get Ki Elevation gain
                           str1 = String("5,");
                           str2 = String(control_el.i, 2);
                           str3 = String("\n");
                           Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '6') {
                           // Get Kd Elevation gain
                           str1 = String("6,");
                           str2 = String(control_el.d, 2);
                           str3 = String("\n");
                           Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '7') {
                           // Get Azimuth park position
                           str1 = String("7,");
                           str2 = String(rotator.park_az, 2);
                           str3 = String("\n");
                           Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '8') {
                           // Get Elevation park position
                           str1 = String("8,");
                           str2 = String(rotator.park_el, 2);
                           str3 = String("\n");
                           Serial.print(str1 + str2 + str3);
                       } else if (buffer[3] == '9') {
                           // Get control mode
                           str1 = String("9,");
                           str2 = String(rotator.control_mode);
                           str3 = String("\n");
                           Serial.print(str1 + str2 + str3);
                       }
                   } else if (buffer[0] == 'C' && buffer[1] == 'W') {
                       // Set Config
                       if (buffer[2] == '1') {
                           // Set Kp Azimuth gain
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               control_az.p = atof(data);
                           }
                       } else if (buffer[2] == '2') {
                           // Set Ki Azimuth gain
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               control_az.i = atof(data);
                           }
                       } else if (buffer[2] == '3') {
                           // Set Kd Azimuth gain
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               control_az.d = atof(data);
                           }
                       } else if (buffer[2] == '4') {
                           // Set Kp Elevation gain
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               control_el.p = atof(data);
                           }
                       } else if (buffer[2] == '5') {
                           // Set Ki Elevation gain
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               control_el.i = atof(data);
                           }
                       } else if (buffer[2] == '6') {
                           // Set Kd Elevation gain
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               control_el.d = atof(data);
                           }
                       }  else if (buffer[2] == '7') {
                           // Set the Azimuth park position
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               rotator.park_az = atof(data);
                           }
                       } else if (buffer[2] == '8') {
                           // Set the Elevation park position
                           rawData = strtok_r(Data, ",", &Data);
                           strncpy(data, rawData + 4, 10);
                           if (isNumber(data)) {
                               rotator.park_el = atof(data);
                           }
                       }
                   } else if (buffer[0] == 'R' && buffer[1] == 'S'
                           && buffer[2] == 'T') {
                       // Custom command to test the watchdog timer routine
                       while(1)
                           ;
                   } else if (buffer[0] == 'R' && buffer[1] == 'B') {
                       // Custom command to reboot the uC
                       wdt_enable(WDTO_2S);
                       while(1);
                   }
                   // Reset the buffer an clean the serial buffer
                   BufferCnt = 0;
                   Serial.flush();
               } else {
                   // Fill the buffer with incoming data
                   buffer[BufferCnt] = incomingByte;
                   BufferCnt++;
               }
           }
       }


The SATNOGS rotator controller code uses two endstop limit switches, an optical encoder, and a home switch to determine absolute position,
the TMC5272 eval board does not have enough inputs for 6 switches so I'll need to figure out some other way to determine "home".

I've attached the .stl files for the 3D printed parts.

If you'd like to build your own 2.4GHz Yagi antenna, here is a link:
Build a 2.4 GHz 10 element Yagi Antenna - IW5EDI Simone - Ham-Radio

This site shows how to run an Arduino based rotor controller from Gpredict:


SatNOGS Arduino Uno/CNC Shield Based Rotator Controller - SatNOGS Wiki

Ham Radio Control Libraries download | SourceForge.net
Gpredict download | SourceForge.net

Install hamlib

Download the Hamblib software and follow the installation process. this may require you installing it with Administrator rights. using device manager find the port for the Arduino

Using a text editor, like Notepad++, to create a file with the code below in it. Note where COM7 is and amend with your own COM port. Save as a batch file (i.e. with th extension .bat file in the same folder as rotctld (Usually found in C:\Program Files (x86)\hamlib-w64-3.2\bin )

rotctld -m 202 -r COM7 -s 9600 -T 127.0.0.1 -t 4533 -C timeout=500 -C retry=0 -vvvvvvvv > pause
image

image

hamlib-rotctl-easycomm-parser/src at main · yapiolibs/hamlib-rotctl-easycomm-parser · GitHub

This is the given example of a command parser:
void loop()
{
    EasycommData result;

    easycommData(&result);
    if(easycommParseCommand("AZ000.1 EL000.0 UP000000000 UUU DN000000000 DDD", &result, EasycommParserStandard1))
    {
        // standard 1 command was parsed
    }

    easycommData(&result);
    if(easycommParseCommand("AZ100", &result, EasycommParserStandard2))
    {
        // standard 2 command was parsed
    }

    easycommData(&result);
    if(easycommParseCommand("VU50", &result, EasycommParserStandard23))
    {
        // standard 3 command was parsed
    }
}

So I have two problems right now:

1) SatPC32 will connect to the Arduino and send commands through a FTDI serial interface
and the software recognizes the input, but SatPC32 does not format the rotor control commands
in EasyComm format, so I have to figure out what format the commercial rotors use and code that into a library to use.

2) Gpredict outputs the correct format for EasyComm, but I haven't been able to get the rotctl batch file to run correctly for Gpredict to find the module.

This works manually using SatPC32:

image
Here's a short video


Finally! It works. Here's a video of Gpredict antenna module control of the rotor system with hamlib rotctld running in the background.

image
image
image
I don't have my limit switches wired in yet, however, all the rest of the system seems to be working using Gpredict and HamLib rotctld.
I'll try and tackle the SatPC32 interface later.
The code is attached as TMC5272_TMC_API_rsc01052025a.zip
Thanks to everyone who helped me get this project working.......



Attachments

GimbalBase30Nov2024.stl

Gimbalop4dec2024.stl

Gimbatop4dec2024.stl

TMC5272_TMC_API_rsc01052025a.zip

Category : project