Solved the meridian flip problem


#1

Hello everyone. It’s time to upgrade the rotors!
A few days ago I contacted the Engineer Francesco Bandini, (IK5XWA) representing the problem of the meridian flip treated in this post https://community.libre.space/t/the-meridian-flip-fix-for-arduino-software- repositioning-rotator/2702
the problem is present on all the rotors’ firmware, both Stepper Motor and DC motor. It 's a big problem because in many cases during the satellite tracking, the rotor relocating loses for a few tens of seconds tracking.
Thanks to Francesco, who had already solved the problem with the firmware of his rotor https://www.youtube.com/watch?v=MNwRFbsHNNY&t=282s, after having sent the firmware for stepper motor Satnogs, analyzing it has implemented the code that solves the MERIDIAN FLIP. I tested it and it seems to work perfectly, the rotor does not reposition itself and continues tracking until the satellite is lost on the horizon.
He added some code in the cmd_proc routine, from line 206 onwards.

//----------------------------------------------------------
// detect any 360-0 or 0-360 crossing to avoid Meridian Flip
//----------------------------------------------------------
double eps; // differenza in gradi tra posiz. voluta e attuale
double az_val = angleAz; // posizione voluta in gradi
double az_att = step2deg(AZstepper.currentPosition()); // posizione attuale in gradi

	  // da qui si presuppone che le posizioni siano sempre tra 0 e 360, quindi per sicurezza le riporta nel range 0-360
	  int n_giri=( int ) (az_att / 360.0);   // esempio con az_att=357 -> ngiri = 0
      az_att = az_att - ((double) n_giri)*360.;  // azimuth attuale, riportato tra 0 e 360 gradi
	  
	  n_giri=( int ) (az_val / 360.0);
	  az_val = az_val - ((double) n_giri)*360.;  // azimuth voluto, riportato tra 0 e 360 gradi
	  
      eps = az_val - az_att;     // calcola errore di posizione

      if (fabs(eps) > 180.)      // deve fare piu' di mezzo giro (quindi deve attraversare il nord)
      {
         if (az_att < 180)       // lo attraversa girando in senso antiorario
         {
           // esempio: deve andare da +10 a +350, eps diventa (350 - 360) -10 = -20 e gira al contrario 
           eps = (az_val - 360) - az_att; 
         }
         else    // az_att > 180
         {
           // ha passato il nord girando in senso orario (esempio az_att = 350 e az_val = 10)
           eps = az_val -(az_att - 360);   // 10 - (350 - 360) = +20 e continua in senso orario
         }      // end if az_att > 180
      }   // end if fabs(eps) > 180

      // ridefinisce la posizione voluta in step
      stepAz = AZstepper.currentPosition() + deg2step(eps);	  
	  //------------------------------------------------------
	  // end of meridian flip detection code
	  //------------------------------------------------------

HERE IS THE ENTIRE CODE FOR STEPPER MOTOR ARDUINO PRO MICRO AND 54 GEAR RATIO

#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <AccelStepper.h>

#define DIR_AZ 18 //PIN for Azimuth Direction
#define STEP_AZ 10 //PIN for Azimuth Steps
#define DIR_EL 6 //PIN for Elevation Direction
#define STEP_EL 7 //PIN for Elevation Steps

#define MS1 9 //PIN for step size
#define EN 8 //PIN for Enable or Disable Stepper Motors

#define SPR 200 //Step Per Revolution
#define RATIO 54 //Gear ratio
#define T_DELAY 60000 //Time to disable the motors in millisecond

#define HOME_AZ 4 //Homing switch for Azimuth
#define HOME_EL 5 //Homing switch for Elevation
/*The MAX_ANGLE depends of ANGLE_SCANNING_MULT and maybe misbehave for large values*/
#define ANGLE_SCANNING_MULT 180 //Angle scanning multiplier
#define MAX_AZ_ANGLE 360 //Maximum Angle of Azimuth for homing scanning
#define MAX_EL_ANGLE 360 //Maximum Angle of Elevation for homing scanning

#define HOME_DELAY 6000 //Time for homing Decceleration in millisecond

/*Global Variables*/
unsigned long t_DIS = 0; //time to disable the Motors
/*Define a stepper and the pins it will use*/
AccelStepper AZstepper(1, STEP_AZ, DIR_AZ);
AccelStepper ELstepper(1, STEP_EL, DIR_EL);

void setup()
{  
  /*Change these to suit your stepper if you want*/
  AZstepper.setMaxSpeed(150);
  AZstepper.setAcceleration(50);
  
  /*Change these to suit your stepper if you want*/
  ELstepper.setMaxSpeed(150);
  ELstepper.setAcceleration(50);
  
  /*Enable Motors*/
  pinMode(EN, OUTPUT);
  digitalWrite(EN, LOW);
  /*Step size*/
  pinMode(MS1, OUTPUT);
  digitalWrite(MS1, LOW); //Full step
  /*Homing switch*/
  pinMode(HOME_AZ, INPUT);
  pinMode(HOME_EL, INPUT);
  /*Serial Communication*/
  Serial.begin(19200);
  /*Initial Homing*/
  Homing(deg2step(-ANGLE_SCANNING_MULT), deg2step(-ANGLE_SCANNING_MULT));
}

void loop()
{ 
  /*Define the steps*/
  static int AZstep = 0;
  static int ELstep = 0;
  /*Time Check*/
  if (t_DIS == 0)
    t_DIS = millis();

  /*Disable Motors*/
  if (AZstep == AZstepper.currentPosition() && ELstep == ELstepper.currentPosition() && millis()-t_DIS > T_DELAY)
  {
    digitalWrite(EN, HIGH);
  }
  /*Enable Motors*/
  else
    digitalWrite(EN, LOW);
    
  /*Read the steps from serial*/
  cmd_proc(AZstep, ELstep);
  /*Move the Azimuth & Elevation Motor*/
  stepper_move(AZstep, ELstep);
}

/*Homing Function*/
void Homing(int AZsteps, int ELsteps)
{
  int value_Home_AZ = HIGH;
  int value_Home_EL = HIGH;
  int n_AZ = 1; //Times that AZ angle has changed
  int n_EL = 1; //Times that EL angle has changed
  boolean isHome_AZ = false;
  boolean isHome_EL = false;
  
  AZstepper.moveTo(AZsteps);
  ELstepper.moveTo(ELsteps);
  
  while(isHome_AZ == false || isHome_EL == false)
  {
    value_Home_AZ = digitalRead(HOME_AZ);
    value_Home_EL = digitalRead(HOME_EL);
    /* Change to LOW according to Home sensor */
    if (value_Home_AZ == HIGH)
    {
      AZstepper.moveTo(AZstepper.currentPosition());
      isHome_AZ = true;
    }   
    /* Change to LOW according to Home sensor */
    if (value_Home_EL == HIGH)
    {
      ELstepper.moveTo(ELstepper.currentPosition());
      isHome_EL = true;
    }
    if (AZstepper.distanceToGo() == 0 && !isHome_AZ)
    {
      n_AZ++;
      AZsteps = deg2step(pow(-1,n_AZ)*n_AZ*ANGLE_SCANNING_MULT);
      if (abs(n_AZ*ANGLE_SCANNING_MULT) > MAX_AZ_ANGLE)
      {
        error(0);
        break;
      }
      AZstepper.moveTo(AZsteps);
    } 
    if (ELstepper.distanceToGo() == 0 && !isHome_EL)
    { 
      n_EL++;
      ELsteps = deg2step(pow(-1,n_EL)*n_EL*ANGLE_SCANNING_MULT);
      if (abs(n_EL*ANGLE_SCANNING_MULT) > MAX_EL_ANGLE)
      {
        error(1);
        break;
      }
      ELstepper.moveTo(ELsteps);
    }
    
    AZstepper.run();
    ELstepper.run();
  }
  /*Delay to Deccelerate*/
  long time = millis();  
  while(millis() - time < HOME_DELAY)
  {  
    AZstepper.run();
    ELstepper.run();
  }
  /*Reset the steps*/
  AZstepper.setCurrentPosition(0);
  ELstepper.setCurrentPosition(0); 
}
 
/*EasyComm 2 Protocol & Calculate the steps*/
void cmd_proc(int &stepAz, int &stepEl)
{
  /*Serial*/
  char buffer[256];
  char incomingByte;
  char *p=buffer;
  char *str;
  static int counter=0;
  char data[100];
  
  double angleAz,angleEl;
  
  /*Read from serial*/
  while (Serial.available() > 0)
  {
    incomingByte = Serial.read();
    /* XXX: Get position using custom and test code */
    if (incomingByte == '!')
    {
      /* Get position */
      Serial.print("TM");
      Serial.print(1);
      Serial.print(" ");
      Serial.print("AZ");
      Serial.print(10*step2deg(AZstepper.currentPosition()), 1);
      Serial.print(" ");
      Serial.print("EL");
      Serial.println(10*step2deg(ELstepper.currentPosition()), 1);
    }
    /*new data*/
    else if (incomingByte == '\n')
    {
      p = buffer;
      buffer[counter] = 0;
      if (buffer[0] == 'A' && buffer[1] == 'Z')
      {
        if (buffer[2] == ' ' && buffer[3] == 'E' && buffer[4] == 'L')
        {
          /* Get position */
          Serial.print("AZ");
          Serial.print(step2deg(AZstepper.currentPosition()), 1);
          Serial.print(" ");
          Serial.print("EL");
          Serial.print(step2deg(ELstepper.currentPosition()), 1);
          Serial.println(" ");
        }
        else
        {
          /*Get the absolute value of angle*/
          str = strtok_r(p, " " , &p);
          strncpy(data, str+2, 10);
          angleAz = atof(data);
          /*Calculate the steps*/
          stepAz = deg2step(angleAz);
		  
		  //----------------------------------------------------------
		  // detect any 360-0 or 0-360 crossing to avoid Meridian Flip
		  //----------------------------------------------------------
          double eps;     // differenza in gradi tra posiz. voluta e attuale
		  double az_val = angleAz;		// posizione voluta in gradi
		  double az_att = step2deg(AZstepper.currentPosition());	// posizione attuale in gradi
		  
		  // da qui si presuppone che le posizioni siano sempre tra 0 e 360, quindi per sicurezza le riporta nel range 0-360
		  int n_giri=( int ) (az_att / 360.0);   // esempio con az_att=357 -> ngiri = 0
	      az_att = az_att - ((double) n_giri)*360.;  // azimuth attuale, riportato tra 0 e 360 gradi
		  
		  n_giri=( int ) (az_val / 360.0);
		  az_val = az_val - ((double) n_giri)*360.;  // azimuth voluto, riportato tra 0 e 360 gradi
		  
          eps = az_val - az_att;     // calcola errore di posizione
  
          if (fabs(eps) > 180.)      // deve fare piu' di mezzo giro (quindi deve attraversare il nord)
          {
             if (az_att < 180)       // lo attraversa girando in senso antiorario
             {
               // esempio: deve andare da +10 a +350, eps diventa (350 - 360) -10 = -20 e gira al contrario 
               eps = (az_val - 360) - az_att; 
             }
             else    // az_att > 180
             {
               // ha passato il nord girando in senso orario (esempio az_att = 350 e az_val = 10)
               eps = az_val -(az_att - 360);   // 10 - (350 - 360) = +20 e continua in senso orario
             }      // end if az_att > 180
          }   // end if fabs(eps) > 180

          // ridefinisce la posizione voluta in step
          stepAz = AZstepper.currentPosition() + deg2step(eps);	  
		  //------------------------------------------------------
		  // end of meridian flip detection code
		  //------------------------------------------------------

          /*Get the absolute value of angle*/
          str = strtok_r(p, " " , &p);
          if (str[0] == 'E' && str[1] == 'L')
          {
            strncpy(data, str+2, 10);
            angleEl = atof(data);
            /*Calculate the steps*/
            stepEl = deg2step(angleEl);
          }
        }
      }
      /* Stop Moving */
      else if (buffer[0] == 'S' && buffer[1] == 'A' && buffer[2] == ' ' && buffer[3] == 'S' && buffer[4] == 'E')
      {
        /* Get position */
        Serial.print("AZ");
        Serial.print(step2deg(AZstepper.currentPosition()), 1);
        Serial.print(" ");
        Serial.print("EL");
        Serial.println(step2deg(ELstepper.currentPosition()), 1);
        stepAz = AZstepper.currentPosition();
        stepEl = ELstepper.currentPosition();
      }
      /* Reset the rotator */
      else if (buffer[0] == 'R' && buffer[1] == 'E' && buffer[2] == 'S' && buffer[3] == 'E' && buffer[4] == 'T')
      {
        /* Get position */
        Serial.print("AZ");
        Serial.print(step2deg(AZstepper.currentPosition()), 1);
        Serial.print(" ");
        Serial.print("EL");
        Serial.println(step2deg(ELstepper.currentPosition()), 1);
        /*Move the steppers to initial position*/
        Homing(0,0);
        /*Zero the steps*/
        stepAz = 0;
        stepEl = 0;
      }      
      counter = 0;
      /*Reset the disable motor time*/
      t_DIS = 0;
    }
    /*Fill the buffer with incoming data*/
    else {
      buffer[counter] = incomingByte;
      counter++;
    }
  }
}

/*Error Handling*/
void error(int num_error)
{
  switch (num_error)
  {
    /*Azimuth error*/
    case (0):
      while(1)
      {
        Serial.println("AL001");
        delay(100);
      }
    /*Elevation error*/
    case (1):
      while(1)
      {
        Serial.println("AL002");
        delay(100);
      }
    default:
      while(1)
      {
        Serial.println("AL000");
        delay(100);
      }
  }
}

/*Send pulses to stepper motor drivers*/
void stepper_move(int stepAz, int stepEl)
{
  AZstepper.moveTo(stepAz);
  ELstepper.moveTo(stepEl);
    
  AZstepper.run();
  ELstepper.run();
}

/*Convert degrees to steps*/
int deg2step(double deg)
{
  return(RATIO*SPR*deg/360);
}

/*Convert steps to degrees*/
double step2deg(int Step)
{
  return(360.00*Step/(SPR*RATIO));
}

SatNOGS Rotator Unwinder
#2

Although I am not good in italian I can somehow read that code (and I also know where and what the problem actually is). Could you please translate the comments for not losing context?
It would also be great to have the code-tags set correctly and indented code lines :wink:

It would be great to have this implemented in other rotators, too, but I think there might be differences across the models. My G-5400 has that flip at +/-180°…

There’s a sample implementation also available in gpredict (see gtk-rot-ctrl.c) which works perfect with my setup. Maybe it would be better to implement that functionality at client side (to make it configurable).

Thanks for sharing this!


#3

@DL4PD I agree !!!

btw, there is an open issue in rotator firmware, @iz5rzr, it would be nice to make a merge request about it. :slight_smile:


#4

Done.
The stepper motor rotator work perfectly and not nedd any implementation on Gpredict also with this mod you can use other tracking software like HRD deluxe
I do not try for now the dc motor or other rotators

the code is translate


#5

HI,
We are also using a G-5500 with az. from -180 to +180deg (with stop in south direction). So far, for any pass crossing the +/-180 deg boundary we have this flip problem, since elevation is managed by SW (gPredict) from 0 to 90deg instead of 0 to 180.
I was also thinking to take benefit of the 180deg angle in elevation to avoid the flip, but without knowing a-priori if the south is crossed or not, I cannot implement any workaround.
Am I missing something? Is there another way to do this? Really, I had no time to think in detail about this.

Alberto


#6

I’ll have to look into my config, but one of them works for me!


#7

It would be good to understand if there is already some feature somewhere… Just to better explain, this is a typical pass that cause a problem:

https://network.satnogs.org/observations/423527/

The satellite is crossing the south. So what I get, is that elevation start from 0 goes up to max (46) and then back to zero. Azimuth is going from 224 to 63 passing through 180deg. At this point the rotor does a 360 deg turn, to pass from 180(west) to 180(east) and then it continue to 63.
The best way to implement this (knowing it pass in south direction) would be to start at an elevation of 180deg, with azimut of 224-180=44deg and then decrease elevation to 180-46=134 and then back to 180, while decreasing the azimuth from the initial 44deg to 243deg, passing through the north (the 360deg jump in angles is managed by the rotor itself without discontinuity).
But I can only implement this if I know at the beginning whether the pass is via north or south (to implement the 180deg swap of angles).

May be a bit tricky…

Alberto


#8

You were talking about gpredict and it does exactly that! The problem on a satnogs node is: the prediction algorithm reports up to 90⁰ elevation, because it does not know anything about a rotator. It predicts passes from 0 to 360 degrees azimuth and 0 to 90 degrees elevation. If someone would implement some checks this prediction can be “modified” to support a rotator flip instead of turning into the opposite position.


#9

Ok… This was a point I missed. I thought SatNogs is relying on gpredict to compute rotor position. So, this is why I was a bit confused.
Thanks. I will do a test using gPredict to track and see how it works.

Alberto


#10

The modification published on the arduino stepper motor code solves the “MERIDIAN FLIP” for any passage. Of course, the code I published was updated for my version of rotor which is a Satnogs Rotator V3 with pcb V1.2 and which mounts an arduino pro micro and then the firmware on arduino related to the stepper motor.
To me it solves the Meridian Flip perfectly and the rotor tracks all the satellites in any direction regardless of the tracking software (Gpredict, HRD or other).
It has nothing to do with the Satnogs Client as it is an Arduino that controls the rotor, its firmware (stepper_motor_controller.ino) or the DC Motor.
For other types of rotor I do not know what to say.
The fact is this, no one to date had solved the problem and with this mod is resolved.
Now if the part of the code from line 206 up to the end of the routine is implemented in the other versions of software for rotor control SatNOGS eliminates the problem and all the satellites will be tracked without repositioning.
I repeat, this fix is only for those who have the SatNOGS Rotator, but analyzing this fix maybe someone more 'expert in the field can implement it on other rotors or software.


#11

Yeah, don’t get me wrong: nice work! It’s one way to go, but it’s not correct that there has never before been a solution :wink:
Simply look into gpredict sources.
My point was: if we fix that in the satnogs-client, we will immediately support all rotators with a single change. If we fix it in the rotator controller firmware the rotator is immediately capable of doing this for any software driving it, but the changes have to be made to several firmwares (and it will be impossible for closed ones).

Nevertheless: your approach is one of those two ways and a good one :slight_smile:


#12

Hi
In reality, the problem is also common on astrophils apparatuses and there are systems that obviate this.I do not know the Yaesu management system, which among other things I use the yaesu GS-232 protocol.Having published the fix can be useful, in addition to having in the meantime arranged the SatNOGS rotator (which must be adapted in the C code of arduino depending on the firmware for the various PCB versions), and analyzing the solution may also be adopted for other types of rotor.In fact, I have already adapted this fix for the Satnogs Rotator and for the Satnogs Rotator controlled by the UNO arduino as per the IK5XWA project (yaesu G-232 protocol).


#13

Hi, I re-checked the SW, and I think this is not exactly my problem. I think I will re post somewhere else my issue.
If I am not wrong, the SW detect when I have to cross the 0-360 boundary and compute the rotation in the right direction.
I solved this in my interface between PC and G5500 (may be I will publish it in case someone will need something similar), just adding 180deg and doing mod(360) to the positions (both actual and set). So that angles goes from 0 (south) to 180 (north) to 360(south). Since I cannot cross the south because of mechanical stop, I will always turn between 0 and 360 without problem.
But, as said above, the problem is on elevation which is not managed up to 180deg by satnogs (while others, like gPredict or WxTrack do).

Tnx for all info!
Alberto