Ryan Malloy c93bbef26d Initial travler-rotor library scaffolding
Extract Gabe Emerson's Trav'ler rotor scripts into a proper Python
library with firmware protocol abstraction (HAL 2.05 + HAL 0.0.00),
Hamlib rotctld TCP server, Click CLI, and isolated leap-frog algorithm
with the elevation copy-paste bug fixed.
2026-02-11 04:10:17 -07:00

160 lines
5.2 KiB
Python

#Python program to control Winegard Trav'ler antenna as an AZ/EL Rotor
#Version 2.0B (For HAL Version 2.05, NOT for HAL v0.0)
#Gabe Emerson / Saveitforparts 2025, Email: gabe@saveitforparts.com
import serial
import socket
import regex as re
#initialize some variables
current_az = 0.0
current_el = 0.0
move_nbr = 0
#define "antenna" as the serial port device to interface with
antenna = serial.Serial(
port='/dev/ttyUSB0', #pass this from command line in future?
baudrate = 57600,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1)
print ('Antenna connected on ', antenna.port)
#Prep the Trav'ler firmware to accept commands
antenna.write(bytes(b'q\r')) #go back to root menu in case firmware was left in a submenu
antenna.write(bytes(b'\r')) #clear firmware prompt to avoid unknown command errors
antenna.write(bytes(b'motor\r')) #enter motor submenu
#listen to local port for rotctld commands
listen_ip = '127.0.0.1' #listen on localhost
listen_port = 4533 #pass this from command line in future?
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.bind((listen_ip, listen_port))
client_socket.listen(1)
while 1:
print ('Listening for rotor commands on', listen_ip, ':', listen_port)
print ('Ctrl-C to exit')
conn, addr = client_socket.accept()
print ('Connection from ',addr)
#pass rotor commands to antenna
while 1:
data = conn.recv(100) #get Gpredict's message
if not data:
break
cmd = data.decode("utf-8").strip().split(" ") #grab the incoming command
if cmd[0] == "p": #Gpredict is requesting position
#ask antenna for position
antenna.write(bytes(b'a\r')) #get current Az/El/Sk
antenna.flush()
#read current position from antenna
reply1 = antenna.read(200).decode(errors='ignore').strip() #read dish response
#Regular expression patterns to find AZ and EL values
az_pattern = r"AZ =(\s+\d+\.\d+)"
el_pattern = r"EL =(\s+\d+\.\d+)"
#Use re.search to extract the values
az_match = re.search(az_pattern, reply1)
el_match = re.search(el_pattern, reply1)
#If matches are found, extract the numerical values
if az_match and el_match:
current_az = float(az_match.group(1))
current_el = float(el_match.group(1))
else:
print("AZ or EL not found.")
print("Current position:", current_az,", ", current_el)
response = "{}\n{}\n".format(current_az, current_el) #put az & el into the format Gpredict expects
conn.send(response.encode('utf-8')) #send response to Gpredict
antenna.flush()
elif cmd[0] == "P": #Gpredict is sending desired position
target_az = float(cmd[1])
target_el = float(cmd[2])
print('Gpredict requesting move to:', target_az,', ', target_el)
print('\n')
#Set movement a little farther than requested to "leap-frog" over position.
#This helps avoid the antenna lagging behind the satellite in the middle of the pass
if target_az - current_az > 2:
target_az+=1
elif target_az - current_az < -2:
target_az-=1
elif target_az - current_az > 1:
target_az+=0.5
elif target_az - current_az < -1:
target_az-=0.5
if target_el - current_el > 2:
target_az+=1
elif target_el - current_el < -2:
target_az-=1
elif target_el - current_el > 1:
target_az+=0.5
elif target_el - current_el < -1:
target_az-=0.5
#tell Antenna to move to target position
#Alternate AZ and EL motions to avoid one waiting on the other
if (move_nbr % 2) == 0: #Even numbered step, send AZ first
command = ('a 0 ' + str(target_az) + '\r').encode('ascii') #move azimuth motor
antenna.write(command)
#tell Antenna to move to target el
if (target_el < 15): #HAL 2.05 won't reliably go below 15 degrees with this method
target_el = 15
command = ('a 1 ' + str(target_el) + '\r').encode('ascii') #move elevation motor
antenna.write(command)
else: #Odd numbered step, send EL first
#tell Antenna to move to target el
if (target_el < 15):
target_el = 15
command = ('a 1 ' + str(target_el) + '\r').encode('ascii') #move elevation motor
antenna.write(command)
#tell Antenna to move to target az
command = ('a 0 ' + str(target_az) + '\r').encode('ascii') #move azimuth motor
antenna.write(command)
#Tell Gpredict things went correctly
response="RPRT 0\n " #Everything's under control, situation normal
conn.send(response.encode('utf-8'))
move_nbr+=1 #increment step counter
elif cmd[0] == "S": #Gpredict says to stop, so exit loop and listen for a new connection
print('Gpredict disconnected!')
move_nbr = 0
conn.close()
break
elif cmd[0] == "_": #Gpredict asks for model name (does it ever do this?)
response = "Saveitforparts Winegard Trav'ler Interface v2.0"
conn.send(response.encode('utf-8'))
else:
print('Exiting.')
antenna.write(bytes(b'q\r')) #go back to root menu
antenna.write(bytes(b'\r'))
conn.close()
antenna.close()
exit()