#!/data_systems/opt/bin/python3 # /usr/bin/python """ update_sc_clock_offset_file A script to read the latest clock offset file from OPS, and if necessary update the file /aim/sds/common/input_data/sc_offset.data. Typically the contents of the clock offset file look like this: AIM Clock Drift information as of Mar 20, 2017 - 13:21:57 Current offset: -1.5659642 s Current slope: -0.10804850 s/day However the typical file is not guaranteed, and the contents may differ. Created on 2017/03 @author: Bill Barrett """ import sys import os.path import re import time import traceback from decimal import Decimal # The standard incoming clock file INCOMING_CLOCK_FILE = '/home/aimpro/aim_clock_incoming/clock_info.txt' # The standard resulting clock file OUTPUT_CLOCK_FILE = '/aim/sds/common/input_data/sc_offset.dat' # Parse something like "AIM Clock Drift information as of Mar 20, 2017 - 13:21:57" COMPILE_INFO_DATE = re.compile(r''' ^.*\s+ # text followed by white space (?P [A-Za-z][a-z]{2,}\s+ # something like 'Mar' or 'June' [0-3]?\d,?\s+ # day of month, optionally followed by comma 20[0-4]\d # four digit year (\s+(\-\s+)? # optional dash and white space [0-2]\d:[0-5]\d:[0-6]\d(.*)?)?) # hour : minute : second .*$ # don't care about anything else on the line ''', re.VERBOSE) # NUMBER_REGEX is optional sign, followed by a decimal number # Note that because this has to match 0.1, 0, 0., and .1 # the numeric part is (AB|A|B) NUMBER_REGEX = r'(?P((\+|\-)?((\d+(\.\d*)?)|(\d*\.\d+))))' # The slope and offset have an almost identical pattern SLOPE_OFFSET_REGEX = r'.*{}:?\s+' + NUMBER_REGEX + r'.*' # Parse something like "Current offset: -1.5659642 s" OFFSET_REGEX = SLOPE_OFFSET_REGEX.format('offset') OFFSET_PATTTERN = re.compile(OFFSET_REGEX) # Parse something like "Current slope: -0.10804850 s/day" SLOPE_REGEX = SLOPE_OFFSET_REGEX.format('slope') SLOPE_PATTERN = re.compile(SLOPE_REGEX) # The file /aim/sds/common/input_data/sc_offset.dat has lines # that other than the header should start with something in # the form 'yyyyddd', optionally followed by a fraction of a day FILE_DATE_PATTERN = re.compile(r'^\s*(?P\d{7}(\.\d+)?),?\s+.*$') # The following value can never be a real value for the clock date, offset, and slope INVALID_OR_MISSING_DATA = -9999 # This will match something like either 'Jul 14 2019' or 'Jul 14 2019 06:24:31', # These are the two possibilities in the raw clock file TIME_OF_DAY_REGEX = r'(?P [0-2]\d:[0-5]\d:[0-6]\d)' RAW_DATE_FORMAT = re.compile(r'^(?P[a-zA-Z][a-z]{2,} [0-3]?\d 20[0-4]\d' + TIME_OF_DAY_REGEX + r'?).*') class ScClockOffSetUpdater: """ Read the latest clock offset file from OPS and, if necessary update the file /aim/sds/common/input_data/sc_offset.data. """ def __init__(self, clock_info_file): """ Initialize the place to find the raw TLE file and the dictionary of TLE's. Parameters ---------- clock_info_file : str the input file to be read """ assert os.path.isfile(clock_info_file), \ '{} is not a valid file'.format(clock_info_file) self.clock_info_file = clock_info_file print(time.strftime("%Y-%m-%d %H:%M:%S") + ' reading clock offset information from: {}'.format(self.clock_info_file)) # Initially all of the values that can be read from the file # are set to a no data value: a value that would be indicate a # catastrophic spacecraft failure if it were to occur in real life. self.clock_date = INVALID_OR_MISSING_DATA self.offset = INVALID_OR_MISSING_DATA self.slope = INVALID_OR_MISSING_DATA def __enter__(self): """ Read, remove duplicates and validate the raw TLE file Returns ------- self : object Per standard Python convention enter returns 'self' """ with open(self.clock_info_file, 'r') as clock_info_file: for line in clock_info_file: line = line.rstrip('\n') # Are we looking for the date if self.clock_date == INVALID_OR_MISSING_DATA: # result = COMPILE_INFO_DATE.fullmatch(line) result = COMPILE_INFO_DATE.match(line) if result is not None: self.set_clock_date(result.group('offset_date')) continue if self.offset == INVALID_OR_MISSING_DATA: result = OFFSET_PATTTERN.match(line) if result is not None: self.offset = Decimal(result.group('number')) continue if self.slope == INVALID_OR_MISSING_DATA: result = SLOPE_PATTERN.match(line) if result is not None: self.slope = Decimal(result.group('number')) continue return self def __exit__(self, exception_type, exception_value, traceback_info): """ Necessary if __enter__ is defined. Prints message on error exit. True for a normal exit, False if the exception_type is not None """ if exception_type is not None: print(time.strftime("%Y-%m-%d %H:%M:%S") + ' type: {0}, value: {1}'.format(exception_type, exception_value)) traceback.print_exception(exception_type, exception_value, traceback_info) return False # Comment to pass exception through return True def set_clock_date(self, input_date_string): """Try to parse a valid date for the offset and slope from the file's date line Parameters ---------- input_date_string : str a string from which to try to parse a date """ # Get rid of '-' and ',', if either are present clean_date = input_date_string.replace('-', '') clean_date = clean_date.replace(',', '') # Convert any sequence of more than one space to a single space clean_date = clean_date.replace(' ', ' ') # Get rid of any leading or trailing spaces clean_date = clean_date.strip() # Does what is left match the the expected pattern? date_match = RAW_DATE_FORMAT.match(clean_date) if date_match is None: print(time.strftime("%Y-%m-%d %H:%M:%S") + ' unable to parse clock date: {}'.format(input_date_string)) return # The expected pattern always has month, day, and year. # If it has time of day, parse that as well clean_date = date_match.group('file_date') date_array = clean_date.split() # If more than three characters are in the month string, # truncate the month so the that the '%b' format will work if len(date_array[0]) > 3: date_array[0] = date_array[0][:3] clean_date = ' '.join(date_array) # If the day of the month is less than ten, and not zero padded, # zero pad it so that the '%d' format will work if len(date_array[1]) == 1: date_array[1] = '0{}'.format(date_array[1]) clean_date = ' '.join(date_array) # Create a format and convert the date string to a time structure date_format = '%b %d %Y' if date_match.group('time_of_day') is not None: date_format += ' %H:%M:%S' date_struct = time.strptime(clean_date, date_format) # Using the members of the time structure # 24 hours per day, 1440 minutes per day, 86400 seconds per day self.clock_date = 1000 * date_struct.tm_year + date_struct.tm_yday + \ (float(date_struct.tm_hour) / 24.) + \ (float(date_struct.tm_min) / 1440.) + \ (float(date_struct.tm_sec) / 86400.) # If the clock date is garbage (before launch or too far in the future) # set it back to no data assert self.clock_date >= 2007026 and self.clock_date <= 2050000, \ time.strftime("%Y-%m-%d %H:%M:%S") + \ ' invalid value for clock date: {}'.format(input_date_string) def is_clock_info_txt_in_file(self, spacecraft_offset_file): """ Does the spacecraft offset file already contain this date? Parameters ---------- spacecraft_offset_file : str the path for the spacecraft offset file to be updated Returns ------- Boolean True if the clock data is already in the spacecraft offset file, False otherwise """ # Assume that the line is not duplicate until proven otherwise with open(spacecraft_offset_file, 'r') as offset_file: for line in offset_file: result = FILE_DATE_PATTERN.match(line) if result is not None: file_date = float(result.group('line_date')) # Approximate floating point comparison to compensate # for possible roundoff errors. # Accuracy is approximately the nearest second if abs(file_date - self.clock_date) <= 0.00001: print(time.strftime("%Y-%m-%d %H:%M:%S") + ' clock_info.txt matches sc_offset.dat at date: {}'. format(file_date)) return True return False def update_sc_offset_file(self, spacecraft_offset_file): """ Append the clock date, offset, and slope if the slope is valid to the spacecraft offset file Parameters ---------- spacecraft_offset_file : str the path for the spacecraft offset file to be updated clock_date : float the clock date as yyyyddd offset : Decimal the clock offset for that clock date slope : Decimal the slope of the clock offset for that clock date """ # Bad data should never be added to the file if self.clock_date == INVALID_OR_MISSING_DATA or self.offset == INVALID_OR_MISSING_DATA: return with open(spacecraft_offset_file, 'a') as offset_file: # Append the clock date, offset, and slope if the slope is valid. # Otherwise just append the clock date and offset if self.slope != INVALID_OR_MISSING_DATA: offset_file.write('{0} {1} {2}\n'.format(self.clock_date, self.offset, self.slope)) else: offset_file.write('{0} {1}\n'.format(self.clock_date, self.offset)) print(time.strftime("%Y-%m-%d %H:%M:%S") + ' {0} updated with: {1} {2} {3}'.format( spacecraft_offset_file, self.clock_date, self.offset, self.slope)) if __name__ == "__main__": # Read either a user specified clock file or /home/aimpro/aim_clock_incoming/clock_info.txt if len(sys.argv) > 1: CLOCK_INFO_FILE = sys.argv[1] else: CLOCK_INFO_FILE = INCOMING_CLOCK_FILE with ScClockOffSetUpdater(CLOCK_INFO_FILE) as clock_file_updater: if not clock_file_updater.is_clock_info_txt_in_file(OUTPUT_CLOCK_FILE): clock_file_updater.update_sc_offset_file(OUTPUT_CLOCK_FILE)