2018-08-22 14:35:35 -06:00
#!/usr/bin/env python
2018-12-02 06:53:56 -07:00
from __future__ import division
2018-08-22 14:35:35 -06:00
import requests
import ephem
from datetime import datetime , timedelta
2018-11-18 14:03:16 -07:00
from satellite_tle import fetch_tles
import os
import lxml . html
2018-11-26 09:56:30 -07:00
import argparse
2018-12-02 06:53:56 -07:00
import logging
2018-12-02 10:55:02 -07:00
from utils import get_active_transmitter_info , get_transmitter_stats , \
get_groundstation_info , get_last_update , get_scheduled_passes_from_network , ordered_scheduler , \
2019-03-18 13:01:09 -06:00
efficiency , find_passes , schedule_observation , read_priorities_transmitters , \
get_satellite_info
2018-12-02 10:55:02 -07:00
import settings
2018-12-19 02:00:27 -07:00
from tqdm import tqdm
2019-01-04 03:43:26 -07:00
import sys
2018-12-02 06:53:56 -07:00
_LOG_LEVEL_STRINGS = [ ' CRITICAL ' , ' ERROR ' , ' WARNING ' , ' INFO ' , ' DEBUG ' ]
2018-11-30 07:40:18 -07:00
2018-12-02 10:55:02 -07:00
2018-11-18 14:03:16 -07:00
class twolineelement :
""" TLE class """
2018-08-22 14:35:35 -06:00
def __init__ ( self , tle0 , tle1 , tle2 ) :
2018-11-18 14:03:16 -07:00
""" Define a TLE """
2018-08-22 14:35:35 -06:00
self . tle0 = tle0
self . tle1 = tle1
self . tle2 = tle2
2018-11-30 07:40:18 -07:00
if tle0 [ : 2 ] == " 0 " :
2018-08-22 14:35:35 -06:00
self . name = tle0 [ 2 : ]
else :
self . name = tle0
2018-11-30 07:40:18 -07:00
if tle1 . split ( " " ) [ 1 ] == " " :
2018-11-26 09:56:30 -07:00
self . id = int ( tle1 . split ( " " ) [ 2 ] [ : 4 ] )
else :
self . id = int ( tle1 . split ( " " ) [ 1 ] [ : 5 ] )
2018-11-18 14:03:16 -07:00
class satellite :
""" Satellite class """
2018-11-19 15:45:54 -07:00
def __init__ ( self , tle , transmitter , success_rate , good_count , data_count ) :
2018-11-18 14:03:16 -07:00
""" Define a satellite """
self . tle0 = tle . tle0
self . tle1 = tle . tle1
self . tle2 = tle . tle2
self . id = tle . id
2018-12-02 12:33:06 -07:00
self . name = tle . name . strip ( )
2018-11-18 14:03:16 -07:00
self . transmitter = transmitter
2018-11-19 15:45:54 -07:00
self . success_rate = success_rate
self . good_count = good_count
self . data_count = data_count
2018-08-22 14:35:35 -06:00
2018-12-02 12:33:06 -07:00
def __repr__ ( self ) :
2018-12-03 01:59:01 -07:00
return " %s %s %d %d %d %s " % ( self . id , self . transmitter , self . success_rate ,
self . good_count , self . data_count , self . name )
2018-11-30 07:40:18 -07:00
2018-12-02 06:53:56 -07:00
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
2018-11-15 13:08:48 -07:00
if __name__ == " __main__ " :
2018-11-26 09:56:30 -07:00
# Parse arguments
2018-11-30 07:40:18 -07:00
parser = argparse . ArgumentParser (
description = " Automatically schedule observations on a SatNOGS station. " )
2018-11-26 09:56:30 -07:00
parser . add_argument ( " -s " , " --station " , help = " Ground station ID " , type = int )
2018-12-29 04:41:52 -07:00
parser . add_argument ( " -t " , " --starttime " , help = " Start time (YYYY-MM-DD HH:MM:SS) [default: now] " ,
2019-01-19 11:35:58 -07:00
default = datetime . utcnow ( ) . strftime ( " % Y- % m- %d T % H: % M: % S " ) )
2018-12-29 05:34:12 -07:00
parser . add_argument ( " -d " , " --duration " , help = " Duration to schedule [hours; default 1.0] " , type = float , default = 1 )
2019-01-22 11:07:10 -07:00
parser . add_argument ( " -m " , " --min-horizon " , help = " Minimum horizon [default 0] " , type = float , default = 0. )
parser . add_argument ( " -f " , " --no-search-transmitters " , help = " Do not search good transmitters [default searching] " ,
dest = ' search_transmitters ' , action = ' store_false ' )
parser . set_defaults ( search_transmitters = True )
2018-12-29 05:34:12 -07:00
parser . add_argument ( " -w " , " --wait " ,
help = " Wait time between consecutive observations (for setup and slewing) [seconds; default: 0.0] " ,
type = float , default = 0 )
2019-01-15 09:43:36 -07:00
parser . add_argument ( " -u " , " --username " , help = " old SatNOGS Network username (NOT the new Auth0 username) " )
parser . add_argument ( " -p " , " --password " , help = " old SatNOGS Network password " )
2019-01-19 11:35:58 -07:00
parser . add_argument ( " -n " , " --dryrun " , help = " Dry run (do not schedule passes) " , action = " store_true " )
2019-01-26 07:25:05 -07:00
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 )
2018-12-29 04:41:52 -07:00
parser . add_argument ( " -l " , " --log-level " , default = " INFO " , dest = " log_level " ,
type = _log_level_string_to_int , nargs = " ? " ,
2018-12-02 06:53:56 -07:00
help = " Set the logging output level. {0} " . format ( _LOG_LEVEL_STRINGS ) )
2018-11-26 09:56:30 -07:00
args = parser . parse_args ( )
2018-11-30 07:40:18 -07:00
2019-01-04 03:43:26 -07:00
# Check arguments
if args . station == None :
parser . print_help ( )
sys . exit ( )
2018-12-02 06:53:56 -07:00
# Setting logging level
numeric_level = args . log_level
if not isinstance ( numeric_level , int ) :
2018-12-02 10:55:02 -07:00
raise ValueError ( " Invalid log level " )
2018-12-29 04:41:52 -07:00
logging . basicConfig ( level = numeric_level , format = " %(asctime)s - %(name)s - %(levelname)s - %(message)s " )
2018-12-02 06:53:56 -07:00
2018-11-15 13:08:48 -07:00
# Settings
2018-11-26 09:56:30 -07:00
ground_station_id = args . station
length_hours = args . duration
2018-12-29 05:34:12 -07:00
wait_time_seconds = args . wait
2019-01-22 11:07:10 -07:00
min_horizon_arg = args . min_horizon
2018-12-29 05:34:12 -07:00
if wait_time_seconds < 0 :
wait_time_seconds = 0.0
2018-11-18 14:03:16 -07:00
cache_dir = " /tmp/cache "
2018-11-26 09:56:30 -07:00
username = args . username
password = args . password
schedule = not args . dryrun
2019-01-22 11:07:10 -07:00
search_transmitters = args . search_transmitters
2019-01-26 07:25:05 -07:00
priority_filename = args . priorities
2018-11-18 14:03:16 -07:00
# Set time range
2018-11-26 09:56:30 -07:00
tnow = datetime . strptime ( args . starttime , " % Y- % m- %d T % H: % M: % S " )
2018-11-18 14:03:16 -07:00
tmin = tnow
2018-11-30 07:40:18 -07:00
tmax = tnow + timedelta ( hours = length_hours )
2018-11-18 14:03:16 -07:00
2018-11-15 13:08:48 -07:00
# Get ground station information
ground_station = get_groundstation_info ( ground_station_id )
2019-01-04 03:43:26 -07:00
# Exit if ground station is empty
if not ground_station :
sys . exit ( )
2018-11-18 14:03:16 -07:00
# Create cache
if not os . path . isdir ( cache_dir ) :
os . mkdir ( cache_dir )
# Get last update
2018-12-29 04:41:52 -07:00
tlast = get_last_update ( os . path . join ( cache_dir , " last_update_ %d .txt " % ground_station_id ) )
2018-11-18 14:03:16 -07:00
2018-11-19 15:45:54 -07:00
# Update logic
update = False
2018-12-03 03:02:32 -07:00
if tlast is None or ( tnow - tlast ) . total_seconds ( ) > settings . CACHE_AGE * 3600 :
2018-11-19 15:45:54 -07:00
update = True
2018-12-29 04:41:52 -07:00
if not os . path . isfile ( os . path . join ( cache_dir , " transmitters_ %d .txt " % ground_station_id ) ) :
2018-11-19 15:45:54 -07:00
update = True
2018-12-29 04:41:52 -07:00
if not os . path . isfile ( os . path . join ( cache_dir , " tles_ %d .txt " % ground_station_id ) ) :
2018-11-19 15:45:54 -07:00
update = True
2018-11-30 07:40:18 -07:00
2018-11-19 15:45:54 -07:00
# Update
if update :
2018-12-02 06:53:56 -07:00
logging . info ( ' Updating transmitters and TLEs for station ' )
2018-11-18 14:03:16 -07:00
# Store current time
2018-11-30 07:40:18 -07:00
with open ( os . path . join ( cache_dir , " last_update_ %d .txt " % ground_station_id ) , " w " ) as fp :
fp . write ( tnow . strftime ( " % Y- % m- %d T % H: % M: % S " ) + " \n " )
2018-11-18 14:03:16 -07:00
# Get active transmitters in frequency range of each antenna
2018-12-02 09:05:50 -07:00
transmitters = { }
2018-11-18 14:03:16 -07:00
for antenna in ground_station [ ' antenna ' ] :
2018-12-02 09:05:50 -07:00
for transmitter in get_active_transmitter_info ( antenna [ " frequency " ] ,
antenna [ " frequency_max " ] ) :
transmitters [ transmitter [ ' uuid ' ] ] = transmitter
2019-03-18 13:01:09 -06:00
# Get satellites which are alive
alive_norad_cat_ids = get_satellite_info ( )
2018-12-02 09:05:50 -07:00
# Get NORAD IDs
norad_cat_ids = sorted (
2018-12-15 02:57:24 -07:00
set ( [ transmitter [ " norad_cat_id " ] for transmitter in transmitters . values ( )
2019-03-18 13:01:09 -06:00
if transmitter [ " norad_cat_id " ] < settings . MAX_NORAD_CAT_ID
and transmitter [ " norad_cat_id " ] in alive_norad_cat_ids ] ) )
2018-11-18 14:03:16 -07:00
# Store transmitters
2018-12-29 04:41:52 -07:00
fp = open ( os . path . join ( cache_dir , " transmitters_ %d .txt " % ground_station_id ) , " w " )
2018-12-02 06:53:56 -07:00
logging . info ( " Requesting transmitter success rates. " )
2018-12-02 09:05:50 -07:00
transmitters_stats = get_transmitter_stats ( )
for transmitter in transmitters_stats :
2019-03-18 13:01:09 -06:00
# Skip absent transmitters
2018-12-02 09:05:50 -07:00
if not transmitter [ ' uuid ' ] in transmitters . keys ( ) :
2018-12-02 14:13:28 -07:00
continue
2019-03-18 13:01:09 -06:00
# Skip dead satellites
if transmitter [ " norad_cat_id " ] not in alive_norad_cat_ids :
continue
2018-12-29 04:41:52 -07:00
fp . write ( " %05d %s %d %d %d \n " %
( transmitter [ " norad_cat_id " ] ,
transmitter [ " uuid " ] ,
transmitter [ " success_rate " ] ,
transmitter [ " good_count " ] ,
transmitter [ " data_count " ] ) )
2018-12-02 09:05:50 -07:00
2018-12-02 06:53:56 -07:00
logging . info ( " Transmitter success rates received! " )
2018-11-18 14:03:16 -07:00
fp . close ( )
2018-11-30 07:40:18 -07:00
2018-11-18 14:03:16 -07:00
# Get TLEs
tles = fetch_tles ( norad_cat_ids )
# Store TLEs
2018-12-29 04:41:52 -07:00
fp = open ( os . path . join ( cache_dir , " tles_ %d .txt " % ground_station_id ) , " w " )
2018-11-18 14:03:16 -07:00
for norad_cat_id , ( source , tle ) in tles . items ( ) :
2018-11-30 07:40:18 -07:00
fp . write ( " %s \n %s \n %s \n " % ( tle [ 0 ] , tle [ 1 ] , tle [ 2 ] ) )
2018-11-18 14:03:16 -07:00
fp . close ( )
2018-11-30 07:40:18 -07:00
2018-11-15 13:08:48 -07:00
# Set observer
observer = ephem . Observer ( )
observer . lon = str ( ground_station [ ' lng ' ] )
observer . lat = str ( ground_station [ ' lat ' ] )
observer . elevation = ground_station [ ' altitude ' ]
2019-01-22 11:07:10 -07:00
minimum_altitude = max ( ground_station [ ' min_horizon ' ] , min_horizon_arg )
2018-11-15 13:08:48 -07:00
2018-11-18 14:03:16 -07:00
# Read tles
2018-11-30 07:40:18 -07:00
with open ( os . path . join ( cache_dir , " tles_ %d .txt " % ground_station_id ) , " r " ) as f :
2018-11-15 13:08:48 -07:00
lines = f . readlines ( )
2018-11-30 07:40:18 -07:00
tles = [ twolineelement ( lines [ i ] , lines [ i + 1 ] , lines [ i + 2 ] )
2018-11-18 14:03:16 -07:00
for i in range ( 0 , len ( lines ) , 3 ) ]
2018-11-15 13:08:48 -07:00
2018-12-02 12:33:06 -07:00
# Read transmitters
2018-11-18 14:03:16 -07:00
satellites = [ ]
2018-11-30 07:40:18 -07:00
with open ( os . path . join ( cache_dir , " transmitters_ %d .txt " % ground_station_id ) , " r " ) as f :
2018-11-18 14:03:16 -07:00
lines = f . readlines ( )
for line in lines :
2018-11-19 15:45:54 -07:00
item = line . split ( )
2018-11-30 07:40:18 -07:00
norad_cat_id , uuid , success_rate , good_count , data_count = int (
item [ 0 ] ) , item [ 1 ] , float ( item [ 2 ] ) / 100.0 , int ( item [ 3 ] ) , int ( item [ 4 ] )
2018-11-18 14:03:16 -07:00
for tle in tles :
if tle . id == norad_cat_id :
2018-12-29 04:41:52 -07:00
satellites . append ( satellite (
tle ,
uuid ,
success_rate ,
good_count ,
data_count ) )
2018-12-03 01:59:01 -07:00
2018-11-15 13:08:48 -07:00
# Find passes
passes = find_passes ( satellites , observer , tmin , tmax , minimum_altitude )
2019-01-22 22:13:55 -07:00
# Priorities and favorite transmitters
# read the following format
# 43017 1. KgazZMKEa74VnquqXLwAvD
2019-01-26 07:25:05 -07:00
if priority_filename != None and os . path . exists ( priority_filename ) :
priorities , favorite_transmitters = read_priorities_transmitters ( priority_filename )
2019-01-22 22:13:55 -07:00
else :
priorities , favorite_transmitters = { } , { }
2018-11-30 07:40:18 -07:00
2018-08-22 14:35:35 -06:00
# List of scheduled passes
2018-12-29 05:34:12 -07:00
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 ) )
2018-11-30 07:40:18 -07:00
2018-08-22 14:35:35 -06:00
# Get passes of priority objects
prioritypasses = [ ]
normalpasses = [ ]
for satpass in passes :
# Get user defined priorities
if satpass [ ' id ' ] in priorities :
satpass [ ' priority ' ] = priorities [ satpass [ ' id ' ] ]
2019-01-19 11:35:58 -07:00
if satpass [ ' id ' ] in favorite_transmitters :
satpass [ ' uuid ' ] = favorite_transmitters [ satpass [ ' id ' ] ]
2018-08-22 14:35:35 -06:00
prioritypasses . append ( satpass )
2019-01-22 11:07:10 -07:00
elif search_transmitters :
2018-12-16 10:25:26 -07:00
# Find satellite transmitter with highest number of good observations
max_good_count = max ( [ s [ ' good_count ' ] for s in passes if s [ " id " ] == satpass [ " id " ] ] )
2018-12-19 04:39:29 -07:00
if max_good_count > 0 :
satpass [ ' priority ' ] = \
( float ( satpass [ ' altt ' ] ) / 90.0 ) \
* satpass [ ' success_rate ' ] \
* float ( satpass [ ' good_count ' ] ) / max_good_count
2018-12-16 10:28:45 -07:00
else :
satpass [ ' priority ' ] = (
float ( satpass [ ' altt ' ] ) / 90.0 ) * satpass [ ' success_rate ' ]
2018-08-22 14:35:35 -06:00
normalpasses . append ( satpass )
2018-11-30 07:40:18 -07:00
2018-08-22 14:35:35 -06:00
# Priority scheduler
2018-12-29 05:34:12 -07:00
prioritypasses = sorted ( prioritypasses , key = lambda satpass : - satpass [ ' priority ' ] )
scheduledpasses = ordered_scheduler ( prioritypasses , scheduledpasses , wait_time_seconds )
2018-12-12 02:13:02 -07:00
for satpass in passes :
logging . debug ( satpass )
2018-08-22 14:35:35 -06:00
2018-12-29 05:34:12 -07:00
# Normal scheduler
normalpasses = sorted ( normalpasses , key = lambda satpass : - satpass [ ' priority ' ] )
scheduledpasses = ordered_scheduler ( normalpasses , scheduledpasses , wait_time_seconds )
2018-08-22 14:35:35 -06:00
2018-12-29 05:34:12 -07:00
# Compute scheduling efficiency
2018-08-22 14:35:35 -06:00
dt , dttot , eff = efficiency ( scheduledpasses )
2018-12-29 05:34:12 -07:00
logging . info ( " %d passes scheduled out of %d , %.0f s out of %.0f s at %.3f %% efficiency " %
( len ( scheduledpasses ) , len ( passes ) , dt , dttot , 100 * eff ) )
2018-11-30 07:40:18 -07:00
2018-08-22 14:35:35 -06:00
# Find unique objects
satids = sorted ( set ( [ satpass [ ' id ' ] for satpass in passes ] ) )
2018-12-03 01:59:01 -07:00
schedule_needed = False
2019-01-04 03:43:26 -07:00
logging . info ( " GS | Sch | NORAD | Start time | End time | El | " +
2018-12-19 04:39:29 -07:00
" Priority | Transmitter UUID | Satellite name " )
2018-08-22 14:35:35 -06:00
for satpass in sorted ( scheduledpasses , key = lambda satpass : satpass [ ' tr ' ] ) :
2018-12-19 04:39:29 -07:00
logging . info (
2019-01-04 03:43:26 -07:00
" %3d | % 3.d | %05d | %s | %s | %3.0f | %4.6f | %s | %s " %
( ground_station_id ,
satpass [ ' scheduled ' ] ,
2018-12-19 04:39:29 -07:00
int ( satpass [ ' id ' ] ) ,
satpass [ ' tr ' ] . strftime ( " % Y- % m- %d T % H: % M: % S " ) ,
satpass [ ' ts ' ] . strftime ( " % Y- % m- %d T % H: % M: % S " ) ,
2019-01-25 14:27:19 -07:00
float ( satpass [ ' altt ' ] ) if satpass [ ' altt ' ] else 0. ,
2018-12-19 04:39:29 -07:00
satpass [ ' priority ' ] ,
satpass [ ' uuid ' ] ,
satpass [ ' name ' ] . rstrip ( ) ) )
2018-11-30 07:40:18 -07:00
if not satpass [ ' scheduled ' ] :
2018-12-03 01:59:01 -07:00
schedule_needed = True
2019-01-04 03:43:26 -07:00
2018-12-03 01:59:01 -07:00
# Login and schedule passes
if schedule and schedule_needed :
loginUrl = ' {} /accounts/login/ ' . format ( settings . NETWORK_BASE_URL ) # login URL
session = requests . session ( )
login = session . get ( loginUrl ) # Get login page for CSFR token
login_html = lxml . html . fromstring ( login . text )
login_hidden_inputs = login_html . xpath (
r ' //form//input[@type= " hidden " ] ' ) # Get CSFR token
form = { x . attrib [ " name " ] : x . attrib [ " value " ] for x in login_hidden_inputs }
form [ " login " ] = username
form [ " password " ] = password
2019-01-31 13:10:53 -07:00
# Login
session . post ( loginUrl ,
data = form ,
headers = { ' referer ' : loginUrl ,
' user-agent ' : ' satnogs-auto-scheduler/0.0.1 ' } )
2018-12-03 01:59:01 -07:00
2018-12-19 04:39:29 -07:00
scheduledpasses_sorted = sorted ( scheduledpasses , key = lambda satpass : satpass [ ' tr ' ] )
2018-12-19 02:00:27 -07:00
logging . info ( ' Checking and scheduling passes as needed. ' )
2018-12-19 04:39:29 -07:00
for satpass in tqdm ( scheduledpasses_sorted ) :
2018-12-03 01:59:01 -07:00
if not satpass [ ' scheduled ' ] :
2018-12-12 02:13:02 -07:00
logging . debug (
" Scheduling %05d %s %s %3.0f %4.3f %s %s " %
2018-12-29 04:41:52 -07:00
( int ( satpass [ ' id ' ] ) ,
satpass [ ' tr ' ] . strftime ( " % Y- % m- %d T % H: % M: % S " ) ,
satpass [ ' ts ' ] . strftime ( " % Y- % m- %d T % H: % M: % S " ) ,
float ( satpass [ ' altt ' ] ) ,
satpass [ ' priority ' ] ,
satpass [ ' uuid ' ] ,
satpass [ ' name ' ] . rstrip ( ) ) )
2018-12-02 06:54:33 -07:00
schedule_observation ( session ,
2018-11-18 14:03:16 -07:00
int ( satpass [ ' id ' ] ) ,
satpass [ ' uuid ' ] ,
ground_station_id ,
2018-11-30 07:40:18 -07:00
satpass [ ' tr ' ] . strftime ( " % Y- % m- %d % H: % M: % S " ) + " .000 " ,
satpass [ ' ts ' ] . strftime ( " % Y- % m- %d % H: % M: % S " ) + " .000 " )
2018-12-12 02:13:02 -07:00
2018-12-19 04:39:29 -07:00
logging . info ( " All passes are scheduled. Exiting! " )