Re-packaging for initial public release.

(Happy Halloween!)
master
Michael Toren 2017-10-31 14:05:00 -07:00
commit 800a25eecd
3 changed files with 361 additions and 0 deletions

26
LICENSE 100644
View File

@ -0,0 +1,26 @@
Copyright (c) 2017, Astro Digital, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of Astro Digital, Inc.

49
README.md 100644
View File

@ -0,0 +1,49 @@
# greenctld
A hamlib-compatible driver for the [Green Heron Engineering RT-21 Digital Rotor
Controller](https://www.greenheronengineering.com/prod_documents/controllers/docs/RT-21_Manual_current.pdf).
hamlib does not support the rotor, but this program can be used as a drop-in
replacement for rotctld when commanding the RT-21.
The RT-21 is unlike most of the other rotors hamlib supports in that it uses
two serial ports, one for controlling azimuth and one for controlling
elevation. The two serial ports are passed via the ```--az-device``` and
```--el-device``` command line arguments.
The TCP network protocol is compatible with the hamlib protocol documented in
the [rotctld(8) man
page](http://manpages.ubuntu.com/manpages/zesty/man8/rotctld.8.html). This
driver only implements a subset of that protocol, which includes the subset
that [gpredict](http://gpredict.oz9aec.net/) uses. At [Astro
Digital](https://astrodigital.com/), this driver has been used extensively with
gpredict. For debugging the network protocol, the ```--dummy``` option can be
used to simulate a rotor without connecting to a real serial port.
Like rotctld, this program does not daemonize on its own. It also produces
copious debugging output to stdout.
### Usage
* ```--az-device <serial-port>```, the serial port to use for azimuth
* ```--el-device <serial-port>```, the serial port to use for elevation
* ```--speed <baud>```, the serial baud rate to use, defaults to 4800
* ```--timeout <seconds>```, the serial port timeout, defaults to 1.5
* ```--port <port>```, the TCP port to listen for connections on, defaults to 4533, the same as rotctld
* ```--get-pos```, to query the serial ports for the current az/el, and immediately exit. Useful for testing the serial port configuration.
* ```--dummy```, to speak the TCP network protocol only without connecting to a serial port, useful for debugging gpredict integration.
### License
Copyright (c) 2017 [Astro Digital, Inc](https://astrodigital.com/)
Released under the terms of the Simplified BSD License; see the [LICENSE](LICENSE) file for details.
### Author
Michael Toren &lt;mct@toren.net&gt;

286
greenctld 100755
View File

@ -0,0 +1,286 @@
#!/usr/bin/env python2
# vim:set ts=4 sw=4 sts=4 ai et smarttab:
# greenctld, a hamlib-compatible driver for the Green Heron Engineering RT-21
# Digital Rotor Controller. The Green Heron serial protocol is documented at
# https://www.greenheronengineering.com/prod_documents/controllers/docs/RT-21_Manual_current.pdf
#
# The TCP network protocol is compatible with the hamlib rotctld protocol, which
# gpredict speaks.
#
# Copyright (c) 2017, Astro Digital, Inc. Released under the terms of the
# Simplified BSD License; see the LICENSE file for details.
#
# -- Michael Toren <mct@toren.net>
# Astro Digital, Inc.
import socket
import serial
import argparse
import traceback
import time
import select
import sys
import re
import os
class DummyRotor(object):
'''
A fake Rotor class, useful for debugging the TCPServer class on any
machine, even if the rotor is not physically connected to it.
'''
az = 0
el = 0
def set_pos(self, az, el):
print '==> %d,%d' % (az, el)
self.az = az
self.el = el
def get_pos(self):
print '<== %d,%d' % (self.az, self.el)
return (self.az, self.el,)
def stop(self):
print "==> Stop"
class GreenHeronRotor(object):
'''
Driver for the Green Heron Engineering RT-21 Digital Rotor Controller
'''
az_serial = None
el_serial = None
def __init__(self, az_device, el_device, baud, timeout):
self.az_serial = serial.Serial(az_device, baudrate=baud, timeout=timeout)
self.el_serial = serial.Serial(el_device, baudrate=baud, timeout=timeout)
print '--- Serial timeout set to', timeout
def stop(self):
print "==> Stop"
self.az_serial.write(';')
self.el_serial.write(';')
def set_pos(self, az, el):
print '==> %d,%d' % (az, el)
self.az_serial.write('AP1%03d\r;' % az)
self.el_serial.write('AP1%03d\r;' % el)
time.sleep(0.1)
def __parse_response(self, buf):
match = re.match(r'^([0-9][0-9][0-9]);$', buf)
if not match:
return -1
ret = match.groups()[0]
ret = int(ret)
return ret
def get_pos(self):
'''
On success, returns a tuple of (az, el)
On failure, returns False
'''
self.az_serial.flushInput()
self.el_serial.flushInput()
self.az_serial.write('AI1;')
self.el_serial.write('AI1;')
az_buf = self.az_serial.read(4)
el_buf = self.el_serial.read(4)
if len(az_buf) != 4 or len(el_buf) != 4:
print '!!! Serial read failure, received %s and %s' % (repr(az_buf), repr(el_buf))
return False
az = self.__parse_response(az_buf)
el = self.__parse_response(el_buf)
if az < 0 or el < 0:
print '!!! Failed to parse response, received %s and %s' % (repr(az_buf), repr(el_buf))
return False
print '<== %d,%d' % (az, el)
return (az, el,)
class TCPServer(object):
'''
Implements a subset of the rotctld TCP protocol. gpredict only sends three
commands, all of which are supported:
- "p" to request the current position
- "P <az> <el>" to set the desired position
- "q" to quit
This driver also supports:
- "S" to stop any current movement
'''
# A mapping of client fd's -> receive buffers
client_buf = {}
def __init__(self, port, rotor, ip=''):
self.rotor = rotor
self.listener = socket.socket()
self.listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.listener.bind((ip, port))
self.listener.listen(4)
addr = self.listener.getsockname()
print '--- Listening for connections on %s:%d' % (addr[0], addr[1])
def close_client(self, fd):
self.rotor.stop()
try:
fd.close()
del self.client_buf[fd]
except:
pass
def parse_client_command(self, fd, cmd):
cmd = cmd.strip()
if cmd == '':
return
print '<-- %s' % repr(cmd)
# "q", to quit
if cmd == 'q':
self.close_client(fd)
return
# "S", to stop the current rotation
if cmd == 'S':
self.rotor.stop()
print '--> RPRT 0'
fd.send('RPRT 0\n')
return
# "p", to get current position
if cmd == 'p':
pos = self.rotor.get_pos()
if not pos:
print '--> RPRT -6'
fd.send('RPRT -6\n')
else:
az, el = pos
print '--> %d,%d' % (az, el)
fd.send('%.6f\n%.6f\n' % (az, el))
return
# "P <az> <el>" to set desired position
match = re.match(r'^P\s+([\d.]+)\s+([\d.]+)$', cmd)
if match:
az = match.groups()[0]
el = match.groups()[1]
try:
az = int(float(az))
el = int(float(el))
except:
print '--> RPRT -8 (could not parse)'
fd.send('RPRT -8\n')
return
if az == 360:
az = 359
if az > 359:
print '--> RPRT -1 (az too large)'
fd.send('RPRT -1\n')
return
if el > 90:
print '--> RPRT -1 (el too large)'
fd.send('RPRT -1\n')
return
self.rotor.set_pos(az, el)
print '--> RPRT 0'
fd.send('RPRT 0\n')
return
# Nothing else is supported
print '--> RPRT -4 (unknown command)'
fd.send('RPRT -4\n')
def read_client(self, fd):
buf = fd.recv(1024)
if len(buf) == 0:
print '<-- EOF'
self.close_client(fd)
return
self.client_buf[fd] += buf
while True:
cmd, sep, tail = self.client_buf[fd].partition('\n')
# Check if a full line of input is present
if not sep:
return
else:
self.client_buf[fd] = tail
self.parse_client_command(fd, cmd)
# Check if the client sent a "q", to quit
if not self.client_buf.has_key(fd):
return
def __run_once(self):
rlist = [ self.listener ] + self.client_buf.keys()
wlist = []
xlist = []
rlist, wlist, xlist = select.select(rlist, wlist, xlist)
for fd in rlist:
if fd == self.listener:
new_fd, addr = self.listener.accept()
new_fd.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024*16)
new_fd.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024*16)
new_fd.setblocking(False)
self.client_buf[new_fd] = ''
print '<-- Connect %s:%d' % (addr[0], addr[1])
else:
try:
self.read_client(fd)
except Exception as e:
print 'Unhandled exception, killing client and issuing motor stop command:'
traceback.print_exc()
self.close_client(fd)
print
def loop(self):
while True:
self.__run_once()
if __name__ == '__main__':
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
parser = argparse.ArgumentParser()
parser.add_argument('--az-device', '-a', type=str, required=True, help='Serial device for azimuth')
parser.add_argument('--el-device', '-e', type=str, required=True, help='Serial device for elevation')
parser.add_argument('--speed', '-s', type=int, default=4800, help='Serial device speed')
parser.add_argument('--timeout', '-t', type=float, default=1.5, help='Serial timeout')
parser.add_argument('--port', '-p', type=int, default=4533, help='TCP port')
parser.add_argument('--get-pos', '-g', action='store_true', help='Issue get position, and exit; for serial comms test')
parser.add_argument('--dummy', action='store_true', help='Use a dummy rotor, not the real serial device')
args = parser.parse_args()
if args.dummy:
rotor = DummyRotor()
else:
rotor = GreenHeronRotor(args.az_device, args.el_device, args.speed, args.timeout)
if args.get_pos:
print rotor.get_pos()
sys.exit(0)
server = TCPServer(args.port, rotor)
server.loop()