#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Interface with an Astelco OpenTSI-based device
"""
# System modules
import telnetlib
# Other templates
from ..utils import check
from ..utils.logger import log
from .basedevice import BaseDevice
[docs]
class OpenTSI(BaseDevice):
'''Base wrapper class for astelco OpenTSI stuff'''
def __init__(self, url):
'''Constructor.'''
super().__init__(url)
self.url = url.split(":")[0] or url
self.port = url.split(":")[1] or 22
self.timeout = 3
self.connection = True
@check.telnet_errors
def _connect(self):
'''Setup a telnet connection to the cabinet.'''
tn = telnetlib.Telnet(self.url, self.port, timeout=3)
self.connection = True
return tn
@check.telnet_errors
def _send(self, get_or_set, message=None):
'''
Connect to the cabinet and send messages to set or get properties
'''
# Login data for cabinet communication.
# It is a byte-string.
auth = b'auth plain "admin" "admin"\n'
# Send the message. it can be a string (even a multi-line
# string), or a list of strings
if isinstance(message, str):
# Maybe is multi-line
msglist = message.split("\n")
# Removing empty elements
msglist = [x for x in msglist if x]
elif isinstance(message, list):
msglist = message
# GET: Transforming every string in "1 get string\n2 get string\n"
# SET: Transforming every string in "1 set string=val\n2 set
# string=val"
msg = [f"{i + 1} {get_or_set.upper()} {j}\n" for i,
j in enumerate(msglist)]
# From list to string, and byte-coded
msg = bytes("".join(msg), "utf+8")
# Connect to the cabinet.
tn = self._connect()
if not tn:
self.connection = False
return None # {"Error": self.error}
tn.read_until(b"[ruder]\n")
tn.write(auth)
tn.read_until(b"AUTH OK 0 0\n")
# log.debug(msg)
# Send the message.
tn.write(msg)
# For each command, the *asyncrhronous* answer contains:
#
# COMMAND OK
# DATA INLINE ...
# COMMAND COMPLETE
#
# Append all the buffer line by line to a list.
tn_answer = []
for i in range(msg.count(b"\n") * 3):
tn_answer.append(tn.read_until(b"\n"))
# Close the cabinet connection.
tn.close()
# log.debug(tn_answer)
# Keep only the data.
tn_values = [s for s in tn_answer if b"DATA INLINE" in s]
# Create a python dictionary with name and value, as they are async.
telnet_dict = dict([m[14:-1].decode().split("=") for m in tn_values])
# # It can happen that values are missing, i.e. with cabinet off
# if get_or_set == "get":
# if len(telnet_dict) != len(msglist):
# log.warning(f"Astelco asked {len(msglist)} values, {len(telnet_dict)} returned.")
# log.debug(f"Asked: {msglist}")
# log.debug(f"Returned: {telnet_dict}")
# Try transforming values to int or float.
for k, v in telnet_dict.items():
if get_or_set == "set":
print(k, v)
try:
telnet_dict[k] = int(v)
except BaseException:
try:
telnet_dict[k] = float(v)
except BaseException:
try:
telnet_dict[k] = str(v.replace('"', ''))
except ValueError as e:
log.error("Astelco class error:")
log.error(e)
# Order by message, add None if something is missing.
res = [telnet_dict.get(m, None) for m in msglist]
if len(res) > 1:
return res
else:
return res[0]
[docs]
def get(self, message):
'''Send a telnet message to the cabinet to retrieve a value or a set
of values.'''
return self._send("get", message)
[docs]
def put(self, key, val):
'''Send to telnet a key to set a a value.
keys and values can be both lists with the same element number.'''
if isinstance(key, list):
message = [f"{k}={v}" for k, v in zip(key, val)]
else:
message = f"{key}={val}"
return self._send("set", message)
[docs]
class Telescope(OpenTSI):
'''Implementation of the Telescope commands mocking my Alpaca
Telescope wrapper.'''
@property
def tracking(self):
'''Tracking status'''
message = "POINTING.TRACK"
res = self.get(message)
try:
self._tracking = True if int(res) else False
except TypeError as e:
self._tracking = None
return self._tracking
@tracking.setter
def tracking(self, b):
'''(Re)Start/Stop tracking of telescope in function of currently
configured target'''
''' 1 slew to currently configured target and start tracking'''
''' 0 stop tracking'''
message = "POINTING.TRACK"
track = 1 if b else 0
self.put(message, track)
return self.tracking
[docs]
def track(self):
'''More direct way to start tracking'''
self.tracking = True
return self._tracking
[docs]
def abort(self):
'''Direct way to stop tracking'''
'''Check if need to add other abort'''
self.tracking = False
return self._tracking
@property
def is_moving(self):
'''Check the motion state of the telescope'''
'''Bit 0 One or more axes are moving
Bit 1 Trajectories are being executed
Bit 2 Movement is blocked (e.g. by limits)
Bit 3 Telescope is on target position (i.e. stopped or, if tracking, following the target)
Bit 4 Movement is restricted by given jerk/acceleration/velocity limits
Bit 5 Telescope is “unparked”, i.e. moved on startup position by READY=1 or PARK=0
Bit 6 Telescope is “parked”, i.e. moved on park position by READY=0 or PARK=1'''
message = "TELESCOPE.MOTION_STATE"
res = self.get(message)
return res
@property
def status(self):
'''need to check exit and error from get function'''
message = "TELESCOPE.STATUS.LIST"
res = self.get(message)
return res
@property
def state(self):
message = "TELESCOPE.STATUS.GLOBAL"
res = self.get(message)
return res
[docs]
def clear(self, n):
res = self.put("TELESCOPE.STATUS.CLEAR_ERROR", n)
return res
@property
def clock(self):
"""
in UNIX time
"""
message = "POSITION.LOCAL.UTC"
res = self.get(message)
return res
@property
def cover(self):
message = "AUXILIARY.COVER.REALPOS"
res = self.get(message)
self._cover = res
return self._cover
@property
def open(self):
res = self.cover
if res == 1.0:
self._open = True
elif res == 0.0:
self._open = False
else:
self._open = None
return self._open
@open.setter
def open(self, b):
pos = 1.0 if b else 0.0
res = self.put("AUXILIARY.COVER.TARGETPOS", pos)
self._open = self.open
@property
def park(self):
'''Get if the telescope is in parked position'''
message = "TELESCOPE.READY_STATE"
res = self.get(message)
try:
self._park = False if int(res) else True
except (TypeError, ValueError) as e:
# ValueError for "FAILED 2"
self._park = None
return self._park
@park.setter
def park(self, b, timeout=90):
'''Init the telescope homing the axes,
or put the telescope in park position'''
message = "TELESCOPE.READY"
pos = 0.0 if b else 1.0
res = self.put(message, pos)
self._park = self.park
@property
def altaz(self):
# message = ["OBJECT.HORIZONTAL.ALT",
# "OBJECT.HORIZONTAL.AZ"]
message = ["POSITION.HORIZONTAL.ALT",
"POSITION.HORIZONTAL.AZ"]
res = self.get(message)
altaz = res
self._altaz = altaz # [alt, az]
return self._altaz
@altaz.setter
def altaz(self, a):
# self.tracking = False
keys = ["POINTING.SETUP.DEROTATOR.SYNCMODE",
"OBJECT.HORIZONTAL.ALT",
"OBJECT.HORIZONTAL.AZ",
"POINTING.TRACK"]
derot = 2 # from astelos log file "true orientation"
track = 2 # 2: go and stay there
values = [derot, a[0], a[1], track]
res = self.put(keys, values)
self._altaz = self.altaz
@property
def targetradec(self):
'''Get target Right Ascension / Declination'''
message = ["POSITION.EQUATORIAL.RA_J2000",
"POSITION.EQUATORIAL.DEC_J2000"]
res = self.get(message)
targetradec = res
self._targetradec = targetradec
return self._targetradec
@targetradec.setter
def targetradec(self, a):
'''Set target Right Ascension / Declination'''
keys = ["POINTING.SETUP.DEROTATOR.SYNCMODE",
"OBJECT.EQUATORIAL.RA",
"OBJECT.EQUATORIAL.DEC",
"POINTING.TRACK"]
derot = 2 # from astelos log file "true orientation"
track = 1 # 1: go and track
values = [derot, a[0], a[1], track]
res = self.put(keys, values)
self._targetradec = self.targetradec
@property
def radec(self):
'''Get Right Ascension / Declination'''
message = ["POSITION.EQUATORIAL.RA_J2000",
"POSITION.EQUATORIAL.DEC_J2000"]
res = self.get(message)
radec = res
self._radec = radec
return self._radec
@radec.setter
def radec(self, a):
'''Set Right Ascension / Declination'''
keys = ["POINTING.SETUP.DEROTATOR.SYNCMODE",
"OBJECT.EQUATORIAL.RA",
"OBJECT.EQUATORIAL.DEC",
"POINTING.TRACK"]
derot = 2 # from astelos log file "true orientation"
track = 1 # 1: go and track
values = [derot, a[0], a[1], track]
res = self.put(keys, values)
self._radec = self.radec
@property
def offset(self):
'''Get Telescope offset'''
message = ["POSITION.INSTRUMENTAL.ZD.OFFSET",
"POSITION.INSTRUMENTAL.AZ.OFFSET"]
res = self.get(message)
offset = res
self._offset = offset
return self._offset
@offset.setter
def offset(self, a):
'''Set Telescope offset'''
keys = ["POSITION.INSTRUMENTAL.ZD.OFFSET",
"POSITION.INSTRUMENTAL.AZ.OFFSET"]
values = [a[0], a[1]]
if abs(values[0]) > 0.9: # 54 arcmin
log.error(f"zd {a[0]} too large. Maybe arcsec instead of deg?")
return
if abs(values[1]) > 0.9: # 54 arcmin
log.error(f"az {a[1]} too large. Maybe arcsec instead of deg?")
return
res = self.put(keys, values)
self._offset = self.offset
@property
def coordinates(self):
"""
Ask simultaneously
RA2000, DEC2000, ALT, AZ, LST, UTC.
Answer is given as a dict of decimal numbers.
UTC is given as unix time.
"""
message = ["POSITION.EQUATORIAL.RA_J2000",
"POSITION.EQUATORIAL.DEC_J2000",
"POSITION.HORIZONTAL.ALT",
"POSITION.HORIZONTAL.AZ",
# "OBJECT.HORIZONTAL.ALT",
# "OBJECT.HORIZONTAL.AZ",
"POSITION.LOCAL.SIDEREAL_TIME",
"POSITION.LOCAL.UTC"]
res = self.get(message)
# print(telnet_dict)
simple_dict = {
"radec": [res[0],
res[1]],
"altaz": [res[2],
res[3]],
"lst": res[4],
"utc": res[5],
} if res else None
return simple_dict
[docs]
class Focuser(OpenTSI):
'''Implementation of the Focuser commands mocking my Alpaca Telescope
wrapper.'''
@property
def is_moving(self):
'''TBD with the right telnet commands'''
# log.warning("TBD with the right telnet commands")
@property
def position(self):
'''Get Relative focuser position from telnet'''
message = "POSITION.INSTRUMENTAL.FOCUS.OFFSET"
pos = self.get(message)
try:
res = float(pos) * 1000
except TypeError as e:
res = None
except ValueError as e: # "FAILED 1"
res = None
self._position = res
return self._position
@position.setter
def position(self, s): # 0-34500=micron?
'''Set Relative focuser position from telnet'''
message = "POSITION.INSTRUMENTAL.FOCUS.OFFSET"
pos = s / 1000
res = self.put(message, pos)
self._position = self.position
@property
def absolute(self):
'''Get Absolute focuser position from telnet'''
message = "POSITION.INSTRUMENTAL.FOCUS.REALPOS"
res = self.get(message)
self._absolute = res
return self._absolute
@absolute.setter
def absolute(self, s): # 0-34500=micron?
'''Set Absolute focuser position from telnet'''
message = "POSITION.INSTRUMENTAL.FOCUS.OFFSET"
pos = s / 1000
res = self.put(message, pos)
self._absolute = self.absolute
[docs]
class Rotator(OpenTSI):
'''Implementation of the Rotator commands mocking my Alpaca Telescope
wrapper.'''
@property
def is_moving(self):
'''TBD with the right telnet commands'''
# log.warning("TBD with the right telnet commands")
@property
def position(self):
'''Get Relative rotator position from telnet'''
message = "POSITION.INSTRUMENTAL.DEROTATOR[2].OFFSET"
res = self.get(message)
self._position = res
return self._position
@position.setter
def position(self, s): # 0-270 deg?
'''Set Relative rotator position from telnet'''
message = "POSITION.INSTRUMENTAL.DEROTATOR[2].OFFSET"
res = self.put(message, s)
self._position = self.position
@property
def absolute(self):
'''Get Absolute rotator position from telnet'''
message = "POSITION.INSTRUMENTAL.DEROTATOR[2].REALPOS"
res = self.get(message)
self._absolute = res
return self._absolute
[docs]
class Sensor(OpenTSI):
'''Implementation of a Sensor class.'''
def __init__(self, url, temp_id, hum_id):
'''Constructor.'''
super().__init__(url)
self.temp_id = temp_id # recycle for last_update
self.hum_id = hum_id
@property
def temperature(self):
'''Mirrors temperature'''
message = ["AUXILIARY.SENSOR[2].VALUE",
"AUXILIARY.SENSOR[3].VALUE",
"AUXILIARY.SENSOR[4].VALUE"]
res = self.get(message)
return res
@property
def humidity(self):
'''Does it have humidity sensors?'''