Source code for noctua.devices.astelco

#!/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?'''