Satellite Pointer/Tracker


I’m a long-time space buff and satellites amuse me no end. I will always go out for a space station flyover and I’ve eagerly watched for other satellite passes and iridium flares from Heavens-Above. I had no luck with either so I was excited when I saw people building space station pointers figuring I could use something similar to point to satellites and flares more generally.
So, six months later, I have a prototype that serves me well. I can usually spot five or six satellites in an evening and Iridium flares are dead easy.


The heart of the hardware is a couple of hobby servos. The Azimuth servo pans left/right with zero degrees being North and the Altitude servo tilts up and down with zero degrees being the horizon in front,90 degrees being straight up , and 180 degrees being the horizon behind. The azimuth servo is made by futaba and sold by Parallax and has a full 180 degree swing. The Altitude servo is a hitec HS-55 which is lightweight and has a range of about 155 degrees. The combination lets me point anywhere from about 12 degrees above the horizon in any direction. Attached to the altitude servo is a plastic pen barrel with a couple of SMD leds threaded into it. These are powered steadily just to let me see where the pointer is.

There’s also a SparkFun DeadOn real time clock using a DS3234 chip coupled to the Modern Electronics Really Bare Bones Board which runs the show.

The heart of the Software is a library called Plan 13 credited to VE9QRP based on an ancient BBC Basic program written by James Miller. I also use the TimeDate library, Variable Speed Servo library, the Streaming macros and a bunch of the standard ones. Much thanks to the authors.

The prototype runs attached to my laptop which is providing power and monitoring. The laptop also prepares the observing plan using Python scripts to download the data from the Heavens-Above web site – much thanks to them as well.

I haven’t bothered with a schematic because it’s just breadboard connections and a single power supply. I'll post he sketch but it’s my usual, over-thought, under-documented codestravaganza.

The trickiest thing it seemed to me was translating the altitude and azimuth directions given by the plan13 software into pulse widths the servos could eat. I had to worry about the physical limits of the servos and the corresponding pulse widths and switching altitude calculations when the azimuth is “behind” the servos – for example, when the calculation says the satellite is due south and 30 degrees up, I have to point the azimuth server north and run the altitude servo to 180-30 degrees. Add to that the fact that servo makers don’t publish much in the way of specs and the fact that the Futaba servers run backwards compared to the hitec and it’s just a LOT of fun to figure out.

One neat thing in the code is that I started with a command loop driven program where I could put in single character commands like ‘x’ to move the servo to maximum, ‘n’ for minimum. When I needed to get numeric parameters in I cobbled up a little postfix parser so I could put in things like 0,z 30,a to point to 0 azimuth 30 altitude or 18,30,0 19,9,30 l to set the time to 6:30 pm. No error checking of course but really handy in development.

The data to drive a night’s observing session comes from the Heavens-Above web site and is in two parts: There’s an observation plan that lists start and stop time for the satellite passes along with the satellite id and a series of NORAD TLE(two line element) records that define the satellite orbits. Both of these sets of data are compiled into the program as raw ascii and stored in progmem where the sketch peels them out and converts the times and parameters to numeric values. I could have compressed the data and saved the conversion but I want to keep things as transparent as possible. The overall sketch with TLE’s is bulging near the atmega328’s 30K limit though so I may have to compress the data or offload it to an SD card. On the other hand, I have a sanguino somewhere so maybe that…

When it's running autonomously the program picks the first line out of the plan something like "19:44:19 19:55:33 31793" //SL-16 2.7. It waits for the start time then picks out the satellite number ( 31793) and goes through the TLE's looking for it. It loads the orbit data into Plan13 then goes into an update loop where it calls the Plan13 code and moves the servos to point to the location. The pointing is surprisingly close and works very well. One "trick" i used was to ask plan13 for the position where the satellite would be three seconds ahead and point to there before delaying.

I’ve downloaded a couple of iPhone apps that do similar things and they’re cute but I’m still happy with my effort and I plan to continue poking at it. I need a power supply and display separate from my laptop and some sort of box for transport. I’m thinking of a nice wooden box with either drop sides or a flippable lid so the servo mechanism can sit on top. I’d like to integrate a compass, preferably electronic but I’m not sure about accuracy – my iphone compass sucks. I have a 5mw green laser on order and I’ll try a live pointer but the laser does make me nervous. I have an atmel butterfly board somewhere that could maybe be the display. On the software front, the accuracy and pointing are fine now and I need to work on command and control: keeping lists of objects other than satellites to point at, being able to interrupt a satellite track temporarily to point at something else etc.

Here's the sketch - posted in pieces per my tabs.

//Satellite pointer/tracker
#include <Time.h>
#include <SPI.h>
#include <DS3234RTC.h>
#include <avr/pgmspace.h>
#include <MemoryFree.h>
#include <String.h>
#define lcloffset (3600UL)*4 //summer offset from UCT
#include <Streaming.h>
#define cout Serial
#define endl '\n'
#include <Plan13.h>
 
Plan13 p13;  //declare and initialize the plan13 code

prog_uchar PROGMEM initstr[]=  //can be used to provide startup commands
//23456789+123456789+123456789+
" "; 
int initindx=0, initcnt=0;

double fparams[11]; byte  fpcnt=0; //holds the numeric parameters parsed out of the serial string
#define fplimit 11 //max number of parameters
char buff[16]="\0\0\0\0\0\0\0\0\0\0";  //serial buffer
int bufflen=0;
#define buffmax 15
char satnum[]="     "; //area for 5 character satellite number
void setup () {
  Serial.begin(115200);
  delay(1000);
  servoinit(); //initialize both servos
  cout <<"V13 - DS3234lib \n"; //just reminding myself what's i and out
  RTC.begin(9); //start the real time clock interface
  setSyncProvider(RTC.get);   // the function to get the time from the RTC
  if(timeStatus()!= timeSet) {
    Serial.println("RTC NG");
  }
  else{
    Serial.println("RTC OK");      
  }
  digitalClockDisplay(now());     cout<<endl;
  p13.setLocation(-75.517, 45.467, 0); // coordinates of my back yard
//  p13.setLocation(-75.31, 45.49, 0); // Eaglewoods golf in clarence creek
  setaltaz(0,0); to do a quick alignment with geographic north
  delay(10000);
  setaltaz(45,0); //point to Polaris - mag 2 at the end of the little dipper's handle.
  
}
void loop(){  //the command loop reads from serial or the init string, parses out numerics and dispatches routines
  char x;
  while((Serial.available()>0)||(initindx<initcnt)){  //see if there's anything in the initialization string or serial
    if (initindx<initcnt){
      x=pgm_read_byte_near(initstr+initindx);
      initindx++;
    }
    else {
      x=Serial.read();
    }
      switch(x){      
        case 'y': //set the realtime clock from the arduino clock.
          cout<<"sYnc\n";
          syncrtc();;
          break;
        case 'x': //execute the observation plan
          cout<<"X\n";
          execplan();
          break;
        case 'a': //set altitude for testing
          cout<<"A\n";
          stobuff(); //take any parameter left in the buffer
          setaltaz(fparams[0],lastaz);
          fpcnt=0; //clear the parameters
          break;
        case 'z': //set azimuth for testing
          cout<<"Z\n";
          stobuff(); //take any parameter left in the buffer
          setaltaz(lastalt,fparams[0]);
          fpcnt=0;
          break;
        case 'f': //find a tle in the list and put the index in fparams[0]
          cout<<"F\n";
          fparams[0]=findtle(); //this will use and clear the buffer
          if (fparams[0]!=0){  //if it's valid
            fpcnt=1; //accept it
          }
          break;
        case '?': //print status and varaiables
          cout<<"Mem "<<freeMemory()<<endl;
          prtstate();
          break;
        case 'c': //run a plan13 cycle needs l and e first
          cout<<"C";
          cycle13(now());
          break;
        case 'e': //set plan 13 elements
          cout<<"\nset Elem.\n";
          setelements();
          break;
        case 'p': //load parameters from a tle need to do a e afterwards to set the elements
          stobuff(); //take any parameter left in the buffer        
          cout<<"Params<-TLE "<<fparams[0]<<endl;
          fpcnt=0; //eat the tle #
          tle2params(fparams[0]);
          break;
        case ',': case ' ': case '/': // comma or whitespace convert the buffer to a parameter
          cout<<',';
          stobuff();
          break;
        case 'l': //save parameters as a local date and time
          cout<<"\n Local=";
          stobuff();
          setltime();
          break;
        case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': //digits and decimal point
          cout<<x;
          if (bufflen<buffmax){
            buff[bufflen]=x;
            bufflen++;
            buff[bufflen]='\0';
          }
          else {
            cout<<"\nbuff oflo@"<<x<<endl;
          }
          break;
        default:
          cout<<endl<<"inv:"<<x<<endl;
          break;  
    }
  }
}
//routines to deal with accumulating parameters from serial
 
void stobuff(){//take any digits from the buffer and store them as a parameter
  if (bufflen>0){ //if we're holding numbers for conversion
    if (fpcnt<fplimit){ //if there's room
      fpcnt+=1; //add a parameter
      fparams[fpcnt-1]=atof(buff); //convert the buffer to a float
    }
    else {
      cout <<endl<<"param oflo @ "<<buff<<endl;
    }
    bufflen=0;
    buff[0]='\0';
  }
}
prog_char PROGMEM obsplan[]={  //each line defines the start and end of a satellite pass and gives the satellite id
"19:44:19 19:55:33 31793" //SL-16 2.7
"20:20:11 20:31:11 25407" //Cosmos 2.1
"20:30:55 20:36:46 36089" //CZ-2C 3.0
"20:41:42 20:46:12 25391" //Tubsat 3.3
"21:08:41 21:17:49 27597" //ADEOS 2.4
"21:17:36 21:23:50 22566" //Cosmos 2.3
"21:53:55 21:58:52 28353" //SL-16 2.4
"22:00:24 22:05:25 22220" //Cosmos 2.3
};
#define numpasses 8
#define planyear 2011
#define planmonth 9
#define planday 18

//execute the observation plan
#define offhr 0 //offsets within the plan for hour
#define offmin 3 //min
#define offsec 6 //second
#define planlen 23 //length of a plan element
#define beginoffset 0 //offset to begin time
#define endoffset 9 //end tine
#define snpoffset 18 //satellite number offset
#define snplen 5 //satellite # length
void execplan(){ //execute the whole plan
  cout <<" exec plan "<<numpasses<<" passes\n";
  int passnum; //which pass we're on
  for (passnum=1;passnum<=numpasses;passnum++){
    execpass(passnum);
  }
  cout <<"plan done ";
  digitalClockDisplay(now());
  cout<<endl;
}

void execpass(int passnum) { //track a single satellite pass
  time_t beginpass=getbegintime(passnum);
  time_t endpass=getendtime(passnum);
  time_t ctime=now();
  cout <<" at "; digitalClockDisplay(now()); cout<<endl;
  cout <<" beginpass=";digitalClockDisplay(beginpass);cout<<endl;
  if (ctime<beginpass){
    cout <<" secs til start "<<beginpass-ctime<<endl;
  } else {
    cout <<" secs since start "<<ctime-beginpass<<endl;
  }
  cout <<" endpass=";digitalClockDisplay(endpass);cout<<endl;
  cout <<"length "<<endpass-beginpass<<endl;
  while(beginpass>now()){
    cout <<" secs til start "<<beginpass-now()<<endl;
    delay(15000);
  }
  cpyplan2buff(passnum,18,5); bufflen=5; //copy satellite id to buffer
  int foundtle=findtle(); //find the tle
  if (foundtle!=0){
    tle2params(foundtle); //stage the tle elements
    setelements();//                               to plan 13
    while (endpass>now()){
      cout <<"secs til pass end "<<endpass-now()<<endl;
      cycle13(now()+3);  //look ahead 3 seconds
      delay(3000); //and let reality catch up.
    }
  }
  else {
    cout <<"tle "<<buff<<" for plan entry #"<<passnum<<" not found \n";
    cout <<"skip\n";
  }
}   

time_t getbegintime(int passnum){ //get the time_t definition of the beginning of the pass
  return getpasstime(passnum, beginoffset); //point to begin time
}
time_t getendtime(int passnum){ //get the time_t definition of the beginning of the pass
  return getpasstime(passnum, endoffset); //point to end time
}
time_t getpasstime(int passnum, int timeoffset){ //pick up a formatted time from the specified offset in the specified pass record
  tmElements_t tm;          // time elements  for getting a time_t value
  tm.Year=planyear-1970; //set
  tm.Month=planmonth; //the easy
  tm.Day=planday; //things
  cpyplan2buff(passnum,timeoffset+offhr,2); 
  tm.Hour=atoi(buff); cout<<buff;
  cpyplan2buff(passnum,timeoffset+offmin,2);
  tm.Minute=atoi(buff);cout<<buff;
  cpyplan2buff(passnum,timeoffset+offsec,2);
  tm.Second=atoi(buff);cout<<buff<<endl;
  time_t temp=makeTime(tm)+lcloffset;
  return temp;
}

void cpyplan2buff(int n,int byteoffset,int len){ //copy n bytes out of tle n at offset given
  for (int i=0;i<len;i++){
    int passoffset=(n-1)*planlen; //1st byte of the pass pointed to
    char x=pgm_read_byte_near(obsplan+passoffset+byteoffset+i);
    buff[i]=x;
  }
  buff[len]='\0';
}

more code - kids, don't try this at home...

//Routines to interface with the Plan 13 library

void updtime(time_t t){ //set or update plan13's time from the parameter
  p13.setTime(year(t),month(t),day(t),hour(t),minute(t),second(t));
}
void setelements(){ //set the plan13 orbital elements
  p13.setElements(fparams[0],fparams[1],fparams[2],fparams[3],fparams[4],fparams[5],
                   fparams[6],fparams[7],fparams[8],fparams[9],fparams[10]);
  updtime(now());
  p13.initSat();
  fpcnt=0;
}

void cycle13(time_t when){ // run a plan 13 cycle
  updtime(when);
  p13.satvec();
  p13.rangevec();
  digitalClockDisplay(when);
  cout<<" AZ="<<p13.AZ<<" ALT="<<p13.EL<<endl;
  setaltaz(int(p13.EL+.5),int(p13.AZ+.5)); // rounded alt az
}
//two line elements are stored PROGMEM prog_uchar array tles
//23456789+123456789+123456789+123456789+123456789+123456789+123456789
prog_char PROGMEM tles[]={
"1 31793U 07029B   11260.12782331 -.00000001  00000-0  24514-4 0  2274"
"2 31793 070.9752 119.3719 0001888 267.5186 092.5714 14.14086103217800"
"1 25407U 98045B   11259.94296736 -.00000291 +00000-0 -12451-3 0 07277"
"2 25407 071.0084 134.2580 0007876 181.9965 178.1127 14.15544012678959"
"1 36089U 09061B   11259.73403693  .00000148  00000-0  49611-4 0  5618"
"2 36089 098.2453 308.2997 0084104 018.8975 341.5307 14.48573105 97504"
"1 25391U 98042C   11260.57814793  .00007762  00000-0  14131-3 0  2838"
"2 25391 078.9276 127.9292 0128209 222.2239 136.9070 15.40664767732124"
"1 27597U 02056A   11260.21763093  .00000157  00000-0  82866-4 0  5368"
"2 27597 098.3679 303.0927 0001784 055.3868 304.7481 14.26301238455969"
"1 22566U 93016B   11260.12337304 -.00000267  00000-0 -11384-3 0  3062"
"2 22566 071.0072 154.2698 0008387 091.0393 269.1712 14.14692990954243"
"1 28353U 04021B   11260.28898009 -.00000295  00000-0 -13017-3 0   211"
"2 28353 070.9977 161.5059 0004816 170.0950 190.0266 14.13954218375311"
"1 22220U 92076B   11259.94284285 -.00000214 +00000-0 -83949-4 0 05837"
"2 22220 070.9964 159.3696 0014849 236.9349 123.0349 14.16188382973403"
};
int numtles=8;

#define tlelen 69 //length of a tle line
#define yebegin 18 //offset to 1st byte of YE element epoch year
#define yelen 2 //length of YE in tle
#define tebegin 20 //offset to TE
#define telen 12
#define inbegin tlelen+8 //offset to INclination (in 2nd line)
#define inlen 8
#define rabegin tlelen+17 //offset to Right Ascension (in 2nd line)
#define ralen 8
#define ecbegin tlelen+26 //offset to eccentricity (in 2nd line)
#define eclen 7
#define wpbegin tlelen+34 //offset to WP - argument of perigee (in 2nd line)
#define wplen 8
#define mabegin tlelen+43 //offset to mean anomaly (in 2nd line)
#define malen 8
#define mmbegin tlelen+52 //offset to mean motion (in 2nd line)
#define mmlen 11
#define m2begin 33 //offset to 1st derivative of mm/2 (used in drag calc)
#define m2len 10
#define snbegin 2 //satellite designator
#define snlen 5
int findtle(){ //find a tle in the list with satellite # the same as what's in the buffer
  int i, foundtle=0;
  cout <<"looking for tle "<<buff<<endl;
  if (bufflen>= 4&& bufflen<6){ //if length looks ok
    strcpy(satnum,buff); //copy the buffer to the satellite #
    for (i=1;i<=numtles;i++){ //cycle thru the tles  
      tle2buff(i,snbegin,snlen); //get the satellite number
//      cout<<" considering "<<i<<": "<<buff<<endl;
      if (strcmp(satnum,buff)==0){ //if it matches
        foundtle=i; //note the #
      }
    }
  }
  else{
    cout <<"inv. sat. @ "<<buff<<endl;
  }
  if (foundtle==0){
    cout <<"tle "<<satnum<<" not found\n";
  }
  bufflen=0;
  cout <<"ret. "<<foundtle<<endl;
  return foundtle;
  }
    
void tle2params(int n){ //set the parameters from tle n
  tle2buff(n,snbegin,snlen);
  cout<<"satno "<<buff<<endl;
  fparams[0]=tle2ye(n);fparams[1]=tle2te(n);fparams[2]=tle2in(n);fparams[3]=tle2ra(n);fparams[4]=tle2ec(n);fparams[5]=tle2wp(n);
  fparams[6]=tle2ma(n);fparams[7]=tle2mm(n);fparams[8]=0;fparams[9]=0;fparams[10]=180; //phonies out drag factor, orbit # and stellite orientation
  fpcnt=11; //11 parameters loaded
//from setElements(double YE_in, double TE_in, double IN_in, 
//                 double RA_in, double EC_in, double WP_in, 
//                 double MA_in, double MM_in, double M2_in, 
//                 double RV_in, double ALON_in );
}

void tle2buff(int n, int byteoffset,int len){ //copy n bytes out of tle n at offset given
  for (int i=0;i<len;i++){
    int tleoffset=(n-1)*tlelen*2; //1st byte of the 1st line of the tle pointed to
    char x=pgm_read_byte_near(tles+tleoffset+byteoffset+i);
    buff[i]=x;
  }
  buff[len]='\0';
}
double tle2ye(int n){ //return epoch year of tle n
  int len=yelen, offst=yebegin;
  tle2buff(n,offst,len);
  return 2000.0+atof(buff);
}
double tle2te(int n){ //return epoch time of tle n
  int len=telen, offst=tebegin;
  tle2buff(n,offst,len);
  return atof(buff);
}
double tle2in(int n){ //return inclination from tle n
  int len=inlen, offst=inbegin;
  tle2buff(n,offst,len);
  return atof(buff);
}
double tle2ra(int n){ //return right ascension from tle n
  int len=ralen, offst=rabegin;
  tle2buff(n,offst,len);
  return atof(buff);
}
double tle2wp(int n){ //return argument of perigee from tle n
  int len=wplen, offst=wpbegin;
  tle2buff(n,offst,len);
  return atof(buff);
}
double tle2ma(int n){ //return mean anomaly from tle n
  int len=malen, offst=mabegin;
  tle2buff(n,offst,len);
  return atof(buff);
}
double tle2mm(int n){ //return mean motion from tle n
  int len=mmlen, offst=mmbegin;
  tle2buff(n,offst,len);
  return atof(buff);
}
double tle2ec(int n){ //return eccentricity from tle n
  int i=0,len=eclen, offst=(n-1)*2*tlelen+ecbegin;
  buff[0]='0';buff[1]='.'; //make it look floaty
  for (i=0;i<len;i++){
    char x=pgm_read_byte_near(tles+offst+i);
    buff[i+2]=x;
  }
  buff[len+2]='\0';
  return atof(buff);
}
void setltime(){ //set time to hour min sec day month year
  if (fpcnt<4){ //if some params are missing in fill from the plan
    fparams[3]=planday; fparams[4]=planmonth; fparams[5]=planyear-2000; 
    if (fpcnt<3){ //zero the seconds if not supplied
      fparams[2]=0;
    }
  }
  setTime(fparams[0],fparams[1],fparams[2],fparams[3],fparams[4],fparams[5]); // another way to set the time setTime(hr,min,sec,day,month,yr)
  digitalClockDisplay(now());    
  fpcnt=0; //eat the parameters
  setTime(now()+lcloffset);
  cout<<"\n UCT: ";
  digitalClockDisplay(now());    
  cout<<endl;
}

void syncrtc(){ //set the RTC to the millis() clock
  RTC.set(now());   // set the RTC to the system time
  cout <<"RTC set\n";
}
//debug routines 
void prtstate(){
  cout<<endl<<" fpcnt="<<_DEC(fpcnt)<<endl;
  for (int j=1;j<=fpcnt;j++){
    cout<<fparams[j]<<" ";
  }
  cout<<"\n UCT:";
  digitalClockDisplay(now());      
}

void digitalClockDisplay(time_t when){
  // digital clock display of the time
  Serial.print(hour(when));
  printDigits(minute(when));
  printDigits(second(when));
  Serial.print(" ");
  Serial.print(day(when));
  Serial.print(" ");
  Serial.print(month(when));
  Serial.print(" ");
  Serial.print(year(when)); 
//  Serial.println(); 
}

void printDigits(int digits){
  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

Amazed again what can be done with the Arduino! A+

nice job!

watch out not to point the laser at an airplane above you :fearful:

martin_bg:
nice job!

watch out not to point the laser at an airplane above you :fearful:

yes, or at me! I am going to try having the laser controlled by a momentary pushbutton rather than on constantly. I'll see how useful it is. It's currently on a slow-boat from asia.

Impressive idea and result.

Great work! A little video of it in action would be cool. :smiley:

robtillaart:
Amazed again what can be done with the Arduino! A+

Me too !!! :slight_smile:

maidbloke:
Great work! A little video of it in action would be cool. :smiley:

+1!

I have been collecting parts for a radio telescope. You have inspired me to have it internet/Arduino controlled!

This is right up my alley. Excellent gizmo.

Thanks for sharing,
Nicholas

it would have to be a really small radio telescope!

maybe part of the StETI effort (Search for tiny Extraterrestrial Intelligence), or short wave (get it? short wave?)

Hi! If this project is still active, I can't seem to get it to work in Arduino1.6. Can anyone confirm that it still works for them?

(If it works for you, any chance a .zip of the files, or a pointer to a github repository, could be made available?

Thanks!

Neat project! Back in the day I use to run David Ransom's STS software, then run outside to see if i could spot the particular object going over head. It was amazingly accurate. Sadly I don't think it will run on windows 10.

The code includes a library called DS3234RTC.h.
Anyone know where to get this library?

Pete