#!/usr/bin/env python3 """ Module to process log files from Google's Android GNSS Logger app """ import datetime import math import re import sys # Flags to check wether the measurement is correct or not # https://developer.android.com/reference/android/location/GnssMeasurement.html#getState() STATE_2ND_CODE_LOCK = int(0x00010000) STATE_BDS_D2_BIT_SYNC = int(0x00000100) STATE_BDS_D2_SUBFRAME_SYNC = int(0x00000200) STATE_BIT_SYNC = int(0x00000002) STATE_CODE_LOCK = int(0x00000001) STATE_GAL_E1BC_CODE_LOCK = int(0x00000400) STATE_GAL_E1B_PAGE_SYNC = int(0x00001000) STATE_GAL_E1C_2ND_CODE_LOCK = int(0x00000800) STATE_GLO_STRING_SYNC = int(0x00000040) STATE_GLO_TOD_DECODED = int(0x00000080) STATE_GLO_TOD_KNOWN = int(0x00008000) STATE_MSEC_AMBIGUOUS = int(0x00000010) STATE_SBAS_SYNC = int(0x00002000) STATE_SUBFRAME_SYNC = int(0x00000004) STATE_SYMBOL_SYNC = int(0x00000020) STATE_TOW_DECODED = int(0x00000008) STATE_TOW_KNOWN = int(0x00004000) STATE_UNKNOWN = int(0x00000000) ADR_STATE_UNKNOWN = int(0x00000000) ADR_STATE_VALID = int(0x00000001) ADR_STATE_RESET = int(0x00000002) ADR_STATE_HALF_CYCLE_RESOLVED = int(0x00000008) ADR_STATE_HALF_CYCLE_REPORTED = int(0x00000010) ADR_STATE_CYCLE_SLIP = int(0x00000004) # Define constants SPEED_OF_LIGHT = 299792458.0 # [m/s] GPS_WEEKSECS = 604800 # Number of seconds in a week NS_TO_S = 1.0e-9 NS_TO_M = NS_TO_S * SPEED_OF_LIGHT # Constant to transform from nanoseconds to meters BDST_TO_GPST = 14 # Leap seconds difference between BDST and GPST GLOT_TO_UTC = 10800 # Time difference between GLOT and UTC in seconds # Origin of the GPS time scale GPSTIME = datetime.datetime(1980, 1, 6) DAYSEC = 86400 # Number of seconds in a day CURRENT_GPS_LEAP_SECOND = 18 OBS_LIST = ['C', 'L', 'D', 'S'] EPOCH_STR = 'epoch' GLO_L1_CENTER_FREQ = 1.60200e9 GLO_L1_DFREQ = 0.56250e6 # Constellation types CONSTELLATION_GPS = 1 CONSTELLATION_SBAS = 2 CONSTELLATION_GLONASS = 3 CONSTELLATION_QZSS = 4 CONSTELLATION_BEIDOU = 5 CONSTELLATION_GALILEO = 6 CONSTELLATION_UNKNOWN = 0 CONSTELLATION_LETTER = { CONSTELLATION_GPS : 'G', CONSTELLATION_SBAS : 'S', CONSTELLATION_GLONASS : 'R', CONSTELLATION_QZSS : 'J', CONSTELLATION_BEIDOU : 'C', CONSTELLATION_GALILEO : 'E', CONSTELLATION_UNKNOWN : 'X' } class GnssLogHeader(object): """ Class that manages the header from the log file. """ def __init__(self, filename): """ Initializes the header from a file handler to the file. Loads the parameters and field names present in the header part of the log """ self.parameters = {} self.fields = {} with open(filename, 'r') as fh: for line in fh: # Detect end of header if not line.startswith('#'): break # Skip empty lines if line.strip() == '#': continue fields = re.split('[: ,]', line.strip()) method_name = 'parse_{0}'.format(fields[1].lower()) # Call the method that processes the line getattr(GnssLogHeader, method_name)(self, line) def get_fieldnames(self, line): """ """ fields = [f.strip() for f in line[2:].strip().split(',')] # Skip initial hash character key = fields[0] field_names = fields[1:] self.fields[key] = field_names return def parse_header(self, line): """ Get parameters from header line, in this case do nothing """ pass def parse_version(self, line): """ Return a dictionary """ fields = line.strip().split(' ') for field in fields[1:]: if field.endswith(':'): key = field[:-1] self.parameters[key] = '' else: self.parameters[key] += ' ' + field # Clean superfluous spaces self.parameters = { k:self.parameters[k].strip() for k in self.parameters } def parse_fix(self, line): self.get_fieldnames(line) def parse_raw(self, line): """ """ self.get_fieldnames(line) def parse_nav(self, line): """ """ self.get_fieldnames(line) # ------------------------------------------------------------------------------ class GnssLog(object): """ """ CONVERTER = { 'AccumulatedDeltaRangeState' : int, 'ConstellationType' : int, 'MultipathIndicator' : int, 'State' : int, 'Svid' : int } def __init__(self, filename): """ """ # Criteria by which the different data batches are delimited self.BATCH_DELIMITER = 'TimeNanos' self.filename = filename self.header = GnssLogHeader(self.filename) def __field_conversion__(self, fname, valuestr): """ Convert the field, by default will be float, unless it exists in the CONVERTER structure. If an exception occurs, the field will be left as is. """ if fname in GnssLog.CONVERTER: return GnssLog.CONVERTER[fname](valuestr) try: return float(valuestr) except ValueError: return valuestr def __parse_line__(self, line): """ """ line_fields = line.strip().split(',') field_names = self.header.fields[line_fields[0]] fields = { field_names[i] : \ self.__field_conversion__(field_names[i], line_fields[i + 1]) \ for i in range(len(line_fields) - 1)} return fields def raw_batches(self): """ Generator function use to yield a batch """ batch = [] with open(self.filename, 'r') as fh: for line in fh: if not line.startswith('Raw'): continue line_fields = self.__parse_line__(line) if len(batch) > 0 and line_fields[self.BATCH_DELIMITER] != batch[0][self.BATCH_DELIMITER]: yield batch batch = [] batch.append(line_fields) # Yield last batch yield batch def fix_batches(self): """ Generator function used to yield a position batch """ with open(self.filename, 'r') as fh: for line in fh: if not line.startswith('Fix'): continue yield self.__parse_line__(line) # ------------------------------------------------------------------------------ def get_rnx_band_from_freq(frequency): """ Obtain the frequency band >>> get_rnx_band_from_freq(1575420030.0) 1 >>> get_rnx_band_from_freq(1600875010.0) 1 >>> get_rnx_band_from_freq(1176450050.0) 5 >>> get_rnx_band_from_freq(1561097980.0) 2 """ # Backwards compatibility with empty fields (assume GPS L1) ifreq = 154 if frequency == '' else round(frequency / 10.23e6) # QZSS L1 (154), GPS L1 (154), GAL E1 (154), and GLO L1 (156) if ifreq >= 154: return 1 # QZSS L5 (115), GPS L5 (115), GAL E5 (115) elif ifreq == 115: return 5 # BDS B1I (153) elif ifreq == 153: return 2 else: raise ValueError("Cannot get Rinex frequency band from frequency [ {0} ]. " "Got the following integer frequency multiplier [ {1:.2f} ]\n".format(frequency, ifreq)) return ifreq # ------------------------------------------------------------------------------ def get_rnx_attr(band, constellation='G', state=0x00): """ Generate the RINEX 3 attribute from a given band. Assumes 'C' for L1/E1 frequency and 'Q' for L5/E5a frequency. For E5a it assumes Q tracking. """ attr = 'C' # Make distinction between GAL E1C and E1B code if band == 1 and constellation == 'E': if (state & STATE_GAL_E1C_2ND_CODE_LOCK) == 0 and (state & STATE_GAL_E1B_PAGE_SYNC) != 0: attr = 'B' # GAL E5, QZSS L5, and GPS L5 (Q) if band == 5: attr = 'Q' # BDS B1I if band == 2 and constellation == 'C': attr = 'I' return attr # ------------------------------------------------------------------------------ def get_frequency(measurement): v = measurement['CarrierFrequencyHz'] return 154 * 10.23e6 if v == '' else v # ------------------------------------------------------------------------------ def get_obscode(measurement): """ Obtain the measurement code (RINEX 3 format) >>> get_obscode({'CarrierFrequencyHz': 1575420030.0, 'ConstellationType': 1}) '1C' >>> get_obscode({'CarrierFrequencyHz': 1176450050.0, 'ConstellationType': 5}) '5X' """ band = get_rnx_band_from_freq(get_frequency(measurement)) attr = get_rnx_attr(band, constellation=get_constellation(measurement), state=measurement['State']) return '{0}{1}'.format(band, attr) # ------------------------------------------------------------------------------ def get_obslist(batches): """ Obtain the observable list (array of RINEX 3.0 observable codes), particularized per each constellation, e.g. obs = { 'G' : [C1C, L1C, D1C, S1C, C5Q], 'E' : [C1C, L1C, D1C, C5Q], 'R' : [C1P, C2P] } """ obslist = {} for batch in batches: for measurement in batch: obscode = get_obscode(measurement) constellation = get_constellation(measurement) if constellation not in obslist: obslist[constellation] = [] arr = obslist[constellation] if obscode not in arr: obslist[constellation].append(obscode) # Sort observable list for all constellations for c in obslist: arr = sorted(obslist[c]) obslist[c] = [ m + o for o in arr for m in OBS_LIST ] return obslist # ------------------------------------------------------------------------------ def get_glo_freq_chn_list(batches): """ Obtain the GLO frequency channel list (array of RINEX 3.0 observable codes), particularized per each constellation """ freq_chn_list = {} for batch in batches: for measurement in batch: if measurement['ConstellationType'] == CONSTELLATION_GLONASS: try: sat = get_satname(measurement) except ValueError as e: sys.stderr.write("{0}\n".format(e)) continue if sat not in freq_chn_list: freq = get_frequency(measurement) freq_chn = round((freq - GLO_L1_CENTER_FREQ)/GLO_L1_DFREQ) freq_chn_list[sat] = freq_chn return freq_chn_list # ------------------------------------------------------------------------------ def get_glo_cod_phs_bis_list(batches): """ Obtain the GLO code phase bias for the receiver used. This is unknown for the moment """ cod_phs_bis_list = {} return cod_phs_bis_list # ------------------------------------------------------------------------------ def check_adr_state(measurement): """ Checks if measurement is valid or not based on the Sync bits """ # Obtain state, constellation type and frquency value to apply proper sync state state = measurement['AccumulatedDeltaRangeState'] if (state & ADR_STATE_VALID) == 0: raise ValueError("ADR State [ 0x{0:2x} {0:8b} ] has ADR_STATE_VALID [ 0x{1:2x} {1:8b} ] not valid".format(state, ADR_STATE_VALID)) return True # ------------------------------------------------------------------------------ def check_sync_state(measurement): """ Checks if measurement is valid or not based on the Sync bits """ # Obtain state, constellation type and frquency value to apply proper sync state state = measurement['State'] constellation = measurement['ConstellationType'] frequency = get_frequency(measurement) frequency_band = get_rnx_band_from_freq(frequency) # Filtering measurements for GPS constellation (common and non optional for L1 and L5 signals) if constellation == CONSTELLATION_GPS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_BIT_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_BIT_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_BIT_SYNC)) if (state & STATE_SUBFRAME_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_SUBFRAME_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_SUBFRAME_SYNC)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # Filtering measurements for SBAS constellation elif constellation == CONSTELLATION_SBAS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_BIT_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_BIT_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_BIT_SYNC)) if (state & STATE_SYMBOL_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_SYMBOL_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_SYMBOL_SYNC)) if (state & STATE_SBAS_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_SBAS_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_SBAS_SYNC)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # Filtering measurements for GLO constellation elif constellation == CONSTELLATION_GLONASS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_SYMBOL_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_SYMBOL_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_SYMBOL_SYNC)) if (state & STATE_BIT_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_BIT_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_BIT_SYNC)) if (state & STATE_GLO_TOD_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_GLO_TOD_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_GLO_TOD_DECODED)) if (state & STATE_GLO_STRING_SYNC) == 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_GLO_STRING_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_GLO_STRING_SYNC)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_QZSS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_BIT_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_BIT_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_BIT_SYNC)) if (state & STATE_SUBFRAME_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_SUBFRAME_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_SUBFRAME_SYNC)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_BEIDOU: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_BIT_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_BIT_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_BIT_SYNC)) if (state & STATE_SUBFRAME_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_SUBFRAME_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_SUBFRAME_SYNC)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_GALILEO: if frequency_band == 1: if (state & STATE_GAL_E1BC_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_GAL_E1BC_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_GAL_E1BC_CODE_LOCK)) # State value indicates presence of E1B code if (state & STATE_GAL_E1C_2ND_CODE_LOCK) == 0: if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_BIT_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_BIT_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_BIT_SYNC)) if (state & STATE_GAL_E1B_PAGE_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_GAL_E1B_PAGE_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_GAL_E1B_PAGE_SYNC)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # State value indicates presence of E1C code else: if (state & STATE_GAL_E1C_2ND_CODE_LOCK) == 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_GAL_E1BC_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format( state, STATE_GAL_E1C_2ND_CODE_LOCK)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # Measurement is E5a elif frequency_band == 5: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_BIT_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_BIT_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_BIT_SYNC)) if (state & STATE_SUBFRAME_SYNC) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_SUBFRAME_SYNC [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_SUBFRAME_SYNC)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_UNKNOWN: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) else: raise ValueError("ConstellationType [ 0x{0:2x} {0:8b} ] is not valid".format(constellation)) return True # ------------------------------------------------------------------------------ def check_trck_state(measurement): """ Checks if measurement is valid or not based on the Sync bits """ # Obtain state, constellation type and frequency value to apply proper sync state state = measurement['State'] constellation = measurement['ConstellationType'] frequency = get_frequency(measurement) frequency_band = get_rnx_band_from_freq(frequency) # Filtering measurements for GPS constellation (common and non optional for L1 and L5 signals) if constellation == CONSTELLATION_GPS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # Filtering measurements for SBAS constellation elif constellation == CONSTELLATION_SBAS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # Filtering measurements for GLO constellation elif constellation == CONSTELLATION_GLONASS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_GLO_TOD_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_GLO_TOD_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_GLO_TOD_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_QZSS: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_BEIDOU: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_GALILEO: if frequency_band == 1: if (state & STATE_GAL_E1BC_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_GAL_E1BC_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_GAL_E1BC_CODE_LOCK)) # State value indicates presence of E1B code if (state & STATE_GAL_E1C_2ND_CODE_LOCK) == 0: if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # State value indicates presence of E1C code else: if (state & STATE_GAL_E1C_2ND_CODE_LOCK) == 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_GAL_E1BC_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format( state, STATE_GAL_E1C_2ND_CODE_LOCK)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) # Measurement is E5a elif frequency_band == 5: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError( "State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) elif constellation == CONSTELLATION_UNKNOWN: if (state & STATE_CODE_LOCK) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_CODE_LOCK [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_CODE_LOCK)) if (state & STATE_TOW_DECODED) == 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_TOW_DECODED [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_TOW_DECODED)) if (state & STATE_MSEC_AMBIGUOUS) != 0: raise ValueError("State [ 0x{0:2x} {0:8b} ] has STATE_MSEC_AMBIGUOUS [ 0x{1:2x} {1:8b} ] not valid".format(state, STATE_MSEC_AMBIGUOUS)) else: raise ValueError("ConstellationType [ 0x{0:2x} {0:8b} ] is not valid".format(constellation)) return True # ------------------------------------------------------------------------------ def get_constellation(measurement): """ Return the constellation letter from a given measurement >>> get_constellation({'ConstellationType': 1}) 'G' >>> get_constellation({'ConstellationType': 6}) 'E' >>> get_constellation({'ConstellationType': 3}) 'R' >>> get_constellation({'ConstellationType': 5}) 'C' """ ctype = measurement['ConstellationType'] return CONSTELLATION_LETTER[ctype] # ------------------------------------------------------------------------------ def get_satname(measurement): """ Obtain the satellite name from a GNSS Logger measurement >>> get_satname({'ConstellationType': 1, 'Svid': 5}) 'G05' >>> get_satname({'ConstellationType': 6, 'Svid': 11}) 'E11' >>> get_satname({'ConstellationType': 3, 'Svid': 24}) 'R24' """ c = get_constellation(measurement) svid = measurement['Svid'] satname = '{0}{1:02d}'.format(c, svid) # Make sure that we report GLONASS OSN (PRN) instead of FCN # https://developer.android.com/reference/android/location/GnssStatus.html#getSvid(int) if svid > 50 and c == CONSTELLATION_LETTER[CONSTELLATION_GLONASS]: raise ValueError("-- WARNING: Skipping measurement for GLONASS sat " "without OSN [ {0} ]".format(satname)) return satname # ------------------------------------------------------------------------------ def process(measurement, fullbiasnanos=None, integerize=False, pseudorange_bias=0.0, filter_mode="sync"): """ Process a log measurement. This method computes the pseudorange, carrier-phase (in cycles) Doppler (cycles/s) as well as CN/0 :param measurement: GNSS Logger measurement line to process :param fullbiasnanos: Full Bias Nanos, used to either fix it to a certain value (if value is provided) or update it with the data if None (default value) :param integerize: Boolean to control whether to integerize the measurements to the nearest "integerized" time stamp (in this case to the nearest second) :param pseudorange_bias: Add an externally computed bias to the pseudorange. Default is 0.0 :param filter_mode: Specify the filtering scheme for the raw file. Acceptable values are sync: TOW/TOD known, code locked, no ambiguities are detected, and all remaining flags for the signal are set trck: TOW/TOD known, code locked and no ambiguities are detected """ try: satname = get_satname(measurement) except ValueError as e: sys.stderr.write("{0}\n".format(e)) return None obscode = get_obscode(measurement) # Set the fullbiasnanos if not set or if we need to update the fullbiasnanos at each epoch fullbiasnanos = measurement['FullBiasNanos'] if fullbiasnanos is None else fullbiasnanos # Obtain time nanos and bias nanos. Skip if None try: timenanos = float(measurement['TimeNanos']) except ValueError: raise ValueError("-- WARNING: Invalid value of TimeNanos or satellite [ {0} ]\n".format(satname)) try: biasnanos = float(measurement['BiasNanos']) except ValueError: biasnanos = 0.0 # Compute the GPS week number and reception time (i.e. clock epoch) gpsweek = math.floor(-fullbiasnanos * NS_TO_S / GPS_WEEKSECS) local_est_GPS_time = timenanos - (fullbiasnanos + biasnanos) gpssow = local_est_GPS_time * NS_TO_S - gpsweek * GPS_WEEKSECS # Fractional part of the integer seconds frac = gpssow - int(gpssow+0.5) if integerize else 0.0 # Convert the epoch to Python's buiit-in datetime class gpst_epoch = GPSTIME + datetime.timedelta(weeks=gpsweek, seconds=gpssow-frac) try: timeoffsetnanos = float(measurement['TimeOffsetNanos']) except ValueError: timeoffsetnanos = 0.0 # Compute the reception times tRxSeconds = gpssow - timeoffsetnanos * NS_TO_S # Compute wavelength for metric conversion in cycles wavelength = SPEED_OF_LIGHT / get_frequency(measurement) # Check sync state of the satellite for range computation try: if filter_mode == "sync": check_sync_state(measurement) elif filter_mode == "trck": check_trck_state(measurement) else: raise ValueError("-- ERROR: Invalid value of --filter-mode option") except ValueError as e: sys.stderr.write("-- WARNING: {0} for satellite [ {1} ]\n".format(e, satname)) range = 0 else: # Compute transmit time (depends on constellation of origin) constellation = measurement['ConstellationType'] # GLOT is given as TOD, need to change to TOW if constellation == CONSTELLATION_GLONASS: # Compute the UTC time tod_secs = measurement['ReceivedSvTimeNanos'] * NS_TO_S tTxSeconds = glot_to_gpst(gpst_epoch, tod_secs) # Compute the travel time, which will be eventually the pseudorange tau = check_week_crossover(tRxSeconds, tTxSeconds) # BDST uses different epoch as GPS elif constellation == CONSTELLATION_BEIDOU: tTxSeconds = measurement['ReceivedSvTimeNanos'] * NS_TO_S + BDST_TO_GPST # Compute the travel time, which will be eventually the pseudorange tau = check_week_crossover(tRxSeconds, tTxSeconds) # GPS, QZSS, GAL and SBAS share the same epoch time else: tTxSeconds = measurement['ReceivedSvTimeNanos'] * NS_TO_S # Compute the travel time, which will be eventually the pseudorange tau = check_week_crossover(tRxSeconds, tTxSeconds) # Compute the range as the difference between the received time and # the transmitted time range = tau * SPEED_OF_LIGHT - pseudorange_bias # Check if the range needs to be modified with the range rate in # order to make it consistent with the timestamp if integerize: range -= frac * measurement['PseudorangeRateMetersPerSecond'] # Check ADR state of the satellite for carrier phase computation try: check_adr_state(measurement) except ValueError as e: sys.stderr.write("-- WARNING: {0} for satellite [ {1} ]\n".format(e, satname)) cphase = 0 else: # Process the accumulated delta range (i.e. carrier phase). This # needs to be translated from meters to cycles (i.e. RINEX format # specification) cphase = measurement['AccumulatedDeltaRangeMeters'] / wavelength doppler = - measurement['PseudorangeRateMetersPerSecond'] / wavelength cn0 = measurement['Cn0DbHz'] return { EPOCH_STR : gpst_epoch, satname : { 'C' + obscode : range, 'L' + obscode : cphase, 'D' + obscode : doppler, 'S' + obscode : cn0}} # ------------------------------------------------------------------------------ def get_leap_seconds(current_epoch): """ Computes the number of leap seconds passed since the start of GPST :param current_epoch: current datetime value representing the measurements epoch :return: number of leap seconds since GPST """ return None # ------------------------------------------------------------------------------ def glot_to_gpst(gpst_current_epoch, tod_seconds): """ Converts GLOT to GPST :param gpst_current_epoch: Current epoch of the measurement in GPST :param tod_seconds: Time of days as number of seconds :return: Time of week in seconds """ (tod_sec_frac, tod_sec) = math.modf(tod_seconds) tod_sec = int(tod_sec) # Get the GLONASS epoch given the current GPS time glo_epoch = datetime.datetime(year=gpst_current_epoch.year, month=gpst_current_epoch.month, day=gpst_current_epoch.day, hour=gpst_current_epoch.hour, minute=gpst_current_epoch.minute, second=gpst_current_epoch.second)\ + datetime.timedelta( hours=3, seconds=-CURRENT_GPS_LEAP_SECOND) # Adjust the GLONASS time with the TOD measurements glo_tod = datetime.datetime(year=glo_epoch.year, month=glo_epoch.month, day=glo_epoch.day) + datetime.timedelta(seconds=tod_sec) # The day of week in seconds needs to reflect the time passed before the current day starts day_of_week_sec = (glo_tod.isoweekday()) * DAYSEC # Compute time of week in seconds tow_sec = day_of_week_sec + tod_seconds - GLOT_TO_UTC + CURRENT_GPS_LEAP_SECOND return tow_sec # ------------------------------------------------------------------------------ def check_week_crossover(tRxSeconds, tTxSeconds): """ Checks time propagation time for week crossover :param tRxSeconds: received time in seconds of week :param tTxSeconds: transmitted time in seconds of week :return: corrected propagation time """ tau = tRxSeconds - tTxSeconds if tau > GPS_WEEKSECS / 2: del_sec = round(tau/GPS_WEEKSECS)*GPS_WEEKSECS rho_sec = tau - del_sec if rho_sec > 10: tau = 0.0 else: tau = rho_sec return tau # ------------------------------------------------------------------------------ def check_day_crossover(tRxSeconds, tTxSeconds): """ Checks time propagation time for day crossover :param tRxSeconds: received time in seconds of week :param tTxSeconds: transmitted time in seconds of week :return: corrected propagation time """ tau = tRxSeconds - tTxSeconds if tau > DAYSEC / 2: del_sec = round(tau/DAYSEC)*DAYSEC rho_sec = tau - del_sec if rho_sec > 10: tau = 0.0 else: tau = rho_sec return tau # ------------------------------------------------------------------------------ def merge(measdict): """ Merge a list of processed batches, which are dictionaries with an epoch and an internal dictionary with the satellite measurements """ res = None for m in measdict: # Skip empty measurements if m is None: continue # Initialize if res is None: res = m continue exp_epoch = res[EPOCH_STR] got_epoch = m[EPOCH_STR] if got_epoch != exp_epoch: sys.stderr.write("Wrong measurement when merging batches. Expected " "[ {0} ], got [ {1} ]. Will be skipped\n".format(exp_epoch, got_epoch)) continue # Lambda method to get the satellites from the batch satsin = lambda x : [k for k in x.keys() if k is not EPOCH_STR] exp_sats = satsin(res) got_sats = satsin(m) # Loop over all the got satellites and merge them for sat in got_sats: if sat in exp_sats: res[sat].update(m[sat]) else: res[sat] = m[sat] return res # ------------------------------------------------------------------------------ if __name__ == '__main__': import doctest doctest.testmod(raise_on_error=True)