Table of contents
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.
Project
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:
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.
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.
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.
The pinout for the Arduino micro and TMC5272 is as follows:
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:
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.
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.
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
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:
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.
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.stlGimbalop4dec2024.stl
Gimbatop4dec2024.stl
TMC5272_TMC_API_rsc01052025a.zip