def main(): """Repeatedly prints coordinates of camera frame center found by plate solving.""" parser = track.ArgParser() parser.add_argument('--skip-solve', help='skip plate solving', action='store_true') cameras.add_program_arguments(parser, profile='align') args = parser.parse_args() camera = cameras.make_camera_from_args(args, profile='align') cv2.namedWindow('camera', cv2.WINDOW_NORMAL) cv2.resizeWindow('camera', 640, 480) frame_count = 0 while True: print(f'Frame {frame_count:06d} at {datetime.now()}...', end='', flush=True) frame_count += 1 frame = camera.get_frame() cv2.imshow('camera', frame) cv2.waitKey(10) if args.skip_solve: continue try: start_time = time.time() _, sc = track.plate_solve(frame, camera_width=camera.field_of_view[1]) elapsed = time.time() - start_time except track.NoSolutionException: print('No solution found') continue print( f'Found solution at ({sc.to_string("decimal")}) in {elapsed} seconds' )
def main(): """See module docstring""" parser = track.ArgParser() parser.add_argument('--lat', required=True, help='latitude of observer (+N)') parser.add_argument('--lon', required=True, help='longitude of observer (+E)') parser.add_argument('--elevation', required=False, default=0.0, help='elevation of observer (m)', type=float) args = parser.parse_args() # Grab the latest space station TLE file from Celestrak stations = requests.get( 'http://celestrak.com/NORAD/elements/stations.txt').text.splitlines() # Top entry in the station.txt file should be for ISS iss = ephem.readtle(str(stations[0]), str(stations[1]), str(stations[2])) # Create PyEphem Observer object with location information home = ephem.Observer() home.lon = args.lon home.lat = args.lat home.elevation = args.elevation # Print current Az-Alt of ISS once per second while True: home.date = ephem.now() iss.compute(home) print('Az: %s Alt: %s' % (iss.az, iss.alt)) time.sleep(1)
def main(): """See module docstring""" def gamepad_callback(_tracker: Tracker) -> bool: """Callback for gamepad control. Allows manual control of the slew rate via a gamepad when the 'B' button is held down, overriding tracking behavior. This callback is registered with the Tracker object which calls it on each control cycle. Defined inside main() so this has easy access to objects that are within that scope. Args: _tracker: A reference to an object of type Tracker. Not used internally. Returns: True when the 'B' button is depressed which disables the normal control path. False otherwise, which leaves the normal control path enabled. """ if game_pad.state.get('BTN_EAST', 0) == 1: gamepad_x, gamepad_y = game_pad.get_proportional() slew_rates = (mount.max_slew_rate * gamepad_x, mount.max_slew_rate * gamepad_y) for idx, axis_name in enumerate(mount.AxisName): mount.slew(axis_name, slew_rates[idx]) return True else: return False parser = track.ArgParser() targets.add_program_arguments(parser) laser.add_program_arguments(parser) mounts.add_program_arguments(parser, meridian_side_required=True) ntp.add_program_arguments(parser) telem.add_program_arguments(parser) control.add_program_arguments(parser) args = parser.parse_args() # Set priority of this thread to realtime. Do this before constructing objects since priority # is inherited and some critical threads are created by libraries we have no direct control # over. os.sched_setscheduler(0, os.SCHED_RR, os.sched_param(11)) # Check if system clock is synchronized to GPS if args.check_time_sync: try: ntp.check_ntp_status() except ntp.NTPCheckFailure as e: print('NTP check failed: ' + str(e)) if not click.confirm('Continue anyway?', default=True): print('Aborting') sys.exit(2) # Load a MountModel object try: mount_model = track.model.load_stored_model() except track.model.StaleParametersException: if click.confirm('Stored alignment parameters are stale. Use anyway?', default=True): mount_model = track.model.load_stored_model(max_age=None) elif args.target_type == 'camera': if click.confirm( 'Use a default set of alignment parameters instead?', default=True): guide_cam_orientation = click.prompt( 'Enter guide camera orientation in degrees, clockwise positive', type=float) mount_model = track.model.load_default_model( guide_cam_orientation=Longitude(guide_cam_orientation * u.deg)) if 'mount_model' not in locals(): print( 'Aborting: No model could be loaded. To refresh stored model run align program.' ) sys.exit(1) # Create object with base type TelescopeMount mount = mounts.make_mount_from_args(args) telem_logger = telem.make_telem_logger_from_args(args) target = targets.make_target_from_args( args, mount, mount_model, MeridianSide[args.meridian_side.upper()], telem_logger=telem_logger, ) tracker = Tracker( mount=mount, mount_model=mount_model, target=target, telem_logger=telem_logger, ) try: laser_pointer = laser.make_laser_from_args(args) except OSError: print('Could not connect to laser pointer FTDI device.') laser_pointer = None telem_sources = {} try: # Create gamepad object and register callback game_pad = Gamepad() if laser_pointer is not None: game_pad.register_callback('BTN_SOUTH', laser_pointer.set) tracker.register_callback(gamepad_callback) telem_sources['gamepad'] = game_pad print('Gamepad found and registered.') except RuntimeError: print('No gamepads found.') if telem_logger is not None: telem_logger.register_sources(telem_sources) telem_logger.start() stopping_conditions = control.make_stop_conditions_from_args(args) try: tracker.run(stopping_conditions) except KeyboardInterrupt: print('Got CTRL-C, shutting down...') finally: # don't rely on destructors to safe mount! print('Safing mount...', end='', flush=True) if mount.safe(): print('Mount safed successfully!') else: print('Warning: Mount may be in an unsafe state!') try: game_pad.stop() except UnboundLocalError: pass if telem_logger is not None: telem_logger.stop()
def main(): """See module docstring at the top of this file.""" parser = track.ArgParser() parser.add_argument('--timestamp', required=False, help='UNIX timestamp', type=float) subparsers = parser.add_subparsers(title='modes', dest='mode') parser_star = subparsers.add_parser('star', help='named star mode') parser_star.add_argument('name', help='name of star') parser_star = subparsers.add_parser('solarsystem', help='named solar system body mode') parser_star.add_argument('name', help='name of planet or moon') gps_client.add_program_arguments(parser) args = parser.parse_args() # Create a PyEphem Observer object location = gps_client.make_location_from_args(args) observer = ephem.Observer() observer.lat = location.lat.deg observer.lon = location.lon.deg observer.elevation = location.height.value # Get the PyEphem Body object corresonding to the given named star if args.mode == 'star': print('In named star mode: looking up \'{}\''.format(args.name)) target = None for name, _ in ephem.stars.stars.items(): if args.name.lower() == name.lower(): print('Found named star: \'{}\''.format(name)) target = ephem.star(name) break if target is None: raise Exception( 'The named star \'{}\' isn\' present in PyEphem.'.format( args.name)) # Get the PyEphem Body object corresonding to the given named solar system body elif args.mode == 'solarsystem': print('In named solar system body mode: looking up \'{}\''.format( args.name)) # pylint: disable=protected-access ss_objs = [ name.lower() for _, _, name in ephem._libastro.builtin_planets() ] if args.name.lower() in ss_objs: body_type = None for attr in dir(ephem): if args.name.lower() == attr.lower(): body_type = getattr(ephem, attr) print('Found solar system body: \'{}\''.format(attr)) break assert body_type is not None target = body_type() else: raise Exception( 'The solar system body \'{}\' isn\'t present in PyEphem.'. format(args.name)) else: print('You must specify a target.') sys.exit(1) if args.timestamp is not None: observer.date = ephem.Date( datetime.datetime.utcfromtimestamp(args.timestamp)) else: observer.date = ephem.Date(datetime.datetime.utcnow()) target.compute(observer) position = { 'az': target.az * 180.0 / math.pi, 'alt': target.alt * 180.0 / math.pi, } print('Expected position: ' + str(position))
def main(): """See module docstring at the top of this file.""" parser = track.ArgParser() parser.add_argument('outdir', help='output directory') parser.add_argument('--mag-limit', required=True, help='magnitude cutoff for object passes', type=float) time_group = parser.add_argument_group( title='Time Options', description='Options pertaining to time of observation', ) time_group.add_argument( '--tz', required=True, help='time zone short code (use \'help\' for a list of codes)') time_group.add_argument('--year', required=False, help='observation year (default: now)', type=int) time_group.add_argument('--month', required=False, help='observation month (default: now)', type=int) time_group.add_argument('--day', required=False, help='observation day-of-month (default: now)', type=int) time_group.add_argument('--ampm', required=False, help='morning or evening (\'AM\' or \'PM\')', default='PM') gps_client.add_program_arguments(parser) args = parser.parse_args() if args.tz == 'help': print_timezone_help() return if args.ampm.upper() != 'AM' and args.ampm.upper() != 'PM': raise Exception('The AM/PM argument can only be \'AM\' or \'PM\'.') if args.year is not None and args.month is not None and args.day is not None: when = datetime.datetime(args.year, args.month, args.day).date() elif args.year is None and args.month is None and args.day is None: when = datetime.datetime.now().date() else: raise Exception( 'If an explicit observation date is given, then year, month, and day must ' 'all be specified.') # Get location of observer from arguments or from GPS location = gps_client.make_location_from_args(args) base_url = 'http://www.heavens-above.com/' bright_sats_url = ( base_url + f'AllSats.aspx?lat={location.lat.deg}&lng={location.lon.deg}' f'&alt={location.height.value}&tz={args.tz}') # do an initial page request so we can get the __VIEWSTATE and __VIEWSTATEGENERATOR values pre_soup = BeautifulSoup(requests.get(bright_sats_url).text, 'lxml') view_state = pre_soup.find('input', { 'type': 'hidden', 'name': '__VIEWSTATE' })['value'] view_state_generator = pre_soup.find('input', { 'type': 'hidden', 'name': '__VIEWSTATEGENERATOR' })['value'] post_data = [ ('__EVENTTARGET', ''), ('__EVENTARGUMENT', ''), ('__LASTFOCUS', ''), ('__VIEWSTATE', view_state), ('__VIEWSTATEGENERATOR', view_state_generator), # ('utcOffset', '0'), # uhhhhhh... ('ctl00$ddlCulture', 'en'), ('ctl00$cph1$TimeSelectionControl1$comboMonth', str(date_to_monthnum(when))), ('ctl00$cph1$TimeSelectionControl1$comboDay', str(when.day)), ('ctl00$cph1$TimeSelectionControl1$radioAMPM', args.ampm.upper()), ('ctl00$cph1$TimeSelectionControl1$btnSubmit', 'Update'), ('ctl00$cph1$radioButtonsMag', '5.0'), ] bright_sats_soup = BeautifulSoup( requests.post(bright_sats_url, data=post_data).text, 'lxml') # find the rows in the table listing the satellite passes table = bright_sats_soup.find('table', {'class': 'standardTable'}) rows = table.tbody.find_all('tr') # this number is deceptive because it's pre-magnitude-filtering print('Found {} (pre-filtered) satellite passes.'.format(len(rows))) for row in rows: cols = row.find_all('td') # We're not using all of these now, but preserving them is useful as documentation # pylint: disable=unused-variable sat = cols[0].string mag = float(cols[1].string) start_time = cols[2].string # <-- in local time start_alt = cols[3].string start_az = cols[4].string high_time = cols[5].string # <-- in local time high_alt = cols[6].string high_az = cols[7].string end_time = cols[8].string # <-- in local time end_alt = cols[9].string end_az = cols[10].string # manually enforce the magnitude threshold if mag > args.mag_limit: continue # extract the satellite id from the onclick attribute of this table row onclick_str = row['onclick'] url_suffix = re.findall(r"'([^']*)'", onclick_str)[0] satid = re.findall(r"satid=([0-9]*)", url_suffix)[0] print('Getting TLE for ' + sat + '...') # get the TLE from the orbit details page for this satellite orbit_url = base_url + 'orbit.aspx?satid=' + satid orbit_page = requests.get(orbit_url).text orbit_soup = BeautifulSoup(orbit_page, 'lxml') span_tags = orbit_soup.pre.find_all('span') assert len(span_tags) == 2 tle = [sat] for span_tag in span_tags: assert span_tag['id'].startswith('ctl00_cph1_lblLine') tle.append(span_tag.string) os.makedirs(args.outdir, exist_ok=True) filename = os.path.join(os.path.normpath(args.outdir), urlify(sat) + '.tle') with open(filename, 'w') as f: for line in tle: f.write(line + '\n')
def main(): """Run the alignment procedure! See module docstring for a description.""" parser = track.ArgParser() align_group = parser.add_argument_group( title='Alignment Configuration', description='Options that apply to alignment', ) align_group.add_argument('--mount-pole-alt', required=True, help='altitude of mount pole above horizon (deg)', type=float) align_group.add_argument( '--guide-cam-orientation', help='orientation of guidescope camera, clockwise positive (deg)', default=0.0, type=float, ) align_group.add_argument( '--min-positions', help='minimum number of positions to add to mount alignment model', default=10, type=int) align_group.add_argument( '--timeout', help='max time to wait for mount to converge on a new position', default=120.0, type=float) align_group.add_argument( '--max-tries', help='max number of plate solving attempts at each position', default=3, type=int) align_group.add_argument( '--min-alt', help='minimum altitude of alignment positions in degrees', default=20.0, type=float) align_group.add_argument( '--max-rms-error', help='warning printed if RMS error in degrees is greater than this', default=0.2, type=float) gps_client.add_program_arguments(parser) mounts.add_program_arguments(parser) cameras.add_program_arguments(parser, profile='align') ntp.add_program_arguments(parser) telem.add_program_arguments(parser) args = parser.parse_args() # Check if system clock is synchronized to GPS if args.check_time_sync: try: ntp.check_ntp_status() except ntp.NTPCheckFailure as e: print('NTP check failed: ' + str(e)) if not click.confirm('Continue anyway?', default=True): print('Aborting') sys.exit(2) mount = mounts.make_mount_from_args(args) # Get location of observer from arguments or from GPS location = gps_client.make_location_from_args(args) # This is meant to be just good enough that the model is able to tell roughly which direction # is up so that the positions used during alignment are all above the horizon. starter_mount_model = track.model.load_default_model( mount_pole_alt=Longitude(args.mount_pole_alt * u.deg), location=location) camera = cameras.make_camera_from_args(args, profile='align') telem_logger = telem.make_telem_logger_from_args(args) if args.meridian_side is not None: meridian_side = MeridianSide[args.meridian_side.upper()] else: meridian_side = None positions = generate_positions(min_positions=args.min_positions, mount_model=starter_mount_model, mount=mount, min_altitude=Angle(args.min_alt * u.deg), meridian_side=meridian_side) # add the park position to the start of the list to serve as the "depot" in the route park_encoder_positions = mount.get_position() positions.insert( 0, Position( encoder_positions=park_encoder_positions, mount=mount, )) # use travelling salesman solver to sort positions in the order that will take the least time positions = solve_route(positions) # mount is already at the starting position so don't need to go there del positions[0] # directory in which to place observation data for debugging purposes observations_dir = os.path.join( DATA_PATH, 'alignment_' + datetime.utcnow().isoformat(timespec='seconds').replace(':', '')) os.makedirs(DATA_PATH, exist_ok=True) os.mkdir(observations_dir) # pylint: disable=broad-except try: observations = [] num_solutions = 0 for idx, position in enumerate(positions): target = FixedMountEncodersTarget( position.encoder_positions, starter_mount_model, ) pos = target.get_position() print(f'Moving to position {idx + 1} of {len(positions)}: ' f'Az: {pos.topo.az.deg:.2f}, Alt: {pos.topo.alt.deg:.2f}') tracker = Tracker( mount=mount, mount_model=starter_mount_model, target=target, telem_logger=telem_logger, ) if telem_logger is not None: telem_logger.register_sources({'tracker': tracker}) stop_reason = tracker.run( tracker.StoppingConditions(timeout=args.timeout, error_threshold=Angle(2.0 * u.deg))) mount.safe() if tracker.StopReason.CONVERGED not in stop_reason: raise RuntimeError( f'Unexpected tracker stop reason: {stop_reason}') time.sleep(1.0) print( 'Converged on the target position. Attempting plate solving.') # plate solver doesn't always work on the first try for i in range(args.max_tries): print(f'\tPlate solver attempt {i + 1} of {args.max_tries}...', end='', flush=True) if attempt_plate_solving(mount=mount, camera=camera, location=location, observations=observations, observations_dir=observations_dir, num_solutions_so_far=num_solutions): num_solutions += 1 break print('Plate solver found solutions at {} of {} positions.'.format( num_solutions, len(positions))) if num_solutions == 0: print( "Can't solve mount model without any usable observations. Aborting." ) sys.exit(1) elif num_solutions < args.min_positions: if not click.confirm( f'WARNING: You asked for at least {args.min_positions} positions ' f'but only {num_solutions} can be used. Pointing accuracy may be ' f'affected. Continue to solve for model parameters anyway?', default=True): sys.exit(1) observations = pd.DataFrame(observations) observations_filename = os.path.join(observations_dir, 'observations_dataframe.pickle') print(f'Saving observations to {observations_filename}') with open(observations_filename, 'wb') as f: pickle.dump(observations, f, pickle.HIGHEST_PROTOCOL) try: print('Solving for mount model parameters...', end='', flush=True) model_params, result = track.model.solve_model(observations) print('done.') print(result) rms_error = np.sqrt(2 * result.cost / len(observations)) if rms_error > args.max_rms_error: if not click.confirm( f'WARNING: RMS error {rms_error:.4f} > ' f'{args.max_rms_error:.4f} degrees, save this solution ' f'anyway?', default=True): sys.exit(1) else: print(f'RMS error: {rms_error:.4f} degrees') model_param_set = ModelParamSet( model_params=model_params, guide_cam_orientation=Longitude(args.guide_cam_orientation * u.deg), location=location, timestamp=time.time(), ) params_filename = os.path.join(observations_dir, 'model_params.pickle') print(f'Saving model parameters to {params_filename}') with open(params_filename, 'wb') as f: pickle.dump(model_param_set, f, pickle.HIGHEST_PROTOCOL) print('Making this set of model parameters the default') track.model.save_default_param_set(model_param_set) except track.model.NoSolutionException as e: print('failed: {}'.format(str(e))) except RuntimeError as e: print(str(e)) print('Alignment was not completed.') except KeyboardInterrupt: print('Got CTRL-C, shutting down...') finally: # don't rely on destructors to safe mount! print('Safing mount...', end='', flush=True) if mount.safe(): print('Mount safed successfully!') else: print('Warning: Mount may be in an unsafe state!') # remove observations directory if it is empty try: os.rmdir(observations_dir) except (OSError, NameError): pass
def main(): """Apply step functions of varying magnitudes to a mount axis and plot the responses.""" parser = track.ArgParser() mounts.add_program_arguments(parser) parser.add_argument( '--axis', help='axis number (0 or 1)', default=0, type=int, ) args = parser.parse_args() mount = mounts.make_mount_from_args(args) step_responses = {} direction = +1.0 try: for step_magnitude in np.arange(0.5, 4.5, 0.5): positions = [] position_start = mount.get_position()[args.axis] time_start = time.time() direction = -direction rate = 0.0 while True: delta_position = abs( Longitude(mount.get_position()[args.axis] - position_start, wrap_angle=180 * u.deg)) time_elapsed = time.time() - time_start positions.append({ 'time': time_elapsed, 'position': delta_position.deg }) mount.slew(args.axis, rate) if time_elapsed >= 1.0: rate = step_magnitude * direction if time_elapsed >= 4.0: mount.safe() break time.sleep(0.01) step_responses[step_magnitude] = pd.DataFrame(positions) except KeyboardInterrupt: print('Got CTRL-C, shutting down...') finally: # don't rely on destructors to safe mount! print('Safing mount...') if mount.safe(): print('Mount safed successfully!') else: print('Warning: Mount may be in an unsafe state!') # plot step responses for step_magnitude, response in step_responses.items(): step_response = np.diff(response.position) / np.diff(response.time) plt.plot(response.time[:-1] - 1.0, step_response, label=f'{step_magnitude:.1f} deg/s') # plot acceleration limit line t = np.linspace(0, 0.5, 1e3) a = 10 * t # 10 degrees per second squared -- the default for G11 mount when this was written plt.plot(t, a, 'r', label='accel limit') plt.title(f'Step Response for Axis {args.axis}') plt.xlabel('Time [s]') plt.ylabel('Slew Rate [deg/s]') plt.legend() plt.grid(True) plt.show()
def main(): """See module docstring""" parser = track.ArgParser() parser.add_argument( '--mount-type', help='select mount type (nexstar or gemini)', default='gemini' ) parser.add_argument( '--mount-path', help='serial device node or hostname for mount command interface', default='/dev/ttyACM0' ) args = parser.parse_args() # Create object with base type TelescopeMount if args.mount_type == 'nexstar': mount = track.NexStarMount(args.mount_path) elif args.mount_type == 'gemini': mount = track.LosmandyGeminiMount(args.mount_path) else: print('mount-type not supported: ' + args.mount_type) sys.exit(1) axes = mount.AxisName # pylint: disable=too-many-nested-blocks try: # rates to test in arcseconds per second rates = [2**x for x in range(14)] rates.append(16319) rate_est = {} for axis in axes: rate_est[axis] = [] direction = +1 for axis in axes: print('Testing ' + str(axis) + '...') for rate in rates: print('Commanding slew at ' + str(rate) + ' arcseconds per second...') time_start = time.time() while time.time() - time_start < SLEW_CHANGE_TIME: mount.slew(axis, direction * rate / 3600.0) direction *= -1 position_start = mount.get_position() time_start = time.time() while True: position = mount.get_position() time_elapsed = time.time() - time_start position_change = abs( Longitude(position[axis] - position_start[axis], wrap_angle=180*u.deg) ).deg if position_change > SLEW_LIMIT or time_elapsed > TIME_LIMIT: break rate_est[axis].append(position_change / time_elapsed) print('\tmeasured rate: ' + str(rate_est[axis][-1] * 3600.0)) mount.safe() print('Results:') for rate, rate_est_0, rate_est_1 in zip(rates, rate_est[axes[0]], rate_est[axes[1]]): print(str(rate) + ', ' + str(3600 * rate_est_0) + ', ' + str(3600 * rate_est_1)) except KeyboardInterrupt: print('Got CTRL-C, shutting down...') finally: # don't rely on destructors to safe mount! print('Safing mount...') if mount.safe(): print('Mount safed successfully!') else: print('Warning: Mount may be in an unsafe state!')