Sunday, October 31, 2021

Arduino Antenna Rotator for rotctld/gpredict on Linux

 I have an unused TV aerial rotator and would be good to put it, along with an motorized elevation control, for some satellite work. 

Googling around for some Arduino controller code I found from fellow ham YO3RAK at: https://racov.ro/index.php/2020/12/09/arduino-based-antenna-rotator-part3-software-tracking-update/
and:
https://create.arduino.cc/projecthub/viorelracoviteanu/antenna-rotator-controller-compatible-with-tracking-software-48f9cd#code
a nice bit of a project.

Compiled it along with some LCD library changes and noted that was not fully working with rotctld (Hamlib 1.2.15.3) and gredict (1.3) on Linux. Long story short I ended up starting my own code, half way trough discovered what looks like a different implementation or EasyComm II protocol on hamlib in Linux depending on version.

At some point trough my code I reverted to do changes on YO3RAK code to accommodate my setup, a mix of hamlib 1.2.15.3 and 3.1 along with gpredict 1.3 and 2.0-4. Now works fine on both implementations in Linux.

Since I've seen some posts with similar issues here's what I got while testing regarding behaviour:

hamlib 1.2.15:

Query position: "!"
Return: "TM0 AZ<AZIM>0.0 EL<ELEVATION>0.0"
See "0.0" appended, that sends 10x more angle, else gpredict will receive angle/10

hamlib 3.1:

Query position: "AZ EL ."
Return: "AZ<AZIM>.0 EL<ELEVATION>.0"
Here only appending the decimal point and works correctly on gpredict 2.0-4.

To start the process, besides having the the Arduino rotator controller you need to  start rotctld similar to:
rotctld -m 202 -r /dev/ttyUSB0 -s 9600 -T 192.168.0.14 -t 4533

where 192.168.0.14 will be your computer IP address, rotctld will listen on port 4533 for rotator commands from gpredict. /dev/ttyUSB0 will be the virtual USB port of the Arduino/rotator controller device. So, in simple terms, rotctld connects to the Arduino and listens on IP commands that gpredict sends to control the rotator.

Some images of gpredict and rotctld in action along wiht the Arduino code:

 







 To configure gpredict will be similar to this:



To connect to the rotator, this way:


I still don't have the hardware, will update when done.

The code bellow, since blogspot breaks a bit on the formating send me and email if you need a file with the code, file is: original_ant_rotator_easycom2_aug2021_mod_ct2gqv_v3.ino

/// code start

/*  AZ/EL Antenna Rotator controller for Arduino - DC motors
 *  ========================================================
 *  Uses EasyComm protocol for computer - Tracking Software
 *  Manual command by means of two rotary encoders AZ - EL
 * 
 *  Viorel Racoviteannu
 *  https://www.youtube.com/channel/UCiRLZX0bV9rS04BGAyUf-fA
 *  https://racov.ro
 *  YO3RAK@gmail.com
 * 
 * I cannot take any responsibility for missuse of this code
 * or any kind of damage it may occur from using this code.
 *
 * dec 2020 v2 - improved serial comm stability
 * january 2021 - improved near target dead-zone, for which antenna won't move
 * apr 2021 - improved serial comm stability
 * jun 2021 - error proportional power for tracking movement
 * aug 2021 - faster USB update, cold switching Az/El direction, small optimizations in the code
 *
 * Mods by CT2GQV bellow
 * 2021-10-31 added code to accept AZ EL as query since Hamlib 3.1 sends it, while Hamlib 1.2.15.3 sends the "!"
 * 2021-10-30 Original code moded to acept the "!" from Hamlib 1.2.15.3  using gpredict in 1.3 Linux
 * to include parsing of rotctld commands from Linux via gpredict.
 *  gpredict will connect to "rotator" (rotctld) with IP 192.168.0.14 (example)
 *   and rotctld will listen to IP 192.168.0.14 and send commands for the arduino rotator
 *   controler (from YO3RAK with mod CT2GQV) *   via serial USB
 *   ex: rotctld -m 202 -r /dev/ttyUSB0 -s 9600 -T 192.168.0.14 -t 4533.
 *   ex: rotctld -m 202 -r /dev/<arduino usb> -s 9600 -T <your IP address to listen> -t <port to listen>
 *
 *
 */
 
#include <Wire.h> // Library for I2C communication
#include <LiquidCrystal_I2C.h> // Library for LCD
// Wiring: SDA pin is connected to A4 and SCL pin to A5.
// Connect to LCD via I2C, default address 0x27 (A0-A2 not jumpered)
// LiquidCrystal_I2C lcd(0x27, 16, 2); // address, chars, rows.

// CT2GQV mod
LiquidCrystal_I2C lcd(0x27,2,1,0,4,5,6,7); // 0x27 is the default I2C bus address (this could be different for some modules)
// how much serial data we expect from rotctld before a newline
const unsigned int MAX_INPUT = 14;  // "AZ360.0 EL90.0", that is: 14 char's
int soft_az,soft_el ; // to hold the azimuth and elevation values sent from the gpredict via rotctld software
// end mod CT2GQV

// declaring custom symbol for up/down arrow
 byte DownArrow[8] = {
  B00000,
  B00100,
  B00100,
  B00100,
  B10101,
  B01110,
  B00100,
  B00000
};
 byte UpArrow[8] = {
  B00000,
  B00100,
  B01110,
  B10101,
  B00100,
  B00100,
  B00100,
  B00000
};

/***********************************THIS IS WHERE YOU REALY TWEAK THE ANTENNA MOVEMENT***************/
// ANTENNA potentiometers CALIBRATION
  int AzMin = 1;        //begining of the potentiometer
  int AzMax = 1023;     //end of the potentiometer
  int ElMin = 1;
  int ElMax = 1023;

// Allowed error for which antennna won't move. Minimum 1 degree
  int AzErr = 9;
  int ElErr = 4;

// Angle difference where soft stop begins
  int Amax = 25;        //azimuth
  int Emax = 15;        //elevation

// min and max power for motors, percents;
  int PwAzMin = 40;     //minimum power for which the motor doesn't stall and starts under load
  int PwAzMax = 100;    //full power for the fastest speed
  int PwElMin = 30;
  int PwElMax = 100;
 
  int PwAz = 0;         //calculated power to be transmitted to motor (percents);
  int PwEl = 0;
/***************************************************************************************************/

// Azim encoder variables
  enum AzPinAssignments {
  AzEncoderPinA = 2,    // encoder right
  AzEncoderPinB = 3,    // encoder left
  AzClearButton = 4};   // encoder push
  unsigned int lastReportedPos = 1;   // change management
  static boolean rotating = false;    // debounce management
  // interrupt service routine vars
  boolean A_set = false;
  boolean B_set = false;
 
//Elev encoder variables
  enum ElPinAssignments{
  ElEncoderPinA = 6,    // encoder right
  ElEncoderPinB = 5,    // encoder left
  ElClearButton = 7};   // encoder push
  int aState;
  int aLastState;
 
// other variables
  int AzPotPin = A0;   // select the input pin for the azim. potentiometer
  int AzRotPin = 12;   // select the out pin for rotation direction
  int AzPWMPin = 11;   // select the out pin for azimuth PWM command
  int TruAzim = 0;     // calculated real azimuth value
  int ComAzim = 0;     // commanded azimuth value
  int OldTruAzim = 0;  // to store previous azimuth value
  int OldComAzim = 0;
  char AzDir;          // symbol for azim rot display
  int AzEncBut = 1;    // variable to toggle with encoder push button
  int ElPotPin = A1;   // select the input pin for the elev. potentiometer
  int ElRotPin = 13;   // select the out pin for elevation rotation direction
  int ElPWMPin = 10;   // select the out pin for elevation rotation PWM command
  int TruElev = 0;     // calculated real elevation value
  int ComElev = 0;     // commanded elevation value
  int OldTruElev = 0;  // to store previous elevation value
  int OldComElev = 0;
  char ElDir;          // symbol for elev. rot display
 
// flags for AZ, EL tolerances
  bool AzStop = false;
  bool ElStop = false;
  int ElUp = 0;                  // 1 = Elevation Dn, 0 = Elevation STOP, 2 = Elevation Up
 
//averaging loop
  const int numReadings = 25;
  int readIndex = 0;             // the index of the current reading 
  int azimuth[numReadings];      // the readings from the analog input
  int elevation[numReadings];
  int totalAz = 0;               // the running total
  int totalEl = 0;

// variables for serial comm
  String Azimuth = "";
  String Elevation = "";
  String ComputerRead;
  String ComputerWrite;
  bool AZser = false;
  bool ELser = false;
  bool ANTser = false;

/*************** END VARIABLE DECLARATION  ************/

void setup() {
  Serial.begin(9600);
  Serial.setTimeout(50);          // miliseconds to wait for USB sata. Default 1000
// Initiate the LCD:
//  lcd.begin(16,2);
//  lcd.init();
//  lcd.backlight();

  // LCD initialization
  lcd.begin(16, 2);                           // LCD set for 16 by 2 display
  lcd.setBacklightPin(3,POSITIVE);            // (BL, BL_POL)
  lcd.setBacklight(HIGH);                     // LCD backlight turned ON
  lcd.clear(); 

//creating custom symbol for up/dwn arrow
  lcd.createChar(1, DownArrow);
  lcd.createChar(2, UpArrow);
 
// pin declaration
  pinMode(AzRotPin, OUTPUT);       //declaring  azim. rotation direction Pin as OUTPUT
  pinMode(AzPWMPin, OUTPUT);       //declaring  azimuth PWM command Pin as OUTPUT
  pinMode(ElRotPin, OUTPUT);       //declaring  elev. rotation direction Pin as OUTPUT
  pinMode(ElPWMPin, OUTPUT);
  pinMode(AzPotPin, INPUT);
  pinMode(ElPotPin, INPUT);
  pinMode(AzEncoderPinA, INPUT);
  pinMode(AzEncoderPinB, INPUT);
  pinMode(AzClearButton, INPUT);
  pinMode(ElEncoderPinA, INPUT);
  pinMode(ElEncoderPinB, INPUT);
  pinMode(ElClearButton, INPUT);

// AzEncoder pin on interrupt 0 (pin A)
  attachInterrupt(0, doEncoderA, CHANGE);
// AzEncoder pin on interrupt 1 (pin B)
  attachInterrupt(1, doEncoderB, CHANGE);
// Reads the initial state of the ElEncoderPinA
   aLastState = digitalRead(ElEncoderPinA);

// write on display name and version
  lcd.setCursor(0, 0);           // Set the cursor on the first column first row.(counting starts at 0!)
  lcd.print("EasyCom AntRotor"); // display "..."
  lcd.setCursor(0, 1);           // Set the cursor on the first column the second row
// changed to reflect mod by CT2GQV
  lcd.print("*YO3RAK/CT2GQV*");
  delay(2000);                   // keep for 2 seconds

// display Azim. and Elev. values
  lcd.setCursor(0, 0);
  lcd.print("Azm.---" + String(char(223)) + "=Cd.---" + String(char(223)));  // char(223) is degree symbol
  lcd.setCursor(0, 1);
  lcd.print("Elv. --" + String(char(223)) + "=Cd. --" + String(char(223)));


/* initialization of the averaging loop */
// this is to set azim-command the same value as real, not to jerk the antenna at start-up
  TruAzim = (map(analogRead(AzPotPin), AzMin, AzMax, 0, 359));      // azimuth value 0-359
  if (TruAzim<0) {TruAzim=0;}
  if (TruAzim>359) {TruAzim=359;}  // keep values between limits
  TruElev = (map(analogRead(ElPotPin), ElMin, ElMax, 0, 90));       // elev value 0-90
  if (TruElev<0) {TruElev=0;}
  if (TruElev>90) {TruElev=90;}    // keep values between limits

// initialize all the readings
  for (int thisReading = 0; thisReading < numReadings; thisReading++) {
    azimuth[thisReading] = 0;
    elevation[thisReading] = 0;
  }
  ComAzim = TruAzim;
  ComElev = TruElev;
  OldTruAzim = TruAzim;
  OldComAzim = ComAzim;
  OldTruElev = TruElev;
  OldComElev = TruElev;
  DisplAzim(TruAzim, 4,0);
  DisplAzim(ComAzim,12,0);
  DisplElev(TruElev, 5,1);
  DisplElev(ComElev,13,1);
}
// end SETUP


// here to process incoming serial data after a terminator received
void process_data (const char * data) // ct2gqv
  {
   char data1[3]="", data2[3]="";  // to hold azimuth and elevation sent by gpredict/rotctld
   int j = 0, k = 0 ; // for counters to insert in data1 and data2
   int select = 1; // holding which data to insert the character
  
   // for AZxxx ELxxx command but will also try to parse something like "AZ EL." so we don't do it if data[3] == 'E'
   if (data[0] == 'A' && data[1] == 'Z' && data[3] != 'E'){  // we wait for the AZ command, it comes similar to AZ3599.0 EL900.0

    for (int i = 0; i < MAX_INPUT; i++)
    {
      char x = data[i];
      if (x == ' ') {select = 2;}; // we are done, we had the first space and means we are on to the second value for azimuth
      if (x == '.') {select = 3;}; // we have a dot so we don't parse the data1 until we have the previous wich is a spce then we do data2
 
      if (isdigit(x) == 0)
      {
      // do nothing only if needed to get AZ or EL string's
      }
      else // it's a number
      {
        if (select == 1) {  // holds the azimuth characters until the dot      
        data1[k] = x;        
        k++; };     
        if (select == 2) {  // holds the elevation     
        data2[j] = x;  
        j++; };       
      }
    }; // end of if loop to separate AZ and EL from the rotator command software
   
    soft_az = atoi(data1);
    soft_el = atoi(data2);
 
    // we need to send now the data to the original code
    ComAzim = soft_az;
    ComElev = soft_el;
    };
   
   // for "AZ EL ." command of query position on gpredict 2.0-4 since it's diferent from gpredict 1.3 (we only care about knowing if "AZ EL" was received)
   if (data[0] == 'A' && data[1] == 'Z' && data[3] == 'E' && data[4] == 'L'){
      // removed the "TM0" (ComputerWrite = "TM0 AZ...") on the ComputerWrite string for gpredict 2.0-4 , also on gpredict 2.0-4 the x10 of the el/az looks fixed so removed
      // the adding of "0.0" to the sent position and elevation, now only decimal dedgrees is send ".0"
      ComputerWrite = "AZ"+String(TruAzim)+".0 EL"+String(TruElev)+".0"; // need to be x10 (add 0.0) or gpredict 1.3 will read divide by 10 (might be rotctld causing it also).
      Serial.println(ComputerWrite);
   };
 
  // for "SA SE ." command of stop azimuth and stop postion.... blank for now.. need to study betther the code
   if (data[0] == 'S' && data[1] == 'A' && data[3] == 'S' && data[4] == 'E'){
     // how should we stop here ? and should we report something ? Should we apply breaks on rotator ?
     // for now we do nothing.....to be continued...
   };
 
 
    
   
 } // end void process_data
 
void processIncomingByte (const byte inByte) // CT2GQV
  {
  static char input_line [MAX_INPUT];
  static unsigned int input_pos = 0;
  switch (inByte)
    {
     // for gpredict 1.3 w report the position here, there is also a factor of 10 that needs to be send, so it's "0.0" apended
     // this is on  Hamlib 1.2.15.3 but diferent on Hamlib 3.1 where it sends "AZ EL"
     case '!':  // rotctld when sending "!" is asking rotator position so let's send it
      ComputerWrite = "TM0 AZ"+String(TruAzim)+"0.0 EL"+String(TruElev)+"0.0"; // need to be x10 (add 0.0) or gpredict 1.3 will read divide by 10 (might be rotctld causing it also).
      Serial.println(ComputerWrite);
      break;    
    case '\n':   // end of text
      input_line [input_pos] = 0;  // terminating null byte   
      // terminator reached! process input_line here ...
      process_data (input_line);   
      // reset buffer for next time
      input_pos = 0; 
      break;
    case '\r':   // discard carriage return
      break;
    default:
      // keep adding if not full ... allow for terminating null byte
      if (input_pos < (MAX_INPUT - 1))
        input_line [input_pos++] = inByte;
      break;
    }  // end of switch
  } // end of processIncomingByte 

void loop() {
/************** FYI, this loop repeats 500 times per second !!! **************/
// AZIMUTH/ELEVATION AVERAGING LOOP
  // subtract the oldest value
  totalAz = totalAz - azimuth[readIndex];
  totalEl = totalEl - elevation[readIndex];
  // read from the sensor:
  azimuth[readIndex] = (map(analogRead(AzPotPin), AzMin, AzMax, 0, 359));
  elevation[readIndex] = (map(analogRead(ElPotPin), ElMin, ElMax, 0, 90));
  // add the reading to the total:
  totalAz = totalAz + azimuth[readIndex];
  totalEl = totalEl + elevation[readIndex];
  // do the average
  TruAzim = totalAz / numReadings;
  TruElev = totalEl / numReadings;
  // keep values between limits
  if (TruAzim<0) {TruAzim=0;}
  if (TruAzim>359) {TruAzim=359;}
  if (TruElev<0) {TruElev=0;}
  if (TruElev>90) {TruElev=90;}
  // advance to the next position in the array:
  readIndex = readIndex + 1;
  // if we're at the end of the array, wrap around to the beginning:
  if (readIndex >= numReadings) {readIndex = 0;} 

// this is to read the command from encoder
  ReadAzimEncoder();
  ReadElevEncoder();
 
// original code
// if (Serial.available()) {SerComm();}          // read USB data

while (Serial.available () > 0) // CT2GQV
    processIncomingByte (Serial.read ()); 

// update antenna position display only if value change
  if ((millis()%500)<10){                       // not to flicker the display
    if (OldTruAzim!=TruAzim) {
      DisplAzim(TruAzim,4,0);
      OldTruAzim = TruAzim;
    }
    if (OldTruElev!=TruElev) {
      DisplElev(TruElev,5,1);
      OldTruElev = TruElev;
    }
  }

// update target position display only if value change
  if (OldComAzim != ComAzim) {
    DisplAzim(ComAzim,12,0);
    OldComAzim = ComAzim;
  }
  if (OldComElev != ComElev) {
    DisplElev(ComElev,13,1);
    OldComElev = ComElev;
  }

// this is to rotate in azimuth
  if (TruAzim == ComAzim) {                   // if equal, stop moving
    AzStop = true;
    analogWrite(AzPWMPin, 0);
    lcd.setCursor(8, 0);
    lcd.print("=");
  }
    else if ((abs(TruAzim - ComAzim)<=AzErr)&&(AzStop == false)) {  // if in tolerance, but it wasn't an equal, rotate
      AzimRotate();}
    else if (abs(TruAzim - ComAzim)>AzErr){   // if target is off tolerance
      AzStop = false;                         // it's not equal
      AzimRotate();                           // rotate
    }

// this is to rotate in elevation
  if (TruElev == ComElev) {                   // if equal, stop moving
    ElStop = true;
    analogWrite(ElPWMPin, 0);
    lcd.setCursor(8, 1);
    lcd.print("=");
    ElUp = 0;                                 // flag for elevation STOP
  }
  else if ((abs(TruElev - ComElev)<=ElErr)&&(ElStop == false)) {  // if in tolerance, but it wasn't an equal, rotate
    ElevRotate();}
  else if (abs(TruElev - ComElev)>ElErr){     // if target is off tolerance
    ElStop = false;                           // it's not equal
    ElevRotate();                             // rotate
  }
// this is to interpret Az encoder x10 multiplication
  while (AzEncBut == 10) {                    // while toggled to x10
    analogWrite(AzPWMPin, 0);                 // STOP antenna rotation
    analogWrite(ElPWMPin, 0);
    lcd.setCursor(8, 0);
    lcd.print("*");
    ReadAzimEncoder();
    if (OldComAzim != ComAzim){               // update display only if numbers change
      DisplAzim(ComAzim, 12, 0);
      OldComAzim = ComAzim;
    }
    delay (100);
  }
}
// end main LOOP

//____________________________________________________
// ___________procedures definitions__________________

void DisplAzim(int x, int y, int z) {
  char displayString[7] = "";
  sprintf(displayString, "%03d", x);  //outputs a fixed lenght number (3 integer)
  lcd.setCursor(y, z);                // for no leading zeros "__7" use "%3d"
  lcd.print(displayString);
 
// ************** FOR CALIBRATION PURPOSES **************
//  Serial.print ("Az ");
//  Serial.println (analogRead(AzPotPin));
}

void DisplElev(int x, int y, int z){
  char displayString[7] = "";
  sprintf(displayString, "%02d", x);  //outputs a fixed lenght number (2 integer)
  lcd.setCursor(y, z);                // for no leading zeros "_7" use "%2d"
  lcd.print(displayString);

// ************** FOR CALIBRATION PURPOSES **************
//  Serial.print ("El ");
//  Serial.println (analogRead(ElPotPin));
}

void ReadElevEncoder() {
  aState = digitalRead(ElEncoderPinA); // Reads the "current" state of the ElEncoderPinA
   // If the previous and the current state of the ElEncoderPinA are different, that means a Pulse has occured
   if (aState != aLastState){    
     // If the ElEncoderPinB state is different to the ElEncoderPinA state, that means the encoder is rotating clockwise
     if (digitalRead(ElEncoderPinB) != aState) { ComElev ++;}
      else { ComElev --;}
     if (ComElev <0) {ComElev = 0;}
     if (ComElev >90) {ComElev = 90;}
   }
   aLastState = aState; // Updates the previous state of the ElEncoderPinA with the current state
}

void ReadAzimEncoder() {
  rotating = true;  // reset the debouncer
  if (lastReportedPos != ComAzim) {
    lastReportedPos = ComAzim;
  }
  delay(10);
  if (digitalRead(AzClearButton) == LOW )  {      // if encoder switch depressed
    delay (250);                                  // debounce switch
    if (AzEncBut == 1){
      AzEncBut = 10;
      ComAzim = int(ComAzim/10)*10;               // ComAzim in 10deg. steps
    }
    else {
      AzEncBut = 1;
    }
  }
} //end ReadAzimEncoder()

// Interrupt on A changing state
void doEncoderA() {
  // debounce
  if ( rotating ) delay (1);  // wait a little until the bouncing is done
  // Test transition, did things really change?
  if ( digitalRead(AzEncoderPinA) != A_set ) {   // debounce once more
    A_set = !A_set;
    // adjust counter + if A leads B
    if ( A_set && !B_set )
      ComAzim += AzEncBut;
      ComAzim = ((ComAzim + 360) % 360);         // encoderPos between 0 and 359 deg.
    rotating = false;  // no more debouncing until loop() hits again
  }
}
// Interrupt on B changing state, same as A above
void doEncoderB() {
  if ( rotating ) delay (1);
  if ( digitalRead(AzEncoderPinB) != B_set ) {
    B_set = !B_set;
    //  adjust counter - 1 if B leads A
    if ( B_set && !A_set )
      ComAzim -= AzEncBut;
      ComAzim = ((ComAzim + 360) % 360);         // encoderPos between 0 and 359 deg.
    rotating = false;
  }
 }

void AzimRotate() {
    if ((ComAzim-TruAzim) > (TruAzim-ComAzim)) {      // this to determine direction of rotation
// cold switching - stop motor before changing direction - to protect mechanic and electric parts
        if (AzDir == char(127)) {                     // if previously rotating in the oposite direction
          analogWrite(AzPWMPin, 0);                   // STOP the motor
          delay(400);                                 // pre-switch delay
          digitalWrite(AzRotPin, LOW);                // deactivate rotation pin - rotate right
          delay(200);                                 // post-switch delay
        }
        else {                                        // same directin, no Stop, no delay
          digitalWrite(AzRotPin, LOW);                // deactivate rotation pin - rotate right
        }
          AzDir = char(126);                          // "->"
    }
      else {
        if (AzDir == char(126)) {                     // if previously rotating in the oposite direction
          analogWrite(AzPWMPin, 0);                   // STOP the motor
          delay(400);                                 // pre-switch delay
          digitalWrite(AzRotPin, HIGH);               // activate rotation pin - rotate left
          delay(200);                                 // post-switch delay
        }
        else {                                        // same directin, no Stop, no delay
          digitalWrite(AzRotPin, HIGH);               // activate rotation pin - rotate left
        }
        AzDir = char(127);                            // "<-"
      }
    lcd.setCursor(8, 0);
    lcd.print(String(AzDir));
 // this activates azim PWM pin proportional with angle error
    PwAz = PwAzMin + round((abs(ComAzim-TruAzim))*(PwAzMax-PwAzMin)/(Amax-AzErr));   //formula which outputs a power proportional with angle difference
    if (PwAz > 100) {PwAz = 100;}
    analogWrite(AzPWMPin, round(2.55*PwAz));          // activate Azim drive PWM pin
}

void ElevRotate() {
// this to determine direction of rotation
    if ((ComElev-TruElev) > (TruElev-ComElev)) {
      if (ElUp == 1) {                                // if previously rotating in the oposite direction
        analogWrite(ElPWMPin, 0);                     // STOP the motor
        delay(400);                                   // pre-switch delay
        digitalWrite(ElRotPin, LOW);                  // deactivate rotation pin - rotate UP
        delay(200);                                   // post-switch delay
      }
      else {                                          // same directin, no Stop, no delay
        digitalWrite(ElRotPin, LOW);                  // deactivate rotation pin - rotate UP
      }
      lcd.setCursor(8, 1);
      lcd.write(2);                                   // arrow up
      ElUp = 2;
    }
     else {
      if (ElUp == 2) {                                // if previously rotating in the oposite direction
        analogWrite(ElPWMPin, 0);                     // STOP the motor
        delay(400);                                   // pre-switch delay
        digitalWrite(ElRotPin, HIGH);                 // deactivate rotation pin - rotate UP
        delay(200);                                   // post-switch delay
      }
      else {                                          // same directin, no Stop, no delay
        digitalWrite(ElRotPin, HIGH);                 // deactivate rotation pin - rotate UP
      }
        lcd.setCursor(8, 1);
        lcd.write(1);                                 // arrow down
        ElUp = 1;
    }
 // this activates elev PWM pin proportional with angle error
    PwEl = PwElMin + round((abs(ComElev-TruElev))*(PwElMax-PwElMin)/(Emax-ElErr));   //formula which outputs a power proportional with angle difference
    if (PwEl > 100) {PwEl = 100;}
    analogWrite(ElPWMPin, round(2.55*PwEl));           // activate Elev drive PWM pin
}

/* removed by CT2GQV
void SerComm() {
  // initialize readings
  ComputerRead = "";
  Azimuth = "";
  Elevation = "";

  while(Serial.available()) {
    ComputerRead= Serial.readString();  // read the incoming data as string
//    Serial.println(ComputerRead);     // echo the reception for testing purposes
  }  

// looking for command <AZxxx.x>
    for (int i = 0; i <= ComputerRead.length(); i++) {
     if ((ComputerRead.charAt(i) == 'A')&&(ComputerRead.charAt(i+1) == 'Z')){ // if read AZ
      for (int j = i+2; j <= ComputerRead.length(); j++) {
        if (isDigit(ComputerRead.charAt(j))) {                                // if the character is number
          Azimuth = Azimuth + ComputerRead.charAt(j);
        }
        else {break;}
      }
     }
    }
   
// looking for command <ELxxx.x>
    for (int i = 0; i <= (ComputerRead.length()-2); i++) {
      if ((ComputerRead.charAt(i) == 'E')&&(ComputerRead.charAt(i+1) == 'L')){ // if read EL
        if ((ComputerRead.charAt(i+2)) == '-') {
          ComElev = 0;                  // if elevation negative
          break;
        }
        for (int j = i+2; j <= ComputerRead.length(); j++) {
          if (isDigit(ComputerRead.charAt(j))) {                               // if the character is number
            Elevation = Elevation + ComputerRead.charAt(j);
          }
          else {break;}
        }
      }
    }
   
// if <AZxx> received
    if (Azimuth != ""){
      ComAzim = Azimuth.toInt();
      ComAzim = ComAzim%360;          // keeping values between limits(for trackers with more than 360 deg. rotation)
      }

// if <ELxx> received
    if (Elevation != ""){
      ComElev = Elevation.toInt();
      if (ComElev>180) { ComElev = 0;}
      if (ComElev>90) {               //if received more than 90deg. (for trackers with 180deg. elevation)
        ComElev = 180-ComElev;        //keep below 90deg.
        ComAzim = (ComAzim+180)%360;  //and rotate the antenna on the back
      }
    }

// looking for <AZ EL> interogation for antenna position
  for (int i = 0; i <= (ComputerRead.length()-4); i++) {
    if ((ComputerRead.charAt(i) == 'A')&&(ComputerRead.charAt(i+1) == 'Z')&&(ComputerRead.charAt(i+3) == 'E')&&(ComputerRead.charAt(i+4) == 'L')){
    // send back the antenna position <+xxx.x xx.x>
      ComputerWrite = "+"+String(TruAzim)+".0 "+String(TruElev)+".0";
      Serial.println(ComputerWrite);
    }
  }
}
// end SerComm()
*/

/// code end


Have a nice day!

Saturday, October 02, 2021

Raspberry rotary encoder in Python

 Nothing fancy here, just an adaptation of the code in here to add the switch function in the rotary encoder software.

Connections are like this:


Output will be "UP", "Down" an "click" messages depending on the rotary encoder change, you can then re-use for your own code inside the functions.


I'm using it now to control an SDR receiver (rtl_sdr), might post about it when finished.

The code after the change is this one:

##############

import RPi.GPIO as GPIO
from time import sleep
 
counter = 10
 
Enc_A = 17 
Enc_B = 27 
Enc_SW = 22
 
def init():
    print "Rotary Encoder Test Program"
    GPIO.setwarnings(True)
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(Enc_A, GPIO.IN)
    GPIO.setup(Enc_B, GPIO.IN)
    GPIO.setup(Enc_SW, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.add_event_detect(Enc_A, GPIO.RISING, callback=rotation_decode, bouncetime=10)
    GPIO.add_event_detect(Enc_SW, GPIO.FALLING, callback=swClicked, bouncetime=300)
    return


def swClicked(channel):
#        global paused
#        paused = not paused
        print ("click")  
 
 def rotation_decode(Enc_A):
    global counter
    sleep(0.002)
    Switch_A = GPIO.input(Enc_A)
    Switch_B = GPIO.input(Enc_B)
 
    if (Switch_A == 1) and (Switch_B == 0):
        counter += 1
#        print "direction -> ", counter
        print "UP"
        while Switch_B == 0:
            Switch_B = GPIO.input(Enc_B)
        while Switch_B == 1:
            Switch_B = GPIO.input(Enc_B)
        return
 
    elif (Switch_A == 1) and (Switch_B == 1):
        counter -= 1
#        print "direction <- ", counter
        print "DOWN"
        while Switch_A == 1:
            Switch_A = GPIO.input(Enc_A)
        return
    else:
        return
 
def main():
    try:
        init()
        while True :
            sleep(1)
 
    except KeyboardInterrupt:
        GPIO.cleanup()
 
if __name__ == '__main__':
    main()

##############

As you can see side by side, only some extra lines, all credit to the original code creator.



Have a nice day!