Antenna rotator control program for Raspberry Pi and WSJT-X

For the digital modes traffic, I run WSJT-X on a Raspberry Pi (4B). So, no need to devote a “big” computer for that purpose, the Raspberry does the job perfectly well, and for less power consumption and noise. For my 144 MHz moonbounce operations, I wanted a simple system to drive my existing ERC-3D rotator controller (by DF9GR). That model is now obsolete but works flawlessly and I wanted to keep it as-is, without changing anything. The ERC-3D is linked to azimuth (Yaesu G650) and elevation (Kenpro KR500) antenna rotators.

Rotator Controller

The search for a ready made program I did on the Internet was not successful, since I wanted to keep as-is my exisiting system. Having in mind that WSJT-X provides an azel.dat file including the coordinates of the moon, I decided to write a program to extract these data and to use them to command my ERC-3D, in first place to track the moon.

To be able to do it, a prerequisite is to have Hamlib installed on the Raspberry. What is hamlib : “the Ham Radio Control Library –Hamlib, for short– is a project to provide programs with a consistent Application Programming Interface (API) for controlling the myriad of radios and rotators available to amateur radio and communications users”. To date the current hamlib version is 4.2. and can be downloaded here. How to install it on the Raspberry is out of the present scope, there a several tutorials on the Internet that clearly explain it (it is very easy).

Once Hamlib was installed, the program had to be written (it is available at the end of this post). It has been written in Python 3 and, since I’m not at all a programmer, it is my friend Didier (ON4KDV), who wrote the backbone of the program (many thanks to him !). I then customized it and added some features, a.o. the possibility to command the rotator controller not only to automatically track the moon but also to rotate/elevate the antennas wherever needed.

The program is named “Rotator Controller” and an icon placed on the desktop of the Raspberry allows to launch it easily. It opens in the command-line terminal.

Here is below how it works.

First you select the USB port to be used and then you have the choice :

  • Manual rotation/elevation
  • Automatic tracking of the moon, as from the azel.dat file out of WSJT-X
  • Exit

We start first with the manual rotation/elevation. The possible options are self-explanatory. See also the code at the end, it provides many explanations (comments) too.

Rotator Controller 1

When the option “r” is selected, if it was elevated before, the antenna will come back to the horizon (0° elevation) and towards the wished azimuth. “s” will stop immediately any rotation/elevation.

On the example here, we set the azimuth to 113°. Then we ask for the position (“p”) and we see that the antenna is effectively well on Az. 113°.

Rotator Controller 2

Then we select “l” and we set Az. to 113° and El. to 10°. Requesting the position by typing “p”, we get well 113° Az. and 10° El.

Rotator Controller 3

To come back to the main menu, you have to exit (“e”) and restart the program. Before restarting the program, we must start first WSJT-X, so that the azel.dat file is available. We then restart the program and we select a USB port not already in use by WSJT-X (for the CAT for example).

We select “a” for the automatic moon tracking. We are prompted to set a “refresh duration”, it is the time interval (in minutes, but can be customized in seconds, see comments in the code) between two refreshes of the moon position (access to the azel.dat file in WSJT-X).

Rotator Controller 4

Every x (2 in the example) minutes, as per defined by the “refresh duration”, the moon position is derived from the azel.dat file, the antenna is rotated and elevated accoringly, and the actual antenna position (azimuth and elevation) is reported 5 seconds later.

Rotator Controller 5

If the moon is below the horizon, nothing happens and the program indicates “The moon is below horizon, antenna parked !”, and so on until the moon rises above the horizon. To  exit, press “CTRL” and “C”.

Rotator Controller 6

Here is the heavily commented python 3 program :

#!/usr/bin/python3
# coding: utf-8

# This script allows to rotate an antenna system either by manually typing in a heading (azimuth and/or elevation)
# you want the antenna to rotate to or by automatically tracking the moon azimuth and elevation.
# The Moon azimuth and elevation are extracted out of the file azel.dat provided by the WSJT-X suite.
# Beside WSJT-X, this script also makes use of the rotator controller daemon embedded in the Hamlib
# libraries. So, the prerequisites for this script to run are WSJT-X opened and running, and the
# Hamlib librairies installed on your Raspberry Pi.
# This script has been written in Python 3 by Didier, ON4KDV and Gaëtan, ON4KHG.
# My personnal system makes use of a Raspberry Pi 4B and an ERC-3D (DF9GR) antenna rotator controller.
# Both are linked with a USB (on the Raspberry Pi side) to RS232 serial (on the controller side) connection.
 
# First, import the time, system and subprocess functions needed in this script

import time
import os, sys
import subprocess

# Select the USB port of the Raspberry Pi onto which the rotator controller is connected.
# Normally, as WSJT-X is started beforehand, the USB0 port is already assigned for the CAT & PTT of WSJT-X.
# In my case, I usually select USB1, so, I type “1” below. 

usb_port=(input(“USB port to use (0-3) : “))
print()

if usb_port == “0”:

# The Rotctld(aemon) is opened with the command “rotctld -m 601 -r /dev/ttyUSB0 &”.
# -m 601 is related to the GS-232A rotator protocol.
# You could have to have to select another number according to the protocol and associated hardware in use on your side.
# The list of supported protocols can be obtained by typing “rotctld -l” in the terminal.
# ttyUSB0 is the serial port (USB0) of the Raspberry onto which the rotator controller is connected.

        subprocess.call(‘rotctld -m 601 -r /dev/ttyUSB0 &’, shell=True)

# And so on for the other USB ports.
         
elif usb_port == “1”:
         
        subprocess.call(‘rotctld -m 601 -r /dev/ttyUSB1 &’, shell=True)
         
elif usb_port == “2”:
         
        subprocess.call(‘rotctld -m 601 -r /dev/ttyUSB2 &’, shell=True)
         
elif usb_port == “3”:
         
        subprocess.call(‘rotctld -m 601 -r /dev/ttyUSB3 &’, shell=True)
         
# Select the working mode : manual, automatic (moon tracking) or exit.

Mode=(input(“MANUAL rotation/elevation, type m\nAUTOMATIC tracking, type a\nEXIT, type e\n\n”))

if Mode == “m”:

# If “m” is chosen, the manual rotation mode is selected.
   
    while (True):

# Then, select what you want to do : rotate/elevate the antenna, get its current position, stop the rotation in case
# of emergency or exit.
     
      Submode=(input(“\nROTATE antenna (azimuth only) and set elevation to 0°, type r\nROTATE and ELEVATE antenna (azimuth and elevation), type l\nGet current POSITION, type p\nSTOP, type s\nEXIT, type e\n\n”))
     
      if Submode == “r”:
         
# If “r” is selected, instruct towards which azimuth the antenna has to rotate. With this selection, if the antenna was previously elevated,
# it will be set back to 0° (no elevation) too.        
        
         az=(input(“\nRotate antenna to azimuth (°) : “))
         az=az.strip()
                 
# Build and execute the command “set_pos”. The default port used by the rotctld(aemon) is 4533.

         command_az=’echo “|\set_pos ‘ + az + ‘ 0” | nc -w 1 localhost 4533’
         os.system(command_az)

      elif Submode == “l”:

# If “l” is selected, you can set whatever azimuth and elevation angle.

         az=(input(“\nRotate antenna to azimuth (°) : “))
         az=az.strip()
         el=(input(“Elevate antenna to elevation (°) : “))
         el=el.strip()

         command_azel=’echo “|\set_pos ‘ + az + ‘ ‘ + el + ‘” | nc -w 1 localhost 4533’
         os.system(command_azel)
     
# If “p” to get the position of the antenne is selected, build and execute the command “get_pos”.     
    
      elif Submode == “p”:
         
         command_azpos=’echo “|\get_pos” | nc -w 1 localhost 4533′
         os.system(command_azpos)

# If “s” to (emergency) stop is selected, build and execute the command “S” (Stop).

      elif Submode == “s”:
               
         command_stop=’echo S | nc -w 1 localhost 4533′
         os.system(command_stop)
        
      else:

# If none of the above is selected, then exit the script.

         exit()
    
elif Mode == “a”:

# If “a” is chosen, the automatic (moon tracking) mode is selected.
# Set the refresh rate in minutes of the moon azimuth and elevation.
# With narrow beamwidth antennas (microwaves), the refresh rate has to be fast.
# With wide beamwidth antennas, the refresh rate can be slower. On 2m, I chose 4 minutes.
# If you want a refresh time in seconds, remove the line “ref=ref*60” and rename “min” into “sec”
# in the line ref=int(input(“\nRefresh duration (min) : “)).
   
    ref=int(input(“\nRefresh duration (min) : “))
    ref=ref*60
   
    while (True):

# Open the file azel.dat of WSJT-X in read mode. Make sure the path “/home/pi/.local/share/WSJT-X/azel.dat” is the same
# than the path defined in the settings of WSJT-X (see the WSJT-X user guide).
# The first line of the file is read and placed in the variable “txt” and then the file is closed.

      f=open(“/home/pi/.local/share/WSJT-X/azel.dat”,”r”)
      txt=f.readline()
      f.close()

      if (“Moon” in txt):                  # Check that the first line contains the word “Moon”. 
        p=txt.find(“,”)                    # Search for the 1st comma (,).
        if (p > 1):                        # If the 1st comma has been found
            txt=txt[p+1:]                  # what is in front of the 1st comma is removed, including the comma.
            p=txt.find(“,”)                # Search for the 2nd comma.
            if (p > 1):                    # If the 2nd comma has been found

                az=txt[0:p]              # the text in between the 1st and 2nd commas is saved in “az” (azimuth).
                az=az.strip()            # Spaces before and after are removed, only the figures/sign are kept.
                print(“Moon azimuth (°) :”,az)   # The moon azimuth is displayed.
                txt=txt[p+1:]              # What is in front of the 2nd comma is removed, including the comma.
               
                p=txt.find(“,”)          # Search for the 3rd comma.
                if (p > 1):              # If the 3rd comma has been found

                    el=txt[0:p]          # the text in between the 2nd and 3rd commas is saved in “el” (elevation).
                    el=el.strip()        # Spaces before and after are removed, only the figures/sign are kept.
                    print(“Moon elevation (°) :”,el)   # The moon elevation is displayed.

# Build the command “set_pos” as from the az and el variables extracted above.

                    command_azel=’echo “|\set_pos ‘ + az + ‘ ‘ + el + ‘” | nc -w 1 localhost 4533’
                   
# Convert az and el from text to float (numerical) format.

                    f_az=float(az)
                    f_el=float(el)

# Execute the command only if the azimuth is between 0 and 360° and if the elevation is between 0 and 90°.

                    if(f_az >0 and f_az <361 and f_el >0 and f_el <91):
                         os.system(command_azel)
                        
# Otherwise the moon is below the horizon and nothing is executed but displaying that the moon is below horizon.

                    else:
                         print(‘\nThe moon is below horizon, antenna parked !’)
                        
# We wait 5 seconds, the time for the antenna to rotate (it may actually take longer, depending on the position of
# the antenna at startup of the tracking) and then we display the position of the antenna.
# Finally, we indicate that we wait until the next update, according to the refresh duration (ref) defined above.
                       
                    time.sleep(5)
                    print()
                    command_azelpos=’echo “|\get_pos” | nc -w 1 localhost 4533′
                    os.system(command_azelpos)
                    print(‘\nWaiting for the next update or CTRL+C to exit\n’)
                                    
      time.sleep(ref)

# If none of the working mode (manual or automatic moon tracking) was selected, exit and close the script.

else:
    exit()

SPACE

 




LDMOS 144 MHz 1kW SSPA (2017)

Un peu de travail sur mon amplificateur de puissance 144 MHz, à savoir le remplacement de la “palette RF” (par un module W6PQL utilisant le transistor BLF188XR de chez NXP) et la mise à jour du logiciel Arduino. Le résultat est visible sur la petite vidéo (4 minutes) ci-dessous. Sous 50V / 30A, l’ampli. sort plus de 1kW avec une puissance d’excitation de 2,5W…

A bit of work on my 144 MHz power amplifier, i.e. the replacement of the “RF pallet” (by a W6PQL module, using the transistor BLF188XR from NXP) and an update of the Arduino software. The result can be seen on the short video (4 minutes) hereunder. Under 50V / 30A, the amplifier outputs more than 1kW with a drive power of 2,5W…

https://youtu.be/PjpB5Wd5W3w

SPACE




Réception automatique des balises à l’aide de Spectrum Lab

En 2008, j’ai écrit un article au format pdf relatif à la réception des balises, principalement VHF. Le but étant de mesurer (relativement au niveau de bruit) l’amplitude des balises et d’en relever automatiquement les captures d’écran. Le logiciel utilisé pour ce faire est Spectrum Lab de Wolf, DL4YHF. Le mode opératoire est expliqué de manière exhaustive (en anglais) dans les documents disponibles ici ; les infos ne sont donc pas reproduites dans le présent article.

Ci-dessous un exemple de capture automatique de la balise HB9HB sur 144.448. On voit clairement le signal direct, matérialisé par la trace droite verticale, ainsi que les réflexions sur les avions (“airplane-scatter”), illustrées par les traces inclinées. A l’intersection des deux traces, on remarque un renforcement du signal (la trace résultante est plus claire) lorsque le signal direct et le signal réfléchi arrivent en phase au récepteur.

Example of Screen Capture HB9HBSPACE




Filtre Passe-Bande 144 MHz Haut-Q commercial (2015)

Le filtre montré sur les photos ci-dessous est un filtre passe-bande commercial (fabricant AFL) à haut facteur de qualité (“haut-Q”). Initialement prévu pour un usage professionnel autour de 170 MHz mais plus utilisé, j’ai pu en disposer ; je l’ai réaligné sur 145 MHz (on voit clairement sur les photos la demi spire ajoutée aux bobinages d’origine). La perte d’insertion est excellente, inférieure à 0,4 dB.

Fréquence (MHz)Atténuation (dB)
145 (fr. centrale, fc)0,35
1440,36
1460,36
135 (fc - 10 MHz)35
155 (fc + 10 MHz)28,5
115 (fc - 30 MHz)69
175 (fc + 30 MHz)51
95 (fc - 50 MHz)88
195 (fc + 50 MHz)59

SPACE




Wattmètre de -60 à +60 dBm et mesureur de champs (2014)

Lors de la réalisation de l’amplificateur de puissance 144 MHz 600W à LDMOS, la nécessité de pouvoir mesurer une puissance supérieure à 500W (sur 144) s’est faite sentir. Les Bird 43 sont relativement chers, il faut des bouchons. Achetés sur Internet ou en brocantes, leur origine est incertaine, ainsi que la manière dont ils ont été manipulés au préalable. Il me fallait autre chose !

J’ai donc décidé de construire moi-même mon wattmètre. L’option choisie était de réaliser un coupleur directionnel qui prélèverait environ -50 dB du signal à mesurer. 60 dBm (1kW) – 50 dB = 10 dBm, soit 10mW, ce qui est facile à mesurer à l’aide d’un circuit intégré AD8307 (détecteur logarithmique de chez Maxim) dont je disposais d’un échantillon.

Pour réaliser le coupleur directionnel, j’ai pris du coax à diélectrique Teflon (RG142), qui tient 1kW sur 144. J’ai enlevé la gaine et la tresse sur environ 4 cm. J’ai remplacé la tresse par un feuillard en cuivre (épais) que je n’ai pas refermé complètement, il subsistait donc une fente. Ensuite, j’ai pris un bout de coax semi-rigide (UT-141), que j’ai limé pour enlever le cuivre et ainsi laisser également une fente. Finalement, j’ai disposé la fente du RG142 (ligne principale) en regard de celle de l’UT-141 (ligne couplée) et j’ai refermé le feuillard en cuivre en le soudant sur l’UT-141. J’ai terminé la ligne couplée par un petit potentiomètre, que j’ai ajusté pour atténuer au maximum l’onde réfléchie, et ainsi maximiser la directivité (= la capacité du coupleur à discriminer l’onde incidente de l’onde réfléchie au cas où la charge qui termine le coupleur ne présente pas 50 ohms). Plus la directivité sera grande, plus la mesure de VSWR (ROS) sera précise. Si on ne fait que des mesures de puissance sur charge de 50 ohms, la directivité n’a pas d’importance, puisqu’il n’y a pas d’onde réfléchie (ou si peu).

SPACE

S31_FRSPACE

Le facteur de couplage sur 144 est de -48,5 dB (= un signal qui passe dans la ligne principale est disponible sur le port de la ligne couplée atténué de 48,5 dB). La directivité est de 25 dB (toujours sur 144) et la perte d’insertion de 0,01 dB.

SPACE

Directivité_FR

SPACE

Sur 2,3 GHz, la perte d’insertion n’est toujours que de 0,04 dB mais la directivité tombe à 8 dB. Sur 1,3 GHz, elle est de 14,9 dB.

SPACE

SPACE

SPACE

Exemple pratique de mesure : mon SSPA 144 MHz de 300W. Sur les photos, on voit que l’analyseur de spectre qui termine le port couplé indique 6,54 dBm (en haut à droite). On sait que le signal sur le port couplé est -48,5 dB de celui qui passe dans la ligne principale, et donc que dans la ligne principale on a +48,5 dB par rapport à ce qu’on mesure sur le port couplé. En l’occurrence, 6,54 dBm + 48,5 dB (pas dBm ici) = 55,04 dBm, ce qui, converti en Watt, donne 320W dans la ligne principale, et donc à la sortie de l’ampli.

L’étape suivante consiste à substituer l’analyseur de spectre par un détecteur logarithmique construit sur base du CI AD8307 (Maxim) et qui donne une tension de sortie linéaire en fonction de la puissance d’entrée (+0,25 mV out pour chaque dB de plus à l’entrée), entre 0 et 500 MHz. Ce, de -70 à +10 dBm. L’ensemble coupleur directionnel + AD8307 permettra de mesurer de -70 à (10+48,5) dBm, soit de 0,1µW à 700W, voire plus avec un atténuateur entre le coupleur et l’AD8307 (avec un max de 1kW pour la bonne tenue du RG142).

La  réalisation du détecteur autour de l’AD8307 est basée sur un article de W7ZOI et W7PUA paru en 2001 dans la revue QST : “Simple RF-Power measurement“. L’AD8307 étant tellement sensible, il est indispensable de bien découpler et disposer des perles de ferrite sur l’alimentation du détecteur. Inutile de dire qu’il doit être disposé dans un boîtier impérativement métallique (blindage). Les performances du détecteur on été mesurées à l’aide d’un générateur professionnel (SMC100 de Rohde & Schwarz), comme ci-dessous, la mesure de la tension détectée et présente en sortie de l’AD8307 en fonction de la puissance injectée à l’entrée :

Voltage vs input power

On remarque l’excellente linéarité entre -60 et 10 dBm et en-dessous de 500 MHz.

Le graphique qui suit donne une image de la réponse en fréquence du détecteur. Pour ce faire, on injecte une puissance constante en entrée (ici -30 dBm), tout en faisant varier la fréquence. On voit bien que la réponse n’est pas plate et on mesure un écart maximum de 0,23V (2,48V @ 30 MHz – 2,25V @ 500 MHz). Sachant que la tension en sortie du détecteur varie en moyenne de 0,046V par dB de puissance RF en entrée (déduction faite à partir du graphique précédent), on conclut qu’il y a une différence de 5 dB (0,23/0,046) entre le haut et la bas de la plage de fonctionnement du détecteur.

Voltage vs frequency

Sachant donc que la réponse du détecteur n’est pas plate sur toute la plage de fonctionnement et pour garantir une précision suffisante, j’ai décidé de calibrer le détecteur uniquement sur les bandes amateur, chacune possédant alors ses propres paramètres de calibration. A la fréquence médiane de chaque bande considérée (10m, 6m, 4m, 2m et 70cm), une puissance de -60 à 10 dBm a été injectée par pas de 5 dB en entrée du détecteur et la tension de sortie mesurée à chaque pas. Le tout a été reporté sur un graphique et l’équation de la droite (visible en haut à gauche sur le graphique) qui passe “au mieux” par chaque point a été dérivée (possibilité offerte par MS Excel). On peut voir ci-dessous un exemple de tel graphique (pour la bande des 2m) :

144 ER_FR

Et pour garantir encore plus de précision, une seconde équation de calibration a également été calculée mais cette fois sur une plage dynamique plus restreinte, à savoir de -10 à 10 dBm. Celle-ci permet dès lors de mesurer avec une grande précision de -10 dBm (entrée en direct sur le détecteur) à 58,55 dBm (58,55 = 10+48,55, le facteur de couplage du coupleur directionnel sur 144 MHz) lorsqu’on utilise de coupleur directionnel avec le détecteur, soit donc de 0,1mW à 710W. Voici la droite de calibration sur la plage -10 à 10 dBm sur 144 MHz :

144 HR_FR

L’article d’origine prévoyait d’afficher la tension de sortie de l’AD8307 sur un galvanomètre à aiguille. J’ai souhaité le remplacer par un afficheur LCD, que j’ai développé autour d’un Arduino Uno, d’autant que la tension de sortie du détecteur variant grosso modo de 0 à 5V, se prête bien à être injectée sur une entrée DAC d’un Arduino. De plus au travers de la programmation de l’Arduino, on peut considérer les équations des différentes droites de calibration par bande et ainsi afficher la puissance en dBm (ou autre unité) sur l’afficheur LCD.

Quid de la résolution ? Pour chaque dB de plus sur l’entrée RF, on a vu précédemment que pour chaque dB de plus à l’entrée du détecteur, la tension à la sortie du détecteur AD8307 augmente de 0,046V. Sachant que l’Arduino échantillonne sur 10bits  (2^10 = 1024 niveaux), 5V/1024 = 0,0049V, ce qui est environ un dixième de 0,046V. La résolution de la mesure est donc d’environ 0,1 dB, soit une incertitude de 12W sur une mesure de 500W. Photos de l’ensemble terminé :

SPACE

Le “gros” boîtier, celui qui comporte l’afficheur LCD, renferme également l’Arduino, ainsi qu’une batterie de 12V. C’est le meilleur moyen d’alimentation que j’ai trouvé, garantissant une autonomie suffisante (une simple pile de 9V ne l’est pas). L’utilisation d’une alimentation externe est à proscrire car, même bien décuplée, elle “ramène” des signaux/bruit qui faussent la mesure du wattmètre. Le câble de liaison (alimentation du détecteur + tension mesurée, issue du détecteur) entre le boîtier LCD et le détecteur (petit boîtier) doit être muni de plusieurs ferrites, comme montré sur les photos.

J’ai également adjoint au programme Arduino la possibilité d’afficher, sous forme de bar graph, le niveau de champs (“Field Strength Meter”) ; c’est une mesure relative, pas absolue.

Les menus du wattmètre ont été définis comme suit :

  • Mode
    • Power (affiche la puissance)
    • Voltage (affiche la tension en sortie de l’AD8307)
    • Battery (affiche la tension de la batterie)
    • Field strength (affiche le niveau de champs)
  • Band
    • 10m
    • 6m
    • 4m
    • 2m
    • 70cm
  • Unit
    • mW
    • W
    • dBm
  • Coupler
    • Mesure directe (sans coupleur directionnel)
    • Mesure avec coupleur directionnel
  • Range
    • Etendue (celle avec la calibration de -60 à 10 dBm)
    • Haute résolution (celle avec la calibration de -10 à 10 dBm)

Le programme Arduino est disponible ici et un brouillon d’article (non terminé) ici.

SPACE