PYTHON   75

pwi4 client py

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

  1. """
  2. This Python module wraps the calls and status responses provided
  3. by the HTTP API exposed by PWI4. This code can be called directly
  4. from other Python scripts, or can be adapted to other languages
  5. as needed.
  6. """
  7.  
  8. try:
  9.     # Python 3.x version
  10.     from urllib.parse import urlencode
  11.     from urllib.request import urlopen
  12.     from urllib.error import HTTPError
  13. except ImportError:
  14.     # Python 2.7 version
  15.     from urllib import urlencode
  16.     from urllib2 import urlopen, HTTPError
  17.  
  18. class PWI4:
  19.     """
  20.    Client to the PWI4 telescope control application.
  21.    """
  22.  
  23.     def __init__(self, host="localhost", port=8220):
  24.         self.host = host
  25.         self.port = port
  26.         self.comm = PWI4HttpCommunicator(host, port)
  27.  
  28.     ### High-level methods #################################
  29.  
  30.     def status(self):
  31.         return self.request_with_status("/status")
  32.  
  33.     def mount_connect(self):
  34.         return self.request_with_status("/mount/connect")
  35.  
  36.     def mount_disconnect(self):
  37.         return self.request_with_status("/mount/disconnect")
  38.  
  39.     def mount_enable(self, axisNum):
  40.         return self.request_with_status("/mount/enable", axis=axisNum)
  41.  
  42.     def mount_disable(self, axisNum):
  43.         return self.request_with_status("/mount/disable", axis=axisNum)
  44.  
  45.     def mount_set_slew_time_constant(self, value):
  46.         return self.request_with_status("/mount/set_slew_time_constant", value=value)
  47.  
  48.     def mount_set_axis0_wrap_range_min(self, axis0_wrap_min_degs):
  49.         # Added in PWI 4.0.13
  50.         return self.request_with_status("/mount/set_axis0_wrap_range_min", degs=axis0_wrap_min_degs)
  51.  
  52.     def mount_find_home(self):
  53.         return self.request_with_status("/mount/find_home")
  54.  
  55.     def mount_stop(self):
  56.         return self.request_with_status("/mount/stop")
  57.  
  58.     def mount_goto_ra_dec_apparent(self, ra_hours, dec_degs):
  59.         return self.request_with_status("/mount/goto_ra_dec_apparent", ra_hours=ra_hours, dec_degs=dec_degs)
  60.  
  61.     def mount_goto_ra_dec_j2000(self, ra_hours, dec_degs):
  62.         return self.request_with_status("/mount/goto_ra_dec_j2000", ra_hours=ra_hours, dec_degs=dec_degs)
  63.  
  64.     def mount_goto_alt_az(self, alt_degs, az_degs):
  65.         return self.request_with_status("/mount/goto_alt_az", alt_degs=alt_degs, az_degs=az_degs)
  66.  
  67.     def mount_goto_coord_pair(self, coord0, coord1, coord_type):
  68.         """
  69.        Set the mount target to a pair of coordinates in a specified coordinate system.
  70.        coord_type: can currently be "altaz" or "raw"
  71.        coord0: the azimuth coordinate for the "altaz" type, or the axis0 coordiate for the "raw" type
  72.        coord1: the altitude coordinate for the "altaz" type, or the axis1 coordinate for the "raw" type
  73.        """
  74.         return self.request_with_status("/mount/goto_coord_pair", c0=coord0, c1=coord1, type=coord_type)
  75.  
  76.     def mount_offset(self, **kwargs):
  77.         """
  78.        One or more of the following offsets can be specified as a keyword argument:
  79.  
  80.        AXIS_reset: Clear all position and rate offsets for this axis. Set this to any value to issue the command.
  81.        AXIS_stop_rate: Set any active offset rate to zero. Set this to any value to issue the command.
  82.        AXIS_add_arcsec: Increase the current position offset by the specified amount
  83.        AXIS_set_rate_arcsec_per_sec: Continually increase the offset at the specified rate
  84.  
  85.        As of PWI 4.0.11 Beta 7, the following options are also supported:
  86.        AXIS_stop: Stop both the offset rate and any gradually-applied commands
  87.        AXIS_stop_gradual_offset: Stop only the gradually-applied offset, and maintain the current rate
  88.        AXIS_set_total_arcsec: Set the total accumulated offset at the time the command is received to the specified value. Any in-progress rates or gradual offsets will continue to be applied on top of this.
  89.        AXIS_add_gradual_offset_arcsec: Gradually add the specified value to the total accumulated offset. Must be paired with AXIS_gradual_offset_rate or AXIS_gradual_offset_seconds to determine the timeframe over which the gradual offset is applied.
  90.        AXIS_gradual_offset_rate: Paired with AXIS_add_gradual_offset_arcsec; Specifies the rate at which a gradual offset should be applied. For example, if an offset of 10 arcseconds is to be applied at a rate of 2 arcsec/sec, then it will take 5 seconds for the offset to be applied.
  91.        AXIS_gradual_offset_seconds: Paired with AXIS_add_gradual_offset_arcsec; Specifies the time it should take to apply the gradual offset. For example, if an offset of 10 arcseconds is to be applied over a period of 2 seconds, then the offset will be increasing at a rate of 5 arcsec/sec.
  92.  
  93.        Where AXIS can be one of:
  94.  
  95.        ra: Offset the target Right Ascension coordinate
  96.        dec: Offset the target Declination coordinate
  97.        axis0: Offset the mount's primary axis position
  98.               (roughly Azimuth on an Alt-Az mount, or RA on In equatorial mount)
  99.        axis1: Offset the mount's secondary axis position
  100.               (roughly Altitude on an Alt-Az mount, or Dec on an equatorial mount)
  101.        path: Offset along the direction of travel for a moving target
  102.        transverse: Offset perpendicular to the direction of travel for a moving target
  103.  
  104.        For example, to offset axis0 by -30 arcseconds and have it continually increase at 1
  105.        arcsec/sec, and to also clear any existing offset in the transverse direction,
  106.        you could call the method like this:
  107.  
  108.        mount_offset(axis0_add_arcsec=-30, axis0_set_rate_arcsec_per_sec=1, transverse_reset=0)
  109.  
  110.        """
  111.  
  112.         return self.request_with_status("/mount/offset", **kwargs)
  113.  
  114.     def mount_spiral_offset_new(self, x_step_arcsec, y_step_arcsec):
  115.         # Added in PWI 4.0.11 Beta 8
  116.         return self.request_with_status("/mount/spiral_offset/new", x_step_arcsec=x_step_arcsec, y_step_arcsec=y_step_arcsec)
  117.  
  118.     def mount_spiral_offset_next(self):
  119.         # Added in PWI 4.0.11 Beta 8
  120.         return self.request_with_status("/mount/spiral_offset/next")
  121.  
  122.     def mount_spiral_offset_previous(self):
  123.         # Added in PWI 4.0.11 Beta 8
  124.         return self.request_with_status("/mount/spiral_offset/previous")
  125.  
  126.     def mount_park(self):
  127.         return self.request_with_status("/mount/park")
  128.  
  129.     def mount_set_park_here(self):
  130.         return self.request_with_status("/mount/set_park_here")
  131.  
  132.     def mount_tracking_on(self):
  133.         return self.request_with_status("/mount/tracking_on")
  134.  
  135.     def mount_tracking_off(self):
  136.         return self.request_with_status("/mount/tracking_off")
  137.  
  138.     def mount_follow_tle(self, tle_line_1, tle_line_2, tle_line_3):
  139.         return self.request_with_status("/mount/follow_tle", line1=tle_line_1, line2=tle_line_2, line3=tle_line_3)
  140.  
  141.     def mount_radecpath_new(self):
  142.         return self.request_with_status("/mount/radecpath/new")
  143.  
  144.     def mount_radecpath_add_point(self, jd, ra_j2000_hours, dec_j2000_degs):
  145.         return self.request_with_status("/mount/radecpath/add_point", jd=jd, ra_j2000_hours=ra_j2000_hours, dec_j2000_degs=dec_j2000_degs)
  146.  
  147.     def mount_radecpath_apply(self):
  148.         return self.request_with_status("/mount/radecpath/apply")
  149.  
  150.     def mount_custom_path_new(self, coord_type):
  151.         return self.request_with_status("/mount/custom_path/new", type=coord_type)
  152.  
  153.     def mount_custom_path_add_point_list(self, points):
  154.         lines = []
  155.         for (jd, ra, dec) in points:
  156.             line = "%.10f,%s,%s" % (jd, ra, dec)
  157.             lines.append(line)
  158.  
  159.         data = "\n".join(lines).encode('utf-8')
  160.  
  161.         postdata = urlencode({'data': data}).encode()
  162.  
  163.         return self.request("/mount/custom_path/add_point_list", postdata=postdata)
  164.  
  165.     def mount_custom_path_apply(self):
  166.         return self.request_with_status("/mount/custom_path/apply")
  167.  
  168.     def mount_model_add_point(self, ra_j2000_hours, dec_j2000_degs):
  169.         """
  170.        Add a calibration point to the pointing model, mapping the current pointing direction
  171.        of the telescope to the secified J2000 Right Ascension and Declination values.
  172.  
  173.        This call might be performed after manually centering a bright star with a known
  174.        RA and Dec, or the RA and Dec might be provided by a PlateSolve solution
  175.        from an image taken at the current location.
  176.        """
  177.  
  178.         return self.request_with_status("/mount/model/add_point", ra_j2000_hours=ra_j2000_hours, dec_j2000_degs=dec_j2000_degs)
  179.  
  180.     def mount_model_delete_point(self, *point_indexes_0_based):
  181.         """
  182.        Remove one or more calibration points from the pointing model.
  183.  
  184.        Points are specified by index, ranging from 0 to (number_of_points-1).
  185.  
  186.        Added in PWI 4.0.11 beta 9
  187.  
  188.        Examples:  
  189.          mount_model_delete_point(0)  # Delete the first point
  190.          mount_model_delete_point(1, 3, 5)  # Delete the second, fourth, and sixth points
  191.          mount_model_delete_point(*range(20)) # Delete the first 20 points
  192.        """
  193.  
  194.         point_indexes_comma_separated = list_to_comma_separated_string(point_indexes_0_based)
  195.         return self.request_with_status("/mount/model/delete_point", index=point_indexes_comma_separated)
  196.  
  197.     def mount_model_enable_point(self, *point_indexes_0_based):
  198.         """
  199.        Flag one or more calibration points as "enabled", meaning that these points
  200.        will contribute to the fit of the model.
  201.  
  202.        Points are specified by index, ranging from 0 to (number_of_points-1).
  203.        
  204.        Added in PWI 4.0.11 beta 9
  205.  
  206.        Examples:  
  207.          mount_model_enable_point(0)  # Enable the first point
  208.          mount_model_enable_point(1, 3, 5)  # Enable the second, fourth, and sixth points
  209.          mount_model_enable_point(*range(20)) # Enable the first 20 points
  210.        """
  211.  
  212.         point_indexes_comma_separated = list_to_comma_separated_string(point_indexes_0_based)
  213.         return self.request_with_status("/mount/model/enable_point", index=point_indexes_comma_separated)
  214.  
  215.     def mount_model_disable_point(self, *point_indexes_0_based):
  216.         """
  217.        Flag one or more calibration points as "disabled", meaning that these calibration
  218.        points will still be stored but will not contribute to the fit of the model.
  219.        
  220.        If a point is suspected to be an outlier, it can be disabled. This will cause the model
  221.        to re-fit, and the point's deviation from the newly-fit model can be re-examined before
  222.        being deleted entirely.
  223.  
  224.        Points are specified by index, ranging from 0 to (number_of_points-1).
  225.        
  226.        Added in PWI 4.0.11 beta 9
  227.  
  228.        Examples:  
  229.          mount_model_disable_point(0)  # Disable the first point
  230.          mount_model_disable_point(1, 3, 5)  # Disable the second, fourth, and sixth points
  231.          mount_model_disable_point(*range(20)) # Disable the first 20 points
  232.          mount_model_disable_point(            # Disable all points
  233.              *range(
  234.                  pwi4.status().mount.model.num_points_total
  235.               ))
  236.        """
  237.  
  238.         point_indexes_comma_separated = list_to_comma_separated_string(point_indexes_0_based)
  239.         return self.request_with_status("/mount/model/disable_point", index=point_indexes_comma_separated)
  240.  
  241.     def mount_model_clear_points(self):
  242.         """
  243.        Remove all calibration points from the pointing model.
  244.        """
  245.  
  246.         return self.request_with_status("/mount/model/clear_points")
  247.  
  248.     def mount_model_save_as_default(self):
  249.         """
  250.        Save the active pointing model as the model that will be loaded
  251.        by default the next time the mount is connected.
  252.        """
  253.  
  254.         return self.request_with_status("/mount/model/save_as_default")
  255.  
  256.     def mount_model_save(self, filename):
  257.         """
  258.        Save the active pointing model to a file so that it can later be re-loaded
  259.        by a call to mount_model_load().
  260.  
  261.        This may be useful when switching between models built for different instruments.
  262.        For example, a system might have one model for the main telescope, and another
  263.        model for a co-mounted telescope.
  264.        """
  265.  
  266.         return self.request_with_status("/mount/model/save", filename=filename)
  267.  
  268.     def mount_model_load(self, filename):
  269.         """
  270.        Load a model from the specified file and make it the active model.
  271.  
  272.        This may be useful when switching between models built for different instruments.
  273.        For example, a system might have one model for the main telescope, and another
  274.        model for a co-mounted telescope.
  275.        """
  276.  
  277.         return self.request_with_status("/mount/model/load", filename=filename)
  278.  
  279.     def focuser_connect(self):
  280.         # Added in PWI 4.0.99 Beta 2
  281.         return self.request_with_status("/focuser/connect")
  282.  
  283.     def focuser_disconnect(self):
  284.         # Added in PWI 4.0.99 Beta 2
  285.         return self.request_with_status("/focuser/disconnect")
  286.  
  287.     def focuser_enable(self):
  288.         return self.request_with_status("/focuser/enable")
  289.  
  290.     def focuser_disable(self):
  291.         return self.request_with_status("/focuser/disable")
  292.  
  293.     def focuser_goto(self, target):
  294.         return self.request_with_status("/focuser/goto", target=target)
  295.  
  296.     def focuser_stop(self):
  297.         return self.request_with_status("/focuser/stop")
  298.  
  299.     def rotator_connect(self):
  300.         # Added in PWI 4.0.99 Beta 2
  301.         return self.request_with_status("/rotator/connect")
  302.  
  303.     def rotator_disconnect(self):
  304.         # Added in PWI 4.0.99 Beta 2
  305.         return self.request_with_status("/rotator/disconnect")
  306.  
  307.  
  308.     def rotator_enable(self):
  309.         return self.request_with_status("/rotator/enable")
  310.  
  311.     def rotator_disable(self):
  312.         return self.request_with_status("/rotator/disable")
  313.        
  314.     def rotator_goto_mech(self, target_degs):
  315.         return self.request_with_status("/rotator/goto_mech", degs=target_degs)
  316.  
  317.     def rotator_goto_field(self, target_degs):
  318.         return self.request_with_status("/rotator/goto_field", degs=target_degs)
  319.  
  320.     def rotator_offset(self, offset_degs):
  321.         return self.request_with_status("/rotator/offset", degs=offset_degs)
  322.  
  323.     def rotator_stop(self):
  324.         return self.request_with_status("/rotator/stop")
  325.  
  326.     def m3_goto(self, target_port):
  327.         return self.request_with_status("/m3/goto", port=target_port)
  328.  
  329.     def m3_stop(self):
  330.         return self.request_with_status("/m3/stop")
  331.  
  332.     def virtualcamera_take_image(self):
  333.         """
  334.        Returns a string containing a FITS image simulating a starfield
  335.        at the current telescope position
  336.        """
  337.         return self.request("/virtualcamera/take_image")
  338.    
  339.     def virtualcamera_take_image_and_save(self, filename):
  340.         """
  341.        Request a fake FITS image from PWI4.
  342.        Save the contents to the specified filename
  343.        """
  344.  
  345.         contents = self.virtualcamera_take_image()
  346.         f = open(filename, "wb")
  347.         f.write(contents)
  348.         f.close()
  349.  
  350.     ### Methods for testing error handling ######################
  351.  
  352.     def test_command_not_found(self):
  353.         """
  354.        Try making a request to a URL that does not exist.
  355.        Useful for intentionally testing how the library will respond.
  356.        """
  357.         return self.request_with_status("/command/notfound")
  358.  
  359.     def test_internal_server_error(self):
  360.         """
  361.        Try making a request to a URL that will return a 500
  362.        server error due to an intentionally unhandled error.
  363.        Useful for testing how the library will respond.
  364.        """
  365.         return self.request_with_status("/internal/crash")
  366.    
  367.     def test_invalid_parameters(self):
  368.         """
  369.        Try making a request with intentionally missing parameters.
  370.        Useful for testing how the library will respond.
  371.        """
  372.         return self.request_with_status("/mount/goto_ra_dec_apparent")
  373.  
  374.     ### Low-level methods for issuing requests ##################
  375.  
  376.     def request(self, command, **kwargs):
  377.         return self.comm.request(command, **kwargs)
  378.  
  379.     def request_with_status(self, command, **kwargs):
  380.         response_text = self.request(command, **kwargs)
  381.         return self.parse_status(response_text)
  382.    
  383.     ### Status parsing utilities ################################
  384.  
  385.     def status_text_to_dict(self, response):
  386.         """
  387.        Given text with keyword=value pairs separated by newlines,
  388.        return a dictionary with the equivalent contents.
  389.        """
  390.  
  391.         # In Python 3, response is of type "bytes".
  392.         # Convert it to a string for processing below
  393.         if type(response) == bytes:
  394.             response = response.decode('utf-8')
  395.  
  396.         response_dict = {}
  397.  
  398.         lines = response.split("\n")
  399.        
  400.         for line in lines:
  401.             fields = line.split("=", 1)
  402.             if len(fields) == 2:
  403.                 name = fields[0]
  404.                 value = fields[1]
  405.                 response_dict[name] = value
  406.        
  407.         return response_dict
  408.  
  409.     def parse_status(self, response_text):
  410.         response_dict = self.status_text_to_dict(response_text)
  411.         return PWI4Status(response_dict)
  412.    
  413.  
  414.    
  415. class Section(object):
  416.     """
  417.    Simple object for collecting properties in PWI4Status
  418.    """
  419.  
  420.     pass
  421.  
  422. class PWI4Status:
  423.     """
  424.    Wraps the status response for many PWI4 commands in a class with named members
  425.    """
  426.  
  427.     def __init__(self, status_dict):
  428.         self.raw = status_dict  # Allow direct access to raw entries as needed
  429.  
  430.         self.pwi4 = Section()
  431.         self.pwi4.version = "<unknown>"
  432.         self.pwi4.version_field = [0, 0, 0, 0]
  433.  
  434.         self.pwi4.version = self.raw["pwi4.version"] # Added in 4.0.5 beta 1
  435.  
  436.         # pwi4.version_field[] was added in 4.0.9 beta 2
  437.         self.pwi4.version_field[0] = self.get_int("pwi4.version_field[0]", 0)
  438.         self.pwi4.version_field[1] = self.get_int("pwi4.version_field[1]", 0)
  439.         self.pwi4.version_field[2] = self.get_int("pwi4.version_field[2]", 0)
  440.         self.pwi4.version_field[3] = self.get_int("pwi4.version_field[3]", 0)
  441.  
  442.         # response.timestamp_utc was added in 4.0.9 beta 2
  443.         self.response = Section()
  444.         self.response.timestamp_utc = self.get_string("response.timestamp_utc")
  445.  
  446.  
  447.         self.site = Section()
  448.         self.site.latitude_degs = self.get_float("site.latitude_degs")
  449.         self.site.longitude_degs = self.get_float("site.longitude_degs")
  450.         self.site.height_meters = self.get_float("site.height_meters")
  451.         self.site.lmst_hours = self.get_float("site.lmst_hours")
  452.  
  453.         self.mount = Section()
  454.         self.mount.is_connected = self.get_bool("mount.is_connected")
  455.         self.mount.geometry = self.get_int("mount.geometry")
  456.         self.mount.timestamp_utc = self.get_string("mount.timestamp_utc") # Added in 4.0.9 beta 7
  457.         self.mount.julian_date = self.get_float("mount.julian_date")  # Added in 4.0.9 beta 2
  458.         self.mount.slew_time_constant = self.get_float("mount.slew_time_constant")  # Added in 4.0.9 beta 6
  459.         self.mount.ra_apparent_hours = self.get_float("mount.ra_apparent_hours")
  460.         self.mount.dec_apparent_degs = self.get_float("mount.dec_apparent_degs")
  461.         self.mount.ra_j2000_hours = self.get_float("mount.ra_j2000_hours")
  462.         self.mount.dec_j2000_degs = self.get_float("mount.dec_j2000_degs")
  463.         self.mount.target_ra_apparent_hours = self.get_float("mount.target_ra_apparent_hours") # Added in 4.0.5 beta 1
  464.         self.mount.target_dec_apparent_degs = self.get_float("mount.target_dec_apparent_degs") # Added in 4.0.5 beta 1
  465.         self.mount.azimuth_degs = self.get_float("mount.azimuth_degs")
  466.         self.mount.altitude_degs = self.get_float("mount.altitude_degs")
  467.         self.mount.is_slewing = self.get_bool("mount.is_slewing")
  468.         self.mount.is_tracking = self.get_bool("mount.is_tracking")
  469.         self.mount.field_angle_here_degs = self.get_float("mount.field_angle_here_degs")
  470.         self.mount.field_angle_at_target_degs = self.get_float("mount.field_angle_at_target_degs")
  471.         self.mount.field_angle_rate_at_target_degs_per_sec = self.get_float("mount.field_angle_rate_at_target_degs_per_sec")
  472.         self.mount.path_angle_at_target_degs = self.get_float("mount.path_angle_at_target_degs")
  473.         self.mount.path_angle_rate_at_target_degs_per_sec = self.get_float("mount.path_angle_rate_at_target_degs_per_sec")
  474.         self.mount.distance_to_sun_degs = self.get_float("mount.distance_to_sun_degs")      # Added in 4.0.13
  475.         self.mount.axis0_wrap_range_min_degs = self.get_float("mount.axis0_wrap_range_min_degs") # Added in 4.0.13
  476.  
  477.  
  478.         self.mount.axis0 = Section()
  479.         self.mount.axis1 = Section()
  480.         self.mount.axis = [self.mount.axis0, self.mount.axis1]
  481.  
  482.         for axis_index in range(2):
  483.             axis = self.mount.axis[axis_index]
  484.             prefix = "mount.axis%d." % axis_index
  485.  
  486.             axis.is_enabled = self.get_bool(prefix + "is_enabled")
  487.             axis.rms_error_arcsec = self.get_float(prefix + "rms_error_arcsec")
  488.             axis.dist_to_target_arcsec = self.get_float(prefix + "dist_to_target_arcsec")
  489.             axis.servo_error_arcsec = self.get_float(prefix + "servo_error_arcsec")
  490.             axis.min_mech_position_degs = self.get_float(prefix + "min_mech_position_degs") # Added in 4.0.13
  491.             axis.max_mech_position_degs = self.get_float(prefix + "max_mech_position_degs") # Added in 4.0.13
  492.             axis.target_mech_position_degs = self.get_float(prefix + "target_mech_position_degs") # Added in 4.0.13
  493.             axis.position_degs = self.get_float(prefix + "position_degs")
  494.             axis.position_timestamp_str = self.get_string(prefix + "position_timestamp") # Added in 4.0.9 beta 2
  495.             axis.max_velocity_degs_per_sec = self.get_float(prefix + "max_velocity_degs_per_sec") # Added in 4.0.13
  496.             axis.setpoint_velocity_degs_per_sec = self.get_float(prefix + "setpoint_velocity_degs_per_sec") # Added in 4.0.13
  497.             axis.measured_velocity_degs_per_sec = self.get_float(prefix + "measured_velocity_degs_per_sec") # Added in 4.0.13
  498.             axis.acceleration_degs_per_sec_sqr = self.get_float(prefix + "acceleration_degs_per_sec_sqr") # Added in 4.0.13
  499.             axis.measured_current_amps = self.get_float(prefix + "measured_current_amps") # Added in 4.0.13
  500.        
  501.         self.mount.model = Section()
  502.         self.mount.model.filename = self.get_string("mount.model.filename")
  503.         self.mount.model.num_points_total = self.get_int("mount.model.num_points_total")
  504.         self.mount.model.num_points_enabled = self.get_int("mount.model.num_points_enabled")
  505.         self.mount.model.rms_error_arcsec = self.get_float("mount.model.rms_error_arcsec")
  506.  
  507.         # mount.offests.* was added in PWI 4.0.11 Beta 5
  508.         if "mount.offsets.ra_arcsec.total" not in self.raw:
  509.             self.mount.offsets = None  # Offset reporting not supported by running version of PWI4
  510.         else:
  511.             self.mount.offsets = Section()
  512.  
  513.             self.mount.offsets.ra_arcsec = Section()
  514.             self.mount.offsets.ra_arcsec.total=self.get_float("mount.offsets.ra_arcsec.total")
  515.             self.mount.offsets.ra_arcsec.rate=self.get_float("mount.offsets.ra_arcsec.rate")
  516.             self.mount.offsets.ra_arcsec.gradual_offset_progress=self.get_float("mount.offsets.ra_arcsec.gradual_offset_progress")
  517.  
  518.             self.mount.offsets.dec_arcsec = Section()
  519.             self.mount.offsets.dec_arcsec.total=self.get_float("mount.offsets.dec_arcsec.total")
  520.             self.mount.offsets.dec_arcsec.rate=self.get_float("mount.offsets.dec_arcsec.rate")
  521.             self.mount.offsets.dec_arcsec.gradual_offset_progress=self.get_float("mount.offsets.dec_arcsec.gradual_offset_progress")
  522.  
  523.             self.mount.offsets.axis0_arcsec = Section()
  524.             self.mount.offsets.axis0_arcsec.total=self.get_float("mount.offsets.axis0_arcsec.total")
  525.             self.mount.offsets.axis0_arcsec.rate=self.get_float("mount.offsets.axis0_arcsec.rate")
  526.             self.mount.offsets.axis0_arcsec.gradual_offset_progress=self.get_float("mount.offsets.axis0_arcsec.gradual_offset_progress")
  527.  
  528.             self.mount.offsets.axis1_arcsec = Section()
  529.             self.mount.offsets.axis1_arcsec.total=self.get_float("mount.offsets.axis1_arcsec.total")
  530.             self.mount.offsets.axis1_arcsec.rate=self.get_float("mount.offsets.axis1_arcsec.rate")
  531.             self.mount.offsets.axis1_arcsec.gradual_offset_progress=self.get_float("mount.offsets.axis1_arcsec.gradual_offset_progress")
  532.  
  533.             self.mount.offsets.path_arcsec = Section()
  534.             self.mount.offsets.path_arcsec.total=self.get_float("mount.offsets.path_arcsec.total")
  535.             self.mount.offsets.path_arcsec.rate=self.get_float("mount.offsets.path_arcsec.rate")
  536.             self.mount.offsets.path_arcsec.gradual_offset_progress=self.get_float("mount.offsets.path_arcsec.gradual_offset_progress")
  537.            
  538.             self.mount.offsets.transverse_arcsec = Section()
  539.             self.mount.offsets.transverse_arcsec.total=self.get_float("mount.offsets.transverse_arcsec.total")
  540.             self.mount.offsets.transverse_arcsec.rate=self.get_float("mount.offsets.transverse_arcsec.rate")
  541.             self.mount.offsets.transverse_arcsec.gradual_offset_progress=self.get_float("mount.offsets.transverse_arcsec.gradual_offset_progress")
  542.  
  543.         self.focuser = Section()
  544.         self.focuser.exists = self.get_bool("focuser.exists", False) # Added in 4.0.99 Beta 2
  545.         self.focuser.is_connected = self.get_bool("focuser.is_connected")
  546.         self.focuser.is_enabled = self.get_bool("focuser.is_enabled")
  547.         self.focuser.position = self.get_float("focuser.position")
  548.         self.focuser.is_moving = self.get_bool("focuser.is_moving")
  549.        
  550.         self.rotator = Section()
  551.         self.rotator.exists = self.get_bool("rotator.exists", False) # Added in 4.0.99 Beta 2
  552.         self.rotator.is_connected = self.get_bool("rotator.is_connected")
  553.         self.rotator.is_enabled = self.get_bool("rotator.is_enabled")
  554.         self.rotator.mech_position_degs = self.get_float("rotator.mech_position_degs")
  555.         self.rotator.field_angle_degs = self.get_float("rotator.field_angle_degs")
  556.         self.rotator.is_moving = self.get_bool("rotator.is_moving")
  557.         self.rotator.is_slewing = self.get_bool("rotator.is_slewing")
  558.  
  559.         self.m3 = Section()
  560.         self.m3.exists = self.get_bool("m3.exists", False) # Added in 4.0.99 Beta 2
  561.         self.m3.port = self.get_int("m3.port")
  562.  
  563.         self.autofocus = Section()
  564.         self.autofocus.is_running = self.get_bool("autofocus.is_running")
  565.         self.autofocus.success = self.get_bool("autofocus.success")
  566.         self.autofocus.best_position = self.get_float("autofocus.best_position")
  567.         self.autofocus.tolerance = self.get_float("autofocus.tolerance")
  568.  
  569.  
  570.     def get_bool(self, name, value_if_missing=None):
  571.         if name not in self.raw:
  572.             return value_if_missing
  573.         return self.raw[name].lower() == "true"
  574.  
  575.     def get_float(self, name, value_if_missing=None):
  576.         if name not in self.raw:
  577.             return value_if_missing
  578.         return float(self.raw[name])
  579.  
  580.     def get_int(self, name, value_if_missing=None):
  581.         if name not in self.raw:
  582.             return value_if_missing
  583.         return int(self.raw[name])
  584.    
  585.     def get_string(self, name, value_if_missing=None):
  586.         if name not in self.raw:
  587.             return value_if_missing
  588.         return self.raw[name]
  589.  
  590.     def __repr__(self):
  591.         """
  592.        Format all of the keywords and values we have received
  593.        """
  594.  
  595.         max_key_length = max(len(x) for x in self.raw.keys())
  596.  
  597.         lines = []
  598.  
  599.         line_format = "%-" + str(max_key_length) + "s: %s"
  600.  
  601.         for key in sorted(self.raw.keys()):
  602.             value = self.raw[key]
  603.             lines.append(line_format % (key, value))
  604.         return "\n".join(lines)
  605.  
  606. class PWI4HttpCommunicator:
  607.     """
  608.    Manages communication with PWI4 via HTTP.
  609.    """
  610.  
  611.     def __init__(self, host="localhost", port=8220):
  612.         self.host = host
  613.         self.port = port
  614.  
  615.         self.timeout_seconds = 3
  616.  
  617.     def make_url(self, path, **kwargs):
  618.         """
  619.        Utility function that takes a set of keyword=value arguments
  620.        and converts them into a properly formatted URL to send to PWI.
  621.        Special characters (spaces, colons, plus symbols, etc.) are encoded as needed.
  622.  
  623.        Example:
  624.          make_url("/mount/gotoradec2000", ra=10.123, dec="15 30 45") -> "http://localhost:8220/mount/gotoradec2000?ra=10.123&dec=15%2030%2045"
  625.        """
  626.  
  627.         # Construct the basic URL, excluding the keyword parameters; for example: "http://localhost:8220/specified/path?"
  628.         url = "http://" + self.host + ":" + str(self.port) + path + "?"
  629.  
  630.         # For every keyword=value argument given to this function,
  631.         # construct a string of the form "key1=val1&key2=val2".
  632.         keyword_values = list(kwargs.items()) # Need to explicitly convert this to list() for Python 3.x
  633.         urlparams = urlencode(keyword_values)
  634.  
  635.         # In URLs, spaces can be encoded as "+" characters or as "%20".
  636.         # This will convert plus symbols to percent encoding for improved compatibility.
  637.         urlparams = urlparams.replace("+", "%20")
  638.  
  639.         # Build the final URL and return it.
  640.         url = url + urlparams
  641.         return url
  642.  
  643.     def request(self, path, postdata=None, **kwargs):
  644.         """
  645.        Issue a request to PWI using the keyword=value parameters
  646.        supplied to the function, and return the response received from
  647.        PWI.
  648.  
  649.        Example:
  650.          pwi_request("/mount/gotoradec2000", ra=10.123, dec="15 30 45")
  651.        
  652.        will construct the appropriate URL and issue the request to the server.
  653.  
  654.        If the postdata argument is specified, this will make a POST request
  655.        instead of a GET request, and postdata will be used as the body of the
  656.        POST request.
  657.  
  658.        The server response payload will be returned, or an exception will be thrown
  659.        if there was an error with the request.
  660.        """
  661.  
  662.         # Construct the URL that we will request
  663.         url = self.make_url(path, **kwargs)
  664.  
  665.         # Open a connection to the server, issue the request, and try to receive the response.
  666.         # The server will return an HTTP Status Code as part of the response.
  667.         # If the status code indicates an error, an HTTPError will be thrown.
  668.         try:
  669.             response = urlopen(url, data=postdata, timeout=self.timeout_seconds)
  670.         except HTTPError as e:
  671.             if e.code == 404:
  672.                 error_message = "Command not found"
  673.             elif e.code == 400:
  674.                 error_message = "Bad request"
  675.             elif e.code == 500:
  676.                 error_message = "Internal server error (possibly a bug in PWI)"
  677.             else:
  678.                 error_message = str(e)
  679.  
  680.             try:
  681.                 error_details = e.read()  # Try to read the payload of the response for error information
  682.                 error_message = error_message + ": " + error_details
  683.             except:
  684.                 pass # If that failed, we won't include any further details
  685.            
  686.             raise Exception(error_message) # TODO: Consider a custom exception here
  687.  
  688.            
  689.         except Exception as e:
  690.             # This will often be a urllib2.URLError to indicate that a connection
  691.             # could not be made to the server, but we'll handle any exception here
  692.             raise
  693.  
  694.         payload = response.read()
  695.         return payload
  696.  
  697.    
  698. def list_to_comma_separated_string(value_list):
  699.     """
  700.    Convert list of values (e.g. [3, 1, 5]) into a comma-separated string (e.g. "3,1,5")
  701.    """
  702.  
  703.     return ",".join([str(x) for x in value_list])

Raw Paste


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