#!/usr/bin/env python from __future__ import division import ephem from datetime import datetime, timedelta import os import lxml.html import argparse import logging from utils import read_priorities_transmitters, \ get_priority_passes from auto_scheduler import Twolineelement, Satellite from auto_scheduler.pass_predictor import find_passes from auto_scheduler.schedulers import ordered_scheduler, \ report_efficiency from cache import CacheManager from satnogs_client import get_active_transmitter_info, \ get_groundstation_info, \ get_satellite_info, \ get_scheduled_passes_from_network, \ get_transmitter_stats, \ schedule_observation import settings from tqdm import tqdm import sys _LOG_LEVEL_STRINGS = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'] def _log_level_string_to_int(log_level_string): if log_level_string not in _LOG_LEVEL_STRINGS: message = 'invalid choice: {0} (choose from {1})'.format(log_level_string, _LOG_LEVEL_STRINGS) raise argparse.ArgumentTypeError(message) log_level_int = getattr(logging, log_level_string, logging.INFO) # check the logging log_level_choices have not changed from our expected values assert isinstance(log_level_int, int) return log_level_int def main(): # Parse arguments parser = argparse.ArgumentParser( description="Automatically schedule observations on a SatNOGS station.") parser.add_argument("-s", "--station", help="Ground station ID", type=int) parser.add_argument("-t", "--starttime", help="Start time (YYYY-MM-DD HH:MM:SS) [default: now]", default=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")) parser.add_argument("-d", "--duration", help="Duration to schedule [hours; default: 1.0]", type=float, default=1.0) parser.add_argument("-m", "--min-culmination", help="Minimum culmination elevation [degrees; ground station default, minimum: 0, maximum: 90]", type=float, default=None) parser.add_argument("-r", "--min-riseset", help="Minimum rise/set elevation [degrees; ground station default, minimum: 0, maximum: 90]", type=float, default=None) parser.add_argument("-z", "--horizon", help="Force rise/set elevation to 0 degrees (overrided -r).", action="store_true") parser.add_argument("-f", "--only-priority", help="Schedule only priority satellites (from -P file)", dest='only_priority', action='store_false') parser.set_defaults(only_priority=True) parser.add_argument("-w", "--wait", help="Wait time between consecutive observations (for setup and slewing)" + " [seconds; default: 0, maximum: 3600]", type=int, default=0) parser.add_argument("-n", "--dryrun", help="Dry run (do not schedule passes)", action="store_true") parser.add_argument("-P", "--priorities", help="File with transmitter priorities. Should have " + "columns of the form |NORAD priority UUID| like |43017 0.9" + " KgazZMKEa74VnquqXLwAvD|. Priority is fractional, one transmitter " + "per line.", default=None) parser.add_argument("-M", "--min-priority", help="Minimum priority. Only schedule passes with a priority higher" + "than this limit [default: 0.0, maximum: 1.0]", type=float, default=0.) parser.add_argument("-T", "--allow-testing", help="Allow scheduling on stations which are in testing mode [default: False]", action="store_true") parser.set_defaults(allow_testing=False) parser.add_argument("-l", "--log-level", default="INFO", dest="log_level", type=_log_level_string_to_int, nargs="?", help="Set the logging output level. {0}".format(_LOG_LEVEL_STRINGS)) args = parser.parse_args() # Check arguments if args.station is None: parser.print_help() sys.exit() # Setting logging level numeric_level = args.log_level if not isinstance(numeric_level, int): raise ValueError("Invalid log level") logging.basicConfig(level=numeric_level, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") # Settings ground_station_id = args.station if args.duration > 0.0: length_hours = args.duration else: length_hours = 1.0 if args.wait <= 0: wait_time_seconds = 0 elif args.wait <= 3600: wait_time_seconds = args.wait else: wait_time_seconds = 3600 if args.min_priority < 0.0: min_priority = 0.0 elif args.min_priority > 1.0: min_priority = 1.0 else: min_priority = args.min_priority schedule = not args.dryrun only_priority = args.only_priority priority_filename = args.priorities # Set time range tnow = datetime.strptime(args.starttime, "%Y-%m-%dT%H:%M:%S") tmin = tnow tmax = tnow + timedelta(hours=length_hours) # Get ground station information ground_station = get_groundstation_info(ground_station_id, args.allow_testing) if not ground_station: sys.exit() # Create or update the transmitter & TLE cache cache = CacheManager(ground_station_id, ground_station['antenna'], settings.CACHE_DIR, settings.CACHE_AGE, settings.MAX_NORAD_CAT_ID) cache.update() # Set observer observer = ephem.Observer() observer.lon = str(ground_station['lng']) observer.lat = str(ground_station['lat']) observer.elevation = ground_station['altitude'] # Set minimum culmination elevation if args.min_culmination is None: min_culmination = ground_station['min_horizon'] else: if args.min_culmination < 0.0: min_culmination = 0.0 elif args.min_culmination > 90.0: min_culmination = 90.0 else: min_culmination = args.min_culmination # Set minimum rise/set elevation if args.min_riseset is None: min_riseset = ground_station['min_horizon'] else: if args.min_riseset < 0.0: min_riseset = 0.0 elif args.min_riseset > 90.0: min_riseset = 90.0 else: min_riseset = args.min_riseset # Use minimum altitude for computing rise and set times (horizon to horizon otherwise) if not args.horizon: observer.horizon = str(min_riseset) # Minimum duration of a pass min_pass_duration = settings.MIN_PASS_DURATION # Read tles tles = list(cache.read_tles()) # Read transmitters transmitters = cache.read_transmitters() # Extract satellites from receivable transmitters satellites = [] for transmitter in transmitters: for tle in tles: if tle['norad_cat_id'] == transmitter['norad_cat_id']: satellites.append(Satellite(Twolineelement(*tle['lines']), transmitter['uuid'], transmitter['success_rate'], transmitter['good_count'], transmitter['data_count'], transmitter['mode'])) # Find passes passes = [] logging.info('Finding all passes for %s satellites:' % len(satellites)) # Loop over satellites for satellite in tqdm(satellites): passes.extend(find_passes(satellite, observer, tmin, tmax, min_culmination, min_pass_duration)) priorities, favorite_transmitters = read_priorities_transmitters(priority_filename) # List of scheduled passes scheduledpasses = get_scheduled_passes_from_network(ground_station_id, tmin, tmax) logging.info("Found %d scheduled passes between %s and %s on ground station %d" % (len(scheduledpasses), tmin, tmax, ground_station_id)) # Get passes of priority objects prioritypasses, normalpasses = get_priority_passes(passes, priorities, favorite_transmitters, only_priority, min_priority) # Priority scheduler prioritypasses = sorted(prioritypasses, key=lambda satpass: -satpass['priority']) scheduledpasses = ordered_scheduler(prioritypasses, scheduledpasses, wait_time_seconds) for satpass in passes: logging.debug(satpass) # Normal scheduler normalpasses = sorted(normalpasses, key=lambda satpass: -satpass['priority']) scheduledpasses = ordered_scheduler(normalpasses, scheduledpasses, wait_time_seconds) # Report scheduling efficiency report_efficiency(scheduledpasses, passes) # Find unique objects satids = sorted(set([satpass['id'] for satpass in passes])) schedule_needed = False logging.info("GS | Sch | NORAD | Start time | End time | El | " + "Priority | Transmitter UUID | Mode | Satellite name ") for satpass in sorted(scheduledpasses, key=lambda satpass: satpass['tr']): logging.info( "%3d | %3.d | %05d | %s | %s | %3.0f | %4.6f | %s | %-10s | %s" % (ground_station_id, satpass['scheduled'], int( satpass['id']), satpass['tr'].strftime("%Y-%m-%dT%H:%M:%S"), satpass['ts'].strftime("%Y-%m-%dT%H:%M:%S"), float(satpass['altt']) if satpass['altt'] else 0., satpass['priority'], satpass['uuid'], satpass['mode'], satpass['name'].rstrip())) if not satpass['scheduled']: schedule_needed = True # Login and schedule passes if schedule and schedule_needed: # Sort passes scheduledpasses_sorted = sorted(scheduledpasses, key=lambda satpass: satpass['tr']) logging.info('Checking and scheduling passes as needed.') for satpass in tqdm(scheduledpasses_sorted): if not satpass['scheduled']: logging.debug("Scheduling %05d %s %s %3.0f %4.3f %s %s" % (int(satpass['id']), satpass['tr'].strftime("%Y-%m-%dT%H:%M:%S"), satpass['ts'].strftime("%Y-%m-%dT%H:%M:%S"), float(satpass['altt']), satpass['priority'], satpass['uuid'], satpass['name'].rstrip())) schedule_observation(satpass['uuid'], ground_station_id, satpass['tr'].strftime("%Y-%m-%d %H:%M:%S"), satpass['ts'].strftime("%Y-%m-%d %H:%M:%S")) logging.info("All passes are scheduled. Exiting!") if __name__ == '__main__': main()