PYTHON   67

pwi4 client old py

Guest on 15th August 2022 12:43:42 PM

  1. try:
  2.     # Python 3.x version
  3.     from urllib.parse import urlencode
  4.     from urllib.request import urlopen
  5.     from urllib.error import HTTPError
  6. except ImportError:
  7.     # Python 2.7 version
  8.     from urllib import urlencode
  9.     from urllib2 import urlopen, HTTPError
  10.  
  11. class PWI4:
  12.     """
  13.    Client to the PWI4 telescope control application.
  14.    """
  15.  
  16.     def __init__(self, host="localhost", port=8220):
  17.         self.host = host
  18.         self.port = port
  19.         self.comm = PWI4HttpCommunicator(host, port)
  20.  
  21.     ### High-level methods #################################
  22.  
  23.     def status(self):
  24.         return self.request_with_status("/status")
  25.  
  26.     def mount_connect(self):
  27.         return self.request_with_status("/mount/connect")
  28.  
  29.     def mount_disconnect(self):
  30.         return self.request_with_status("/mount/disconnect")
  31.  
  32.     def mount_enable(self, axisNum):
  33.         return self.request_with_status("/mount/enable", axis=axisNum)
  34.  
  35.     def mount_disable(self, axisNum):
  36.         return self.request_with_status("/mount/disable", axis=axisNum)
  37.  
  38.     def mount_set_slew_time_constant(self, value):
  39.         return self.request_with_status("/mount/set_slew_time_constant", value=value)
  40.  
  41.     def mount_find_home(self):
  42.         return self.request_with_status("/mount/find_home")
  43.  
  44.     def mount_stop(self):
  45.         return self.request_with_status("/mount/stop")
  46.  
  47.     def mount_goto_ra_dec_apparent(self, ra_hours, dec_degs):
  48.         return self.request_with_status("/mount/goto_ra_dec_apparent", ra_hours=ra_hours, dec_degs=dec_degs)
  49.  
  50.     def mount_goto_ra_dec_j2000(self, ra_hours, dec_degs):
  51.         return self.request_with_status("/mount/goto_ra_dec_j2000", ra_hours=ra_hours, dec_degs=dec_degs)
  52.  
  53.     def mount_goto_alt_az(self, alt_degs, az_degs):
  54.         return self.request_with_status("/mount/goto_alt_az", alt_degs=alt_degs, az_degs=az_degs)
  55.  
  56.     def mount_offset(self, **kwargs):
  57.         """
  58.        One or more of the following offsets can be specified as a keyword argument:
  59.  
  60.        AXIS_reset: Clear all position and rate offsets for this axis. Set this to any value to issue the command.
  61.        AXIS_stop_rate: Set any active offset rate to zero. Set this to any value to issue the command.
  62.        AXIS_add_arcsec: Increase the current position offset by the specified amount
  63.        AXIS_set_rate_arcsec_per_sec: Continually increase the offset at the specified rate
  64.  
  65.        Where AXIS can be one of:
  66.  
  67.        ra: Offset the target Right Ascension coordinate
  68.        dec: Offset the target Declination coordinate
  69.        axis0: Offset the mount's primary axis position
  70.               (roughly Azimuth on an Alt-Az mount, or RA on In equatorial mount)
  71.        axis1: Offset the mount's secondary axis position
  72.               (roughly Altitude on an Alt-Az mount, or Dec on an equatorial mount)
  73.        path: Offset along the direction of travel for a moving target
  74.        transverse: Offset perpendicular to the direction of travel for a moving target
  75.  
  76.        For example, to offset axis0 by -30 arcseconds and have it continually increase at 1
  77.        arcsec/sec, and to also clear any existing offset in the transverse direction,
  78.        you could call the method like this:
  79.  
  80.        mount_offset(axis0_add_arcsec=-30, axis0_set_rate_arcsec_per_sec=1, transverse_reset=0)
  81.  
  82.        """
  83.  
  84.         return self.request_with_status("/mount/offset", **kwargs)
  85.  
  86.     def mount_park(self):
  87.         return self.request_with_status("/mount/park")
  88.  
  89.     def mount_set_park_here(self):
  90.         return self.request_with_status("/mount/set_park_here")
  91.  
  92.     def mount_tracking_on(self):
  93.         return self.request_with_status("/mount/tracking_on")
  94.  
  95.     def mount_tracking_off(self):
  96.         return self.request_with_status("/mount/tracking_off")
  97.  
  98.     def mount_follow_tle(self, tle_line_1, tle_line_2, tle_line_3):
  99.         return self.request_with_status("/mount/follow_tle", line1=tle_line_1, line2=tle_line_2, line3=tle_line_3)
  100.  
  101.     def mount_radecpath_new(self):
  102.         return self.request_with_status("/mount/radecpath/new")
  103.  
  104.     def mount_radecpath_add_point(self, jd, ra_j2000_hours, dec_j2000_degs):
  105.         return self.request_with_status("/mount/radecpath/add_point", jd=jd, ra_j2000_hours=ra_j2000_hours, dec_j2000_degs=dec_j2000_degs)
  106.  
  107.     def mount_radecpath_apply(self):
  108.         return self.request_with_status("/mount/radecpath/apply")
  109.  
  110.     def mount_model_add_point(self, ra_j2000_hours, dec_j2000_degs):
  111.         return self.request_with_status("/mount/model/add_point", ra_j2000_hours=ra_j2000_hours, dec_j2000_degs=dec_j2000_degs)
  112.  
  113.     def mount_model_clear_points(self):
  114.         return self.request_with_status("/mount/model/clear_points")
  115.  
  116.     def mount_model_save_as_default(self):
  117.         return self.request_with_status("/mount/model/save_as_default")
  118.  
  119.     def mount_model_save(self, filename):
  120.         return self.request_with_status("/mount/model/save", filename=filename)
  121.  
  122.     def mount_model_load(self, filename):
  123.         return self.request_with_status("/mount/model/load", filename=filename)
  124.  
  125.     def focuser_enable(self):
  126.         return self.request_with_status("/focuser/enable")
  127.  
  128.     def focuser_disable(self):
  129.         return self.request_with_status("/focuser/disable")
  130.  
  131.     def focuser_goto(self, target):
  132.         return self.request_with_status("/focuser/goto", target=target)
  133.  
  134.     def focuser_stop(self):
  135.         return self.request_with_status("/focuser/stop")
  136.  
  137.     def rotator_enable(self):
  138.         return self.request_with_status("/rotator/enable")
  139.  
  140.     def rotator_disable(self):
  141.         return self.request_with_status("/rotator/disable")
  142.        
  143.     def rotator_goto_mech(self, target_degs):
  144.         return self.request_with_status("/rotator/goto_mech", degs=target_degs)
  145.  
  146.     def rotator_goto_field(self, target_degs):
  147.         return self.request_with_status("/rotator/goto_field", degs=target_degs)
  148.  
  149.     def rotator_offset(self, offset_degs):
  150.         return self.request_with_status("/rotator/offset", degs=offset_degs)
  151.  
  152.     def rotator_stop(self):
  153.         return self.request_with_status("/rotator/stop")
  154.  
  155.     def m3_goto(self, target_port):
  156.         return self.request_with_status("/m3/goto", port=target_port)
  157.  
  158.     def m3_stop(self):
  159.         return self.request_with_status("/m3/stop")
  160.  
  161.     def virtualcamera_take_image(self):
  162.         """
  163.        Returns a string containing a FITS image simulating a starfield
  164.        at the current telescope position
  165.        """
  166.         return self.request("/virtualcamera/take_image")
  167.    
  168.     def virtualcamera_take_image_and_save(self, filename):
  169.         """
  170.        Request a fake FITS image from PWI4.
  171.        Save the contents to the specified filename
  172.        """
  173.  
  174.         contents = self.virtualcamera_take_image()
  175.         f = open(filename, "wb")
  176.         f.write(contents)
  177.         f.close()
  178.  
  179.     ### Methods for testing error handling ######################
  180.  
  181.     def test_command_not_found(self):
  182.         """
  183.        Try making a request to a URL that does not exist.
  184.        Useful for intentionally testing how the library will respond.
  185.        """
  186.         return self.request_with_status("/command/notfound")
  187.  
  188.     def test_internal_server_error(self):
  189.         """
  190.        Try making a request to a URL that will return a 500
  191.        server error due to an intentionally unhandled error.
  192.        Useful for testing how the library will respond.
  193.        """
  194.         return self.request_with_status("/internal/crash")
  195.    
  196.     def test_invalid_parameters(self):
  197.         """
  198.        Try making a request with intentionally missing parameters.
  199.        Useful for testing how the library will respond.
  200.        """
  201.         return self.request_with_status("/mount/goto_ra_dec_apparent")
  202.  
  203.     ### Low-level methods for issuing requests ##################
  204.  
  205.     def request(self, command, **kwargs):
  206.         return self.comm.request(command, **kwargs)
  207.  
  208.     def request_with_status(self, command, **kwargs):
  209.         response_text = self.request(command, **kwargs)
  210.         return self.parse_status(response_text)
  211.    
  212.     ### Status parsing utilities ################################
  213.  
  214.     def status_text_to_dict(self, response):
  215.         """
  216.        Given text with keyword=value pairs separated by newlines,
  217.        return a dictionary with the equivalent contents.
  218.        """
  219.  
  220.         # In Python 3, response is of type "bytes".
  221.         # Convert it to a string for processing below
  222.         if type(response) == bytes:
  223.             response = response.decode('utf-8')
  224.  
  225.         response_dict = {}
  226.  
  227.         lines = response.split("\n")
  228.        
  229.         for line in lines:
  230.             fields = line.split("=", 1)
  231.             if len(fields) == 2:
  232.                 name = fields[0]
  233.                 value = fields[1]
  234.                 response_dict[name] = value
  235.        
  236.         return response_dict
  237.  
  238.     def parse_status(self, response_text):
  239.         response_dict = self.status_text_to_dict(response_text)
  240.         return PWI4Status(response_dict)
  241.    
  242.  
  243.    
  244. class Section(object):
  245.     """
  246.    Simple object for collecting properties in PWI4Status
  247.    """
  248.  
  249.     pass
  250.  
  251. class PWI4Status:
  252.     """
  253.    Wraps the status response for many PWI4 commands in a class with named members
  254.    """
  255.  
  256.     def __init__(self, status_dict):
  257.         self.raw = status_dict  # Allow direct access to raw entries as needed
  258.  
  259.         self.pwi4 = Section()
  260.         self.pwi4.version = "<unknown>"
  261.         self.pwi4.version_field = [0, 0, 0, 0]
  262.  
  263.         self.pwi4.version = self.raw["pwi4.version"] # Added in 4.0.5 beta 1
  264.  
  265.         # pwi4.version_field[] was added in 4.0.9 beta 2
  266.         self.pwi4.version_field[0] = self.get_int("pwi4.version_field[0]", 0)
  267.         self.pwi4.version_field[1] = self.get_int("pwi4.version_field[1]", 0)
  268.         self.pwi4.version_field[2] = self.get_int("pwi4.version_field[2]", 0)
  269.         self.pwi4.version_field[3] = self.get_int("pwi4.version_field[3]", 0)
  270.  
  271.         # response.timestamp_utc was added in 4.0.9 beta 2
  272.         self.response = Section()
  273.         self.response.timestamp_utc = self.get_string("response.timestamp_utc")
  274.  
  275.  
  276.         self.site = Section()
  277.         self.site.latitude_degs = self.get_float("site.latitude_degs")
  278.         self.site.longitude_degs = self.get_float("site.longitude_degs")
  279.         self.site.height_meters = self.get_float("site.height_meters")
  280.         self.site.lmst_hours = self.get_float("site.lmst_hours")
  281.  
  282.         self.mount = Section()
  283.         self.mount.is_connected = self.get_bool("mount.is_connected")
  284.         self.mount.geometry = self.get_int("mount.geometry")
  285.         self.mount.julian_date = self.get_float("mount.julian_date")  # Added in 4.0.9 beta 2
  286.         self.mount.slew_time_constant = self.get_float("mount.slew_time_constant")  # Added in 4.0.9 beta 2
  287.         self.mount.ra_apparent_hours = self.get_float("mount.ra_apparent_hours")
  288.         self.mount.dec_apparent_degs = self.get_float("mount.dec_apparent_degs")
  289.         self.mount.ra_j2000_hours = self.get_float("mount.ra_j2000_hours")
  290.         self.mount.dec_j2000_degs = self.get_float("mount.dec_j2000_degs")
  291.         self.mount.azimuth_degs = self.get_float("mount.azimuth_degs")
  292.         self.mount.altitude_degs = self.get_float("mount.altitude_degs")
  293.         self.mount.is_slewing = self.get_bool("mount.is_slewing")
  294.         self.mount.is_tracking = self.get_bool("mount.is_tracking")
  295.         self.mount.field_angle_here_degs = self.get_float("mount.field_angle_here_degs")
  296.         self.mount.field_angle_at_target_degs = self.get_float("mount.field_angle_at_target_degs")
  297.         self.mount.field_angle_rate_at_target_degs_per_sec = self.get_float("mount.field_angle_rate_at_target_degs_per_sec")
  298.        
  299.         self.mount.axis0 = Section()
  300.         self.mount.axis0.is_enabled = self.get_bool("mount.axis0.is_enabled")
  301.         self.mount.axis0.rms_error_arcsec = self.get_float("mount.axis0.rms_error_arcsec")
  302.         self.mount.axis0.dist_to_target_arcsec = self.get_float("mount.axis0.dist_to_target_arcsec")
  303.         self.mount.axis0.servo_error_arcsec = self.get_float("mount.axis0.servo_error_arcsec")
  304.         self.mount.axis0.position_degs = self.get_float("mount.axis0.position_degs")
  305.         self.mount.axis0.position_timestamp_str = self.get_string("mount.axis0.position_timestamp") # Added in 4.0.9 beta 2
  306.        
  307.         self.mount.axis1 = Section()
  308.         self.mount.axis1.is_enabled = self.get_bool("mount.axis1.is_enabled")
  309.         self.mount.axis1.rms_error_arcsec = self.get_float("mount.axis1.rms_error_arcsec")
  310.         self.mount.axis1.dist_to_target_arcsec = self.get_float("mount.axis1.dist_to_target_arcsec")
  311.         self.mount.axis1.servo_error_arcsec = self.get_float("mount.axis1.servo_error_arcsec")
  312.         self.mount.axis1.position_degs = self.get_float("mount.axis1.position_degs")
  313.         self.mount.axis1.position_timestamp_str = self.get_string("mount.axis1.position_timestamp") # Added in 4.0.9 beta 2
  314.  
  315.         self.mount.model = Section()
  316.         self.mount.model.filename = self.get_string("mount.model.filename")
  317.         self.mount.model.num_points_total = self.get_int("mount.model.num_points_total")
  318.         self.mount.model.num_points_enabled = self.get_int("mount.model.num_points_enabled")
  319.         self.mount.model.rms_error_arcsec = self.get_float("mount.model.rms_error_arcsec")
  320.  
  321.         self.focuser = Section()
  322.         self.focuser.is_connected = self.get_bool("focuser.is_enabled")
  323.         self.focuser.is_enabled = self.get_bool("focuser.is_enabled")
  324.         self.focuser.position = self.get_float("focuser.position")
  325.         self.focuser.is_moving = self.get_bool("focuser.is_moving")
  326.        
  327.         self.rotator = Section()
  328.         self.rotator.is_connected = self.get_bool("rotator.is_connected")
  329.         self.rotator.is_enabled = self.get_bool("rotator.is_enabled")
  330.         self.rotator.mech_position_degs = self.get_float("rotator.mech_position_degs")
  331.         self.rotator.field_angle_degs = self.get_float("rotator.field_angle_degs")
  332.         self.rotator.is_moving = self.get_bool("rotator.is_moving")
  333.         self.rotator.is_slewing = self.get_bool("rotator.is_slewing")
  334.  
  335.         self.m3 = Section()
  336.         self.m3.port = self.get_int("m3.port")
  337.  
  338.     def get_bool(self, name, value_if_missing=None):
  339.         if name not in self.raw:
  340.             return value_if_missing
  341.         return self.raw[name].lower() == "true"
  342.  
  343.     def get_float(self, name, value_if_missing=None):
  344.         if name not in self.raw:
  345.             return value_if_missing
  346.         return float(self.raw[name])
  347.  
  348.     def get_int(self, name, value_if_missing=None):
  349.         if name not in self.raw:
  350.             return value_if_missing
  351.         return int(self.raw[name])
  352.    
  353.     def get_string(self, name, value_if_missing=None):
  354.         if name not in self.raw:
  355.             return value_if_missing
  356.         return self.raw[name]
  357.  
  358.     def __repr__(self):
  359.         """
  360.        Format all of the keywords and values we have received
  361.        """
  362.  
  363.         max_key_length = max(len(x) for x in self.raw.keys())
  364.  
  365.         lines = []
  366.  
  367.         line_format = "%-" + str(max_key_length) + "s: %s"
  368.  
  369.         for key in sorted(self.raw.keys()):
  370.             value = self.raw[key]
  371.             lines.append(line_format % (key, value))
  372.         return "\n".join(lines)
  373.  
  374. class PWI4HttpCommunicator:
  375.     """
  376.    Manages communication with PWI4 via HTTP.
  377.    """
  378.  
  379.     def __init__(self, host="localhost", port=8220):
  380.         self.host = host
  381.         self.port = port
  382.  
  383.         self.timeout_seconds = 3
  384.  
  385.     def make_url(self, path, **kwargs):
  386.         """
  387.        Utility function that takes a set of keyword=value arguments
  388.        and converts them into a properly formatted URL to send to PWI.
  389.        Special characters (spaces, colons, plus symbols, etc.) are encoded as needed.
  390.  
  391.        Example:
  392.          make_url("/mount/gotoradec2000", ra=10.123, dec="15 30 45") -> "http://localhost:8220/mount/gotoradec2000?ra=10.123&dec=15%2030%2045"
  393.        """
  394.  
  395.         # Construct the basic URL, excluding the keyword parameters; for example: "http://localhost:8220/specified/path?"
  396.         url = "http://" + self.host + ":" + str(self.port) + path + "?"
  397.  
  398.         # For every keyword=value argument given to this function,
  399.         # construct a string of the form "key1=val1&key2=val2".
  400.         keyword_values = list(kwargs.items()) # Need to explicitly convert this to list() for Python 3.x
  401.         urlparams = urlencode(keyword_values)
  402.  
  403.         # In URLs, spaces can be encoded as "+" characters or as "%20".
  404.         # This will convert plus symbols to percent encoding for improved compatibility.
  405.         urlparams = urlparams.replace("+", "%20")
  406.  
  407.         # Build the final URL and return it.
  408.         url = url + urlparams
  409.         return url
  410.  
  411.     def request(self, path, **kwargs):
  412.         """
  413.        Issue a request to PWI using the keyword=value parameters
  414.        supplied to the function, and return the response received from
  415.        PWI.
  416.  
  417.        Example:
  418.          pwi_request("/mount/gotoradec2000", ra=10.123, dec="15 30 45")
  419.        
  420.        will construct the appropriate URL and issue the request to the server.
  421.  
  422.        The server response payload will be returned, or an exception will be thrown
  423.        if there was an error with the request.
  424.        """
  425.  
  426.         # Construct the URL that we will request
  427.         url = self.make_url(path, **kwargs)
  428.  
  429.         # Open a connection to the server, issue the request, and try to receive the response.
  430.         # The server will return an HTTP Status Code as part of the response.
  431.         # If the status code indicates an error, an HTTPError will be thrown.
  432.         try:
  433.             response = urlopen(url, timeout=self.timeout_seconds)
  434.         except HTTPError as e:
  435.             if e.code == 404:
  436.                 error_message = "Command not found"
  437.             elif e.code == 400:
  438.                 error_message = "Bad request"
  439.             elif e.code == 500:
  440.                 error_message = "Internal server error (possibly a bug in PWI)"
  441.             else:
  442.                 error_message = str(e)
  443.  
  444.             try:
  445.                 error_details = e.read()  # Try to read the payload of the response for error information
  446.                 error_message = error_message + ": " + error_details
  447.             except:
  448.                 pass # If that failed, we won't include any further details
  449.            
  450.             raise Exception(error_message) # TODO: Consider a custom exception here
  451.  
  452.            
  453.         except Exception as e:
  454.             # This will often be a urllib2.URLError to indicate that a connection
  455.             # could not be made to the server, but we'll handle any exception here
  456.             raise
  457.  
  458.         payload = response.read()
  459.         return payload

Raw Paste


Login or Register to edit or fork this paste. It's free.