├── Galileo_HAS_Parser.ipynb ├── README.md ├── __pycache__ ├── has_corrections.cpython-38.pyc ├── has_decoder.cpython-38.pyc ├── has_message.cpython-38.pyc └── has_message.cpython-39.pyc ├── data ├── 220928_000030_javad_Delta3_k.jps ├── 220928_000100_pwrpak7_k.gps └── SEPT271k.22_.zip ├── data_loading.py ├── has_corrections.py ├── has_decoder.py ├── has_encoding_matrix.csv ├── has_message.py ├── has_widgets.py ├── manual └── GHASP_user_manual_Feb23.pdf ├── plot ├── clk_adev.py ├── plot_all.py ├── plot_cb.py ├── plot_clk.py ├── plot_cp.py └── plot_orb.py ├── process_cnav.py ├── reed_solomon.py ├── test_rd.py └── timefun.py /Galileo_HAS_Parser.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "d456bc28", 6 | "metadata": {}, 7 | "source": [ 8 | "# Galileo HAS Parser (GHASP) #" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "32e829e2", 14 | "metadata": {}, 15 | "source": [ 16 | "### Import libraries ###" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "id": "3f2771f7", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Some widgets\n", 27 | "import has_widgets as hw\n", 28 | "from ipyfilechooser import FileChooser" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "id": "6e4e4f62", 34 | "metadata": {}, 35 | "source": [ 36 | "### Open the input type file ###" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 2, 42 | "id": "f147f017-4ddd-49d2-b657-4295e70f3c44", 43 | "metadata": {}, 44 | "outputs": [ 45 | { 46 | "data": { 47 | "application/vnd.jupyter.widget-view+json": { 48 | "model_id": "f5873b01703d4c56bca102a981528522", 49 | "version_major": 2, 50 | "version_minor": 0 51 | }, 52 | "text/plain": [ 53 | "FileChooser(path='E:\\Projects\\2022\\github\\has\\HAS-decoding-main', filename='', title='', show_hidden=False, se…" 54 | ] 55 | }, 56 | "metadata": {}, 57 | "output_type": "display_data" 58 | } 59 | ], 60 | "source": [ 61 | "fc = FileChooser()\n", 62 | "fc" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "id": "0bacc627", 69 | "metadata": {}, 70 | "outputs": [ 71 | { 72 | "name": "stdout", 73 | "output_type": "stream", 74 | "text": [ 75 | "E:\\Projects\\2022\\github\\has\\data\\SEPT267.sbf\n" 76 | ] 77 | } 78 | ], 79 | "source": [ 80 | "filename = fc.selected\n", 81 | "print(filename)" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "id": "a12837e2-b4eb-4c63-a58f-a85ed7102fe4", 87 | "metadata": {}, 88 | "source": [ 89 | "### Set the file options ### " 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 4, 95 | "id": "2a0976fa-ddfd-4ac2-b76c-bb5e3b8f488b", 96 | "metadata": {}, 97 | "outputs": [ 98 | { 99 | "data": { 100 | "application/vnd.jupyter.widget-view+json": { 101 | "model_id": "a236367b1a4746e7a64be22ef23956d3", 102 | "version_major": 2, 103 | "version_minor": 0 104 | }, 105 | "text/plain": [ 106 | "receiver_type_widget(children=(HTML(value='Receiver type:'), Dropdown(description='Rx Type:', options=(…" 107 | ] 108 | }, 109 | "metadata": {}, 110 | "output_type": "display_data" 111 | } 112 | ], 113 | "source": [ 114 | "has_w = hw.receiver_type_widget()\n", 115 | "has_w" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "id": "b7a85082-0c3d-4f3d-8ff9-5b6ac4302c8d", 121 | "metadata": {}, 122 | "source": [ 123 | "### Finally parse the data ###" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 5, 129 | "id": "c690ea2a-9fb2-470a-af63-70081e46e300", 130 | "metadata": {}, 131 | "outputs": [ 132 | { 133 | "data": { 134 | "application/vnd.jupyter.widget-view+json": { 135 | "model_id": "991dfb8f7f4342028e9eee00c585c81e", 136 | "version_major": 2, 137 | "version_minor": 0 138 | }, 139 | "text/plain": [ 140 | "process_button_widget(children=(Button(description='Parse', icon='Initialize', style=ButtonStyle()),), layout=…" 141 | ] 142 | }, 143 | "metadata": {}, 144 | "output_type": "display_data" 145 | } 146 | ], 147 | "source": [ 148 | "parse_button = hw.process_button_widget(filename, has_w.get_options())\n", 149 | "parse_button" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": null, 155 | "id": "22f9fe2a-5c40-4313-9a96-5b2941f347a0", 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [] 159 | } 160 | ], 161 | "metadata": { 162 | "kernelspec": { 163 | "display_name": "Python 3 (ipykernel)", 164 | "language": "python", 165 | "name": "python3" 166 | }, 167 | "language_info": { 168 | "codemirror_mode": { 169 | "name": "ipython", 170 | "version": 3 171 | }, 172 | "file_extension": ".py", 173 | "mimetype": "text/x-python", 174 | "name": "python", 175 | "nbconvert_exporter": "python", 176 | "pygments_lexer": "ipython3", 177 | "version": "3.9.8" 178 | } 179 | }, 180 | "nbformat": 4, 181 | "nbformat_minor": 5 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Galileo HAS Parser: GHASP 2 | 3 | The Galileo High Accuracy Service (HAS) was declared operational on 24th January 2023, during the annual European Space Conference. Through HAS, Galileo is broadcasting orbit, clock and measurement corrections enabling decimeter level positioning. 4 | 5 | HAS corrections are distributed through the E6B signal, which adopts a high-parity vertical Reed-Solomon encoding scheme. Thus, E6B signals need to be decoded in order to recover the HAS corrections enabling Precise Point Positioning (PPP). 6 | 7 | In order to foster the use of HAS corrections, a HAS parser has been developed. This is the user manual of such parser, which has been denoted as Galileo HAS Parser (GHASP). 8 | 9 | GHASP supports several data types from different receiver types and converts E6B data messages, recorded as binary streams, into actual PPP corrections. The outputs of the parser are four Comma-Separated Values (CSV) files containing the different correction types. The corrections can be easily loaded using any scientific programming language and used for different analyses in addition to PPP applications. 10 | -------------------------------------------------------------------------------- /__pycache__/has_corrections.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/__pycache__/has_corrections.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/has_decoder.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/__pycache__/has_decoder.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/has_message.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/__pycache__/has_message.cpython-38.pyc -------------------------------------------------------------------------------- /__pycache__/has_message.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/__pycache__/has_message.cpython-39.pyc -------------------------------------------------------------------------------- /data/220928_000030_javad_Delta3_k.jps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/data/220928_000030_javad_Delta3_k.jps -------------------------------------------------------------------------------- /data/220928_000100_pwrpak7_k.gps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/data/220928_000100_pwrpak7_k.gps -------------------------------------------------------------------------------- /data/SEPT271k.22_.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/data/SEPT271k.22_.zip -------------------------------------------------------------------------------- /data_loading.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon May 9 07:08:06 2022 5 | 6 | @author: daniele 7 | """ 8 | 9 | import pandas as pd 10 | import numpy as np 11 | import timefun as tf 12 | import struct 13 | 14 | """ 15 | Summary : 16 | Set of functions implementing data loading from different file formats 17 | 18 | Mainly based on the code developed by Melania Susi. 19 | """ 20 | 21 | 22 | def load_from_parsed_Septentrio(filename : str, _type : str = "hexa" ) : 23 | """ 24 | Summary : 25 | Load the data from a text file obtained by parsing the GALRawCNAV 26 | message. 27 | 28 | Arguments : 29 | filename - pathname of the file to be loaded 30 | _type - indicates the way data have been parsed 31 | txt: payload as 32 bit integers 32 | hexa: payload as sequence of hexadecimal values 33 | """ 34 | # Specify the data type 35 | data_types = {"word 1" : np.uint32, "word 2" : np.uint32, "word 3" : np.uint32, 36 | "word 4" : np.uint32, "word 5" : np.uint32, "word 6" : np.uint32, 37 | "word 7" : np.uint32, "word 8" : np.uint32, "word 9" : np.uint32, 38 | "word 10" : np.uint32, "word 11" : np.uint32, "word 12" : np.uint32, 39 | "word 13" : np.uint32, "word 14" : np.uint32, "word 15" : np.uint32, 40 | "word 16" : np.uint32} 41 | 42 | if _type == "txt" : 43 | # header_list = ["TOW", "WNc [w]", "SVID", "CRCPassed", "ViterbiCnt", "signalType", "NAVBits"] 44 | header_list = ["TOW", "WNc [w]", "SVID", "CRCPassed", "ViterbiCnt", "signalType", "word 1", \ 45 | "word 2", "word 3", "word 4", "word 5", "word 6", "word 7", "word 8",\ 46 | "word 9", "word 10", "word 11", "word 12", "word 13", "word 14",\ 47 | "word 15", "word 16"] 48 | 49 | 50 | if filename.endswith('.zip') : 51 | df = pd.read_csv(filename, compression='zip', sep=',| ', names=header_list, \ 52 | engine='python', dtype = data_types) 53 | else : 54 | df = pd.read_csv(filename, sep=',| ', names=header_list, \ 55 | engine='python', dtype = data_types) 56 | 57 | elif _type == "hexa" : 58 | header_list = ["TOW", "WNc [w]", "SVID", "CRCPassed", "ViterbiCnt", "signalType",\ 59 | "VITERBI_TYPE","RxChannel","word 1", \ 60 | "word 2", "word 3", "word 4", "word 5", "word 6", "word 7", "word 8",\ 61 | "word 9", "word 10", "word 11", "word 12", "word 13", "word 14",\ 62 | "word 15", "word 16"] 63 | 64 | converters = { 65 | "word 1" : lambda x: int(x, 16),\ 66 | "word 2" : lambda x: int(x, 16),\ 67 | "word 3" : lambda x: int(x, 16),\ 68 | "word 4" : lambda x: int(x, 16),\ 69 | "word 5" : lambda x: int(x, 16),\ 70 | "word 6" : lambda x: int(x, 16),\ 71 | "word 7" : lambda x: int(x, 16),\ 72 | "word 8" : lambda x: int(x, 16),\ 73 | "word 9" : lambda x: int(x, 16),\ 74 | "word 10" : lambda x: int(x, 16),\ 75 | "word 11" : lambda x: int(x, 16),\ 76 | "word 12" : lambda x: int(x, 16),\ 77 | "word 13" : lambda x: int(x, 16),\ 78 | "word 14" : lambda x: int(x, 16),\ 79 | "word 15" : lambda x: int(x, 16),\ 80 | "word 16" : lambda x: int(x, 16) 81 | } 82 | 83 | if filename.endswith('.zip') : 84 | df = pd.read_csv(filename, compression='zip', sep=',| ', names=header_list, \ 85 | engine='python', converters = converters) 86 | else : 87 | df = pd.read_csv(filename, sep=',| ', names=header_list, \ 88 | engine='python', converters = converters) 89 | 90 | IsInterpreted = False 91 | 92 | # Apply specific processing depending if data have been interpreted or if left raw 93 | if isinstance(df["CRCPassed"].values[0], str) : 94 | df["CRCPassed"] = (df["CRCPassed"].values == "Passed") 95 | IsInterpreted = True 96 | 97 | if isinstance(df["SVID"].values[0], str) : 98 | df["SVID"] = df["SVID"].apply(lambda x: int(x[1:])) 99 | IsInterpreted = True 100 | 101 | if not IsInterpreted : 102 | # In this case, the Tow is expressed in ms 103 | # Covert it in s 104 | df["TOW"] = (df["TOW"].values / 1000).astype(int) 105 | 106 | # The Galileo satellite IDs have an offset of 70 107 | df["SVID"] = df["SVID"].values - 70 108 | 109 | return df 110 | 111 | def load_from_binary_Septentrio(filename : str) : 112 | """ 113 | Summary : 114 | Load the data from a Septentrio (SBF) binary file. 115 | 116 | Arguments : 117 | filename - pathname of the file to be loaded 118 | """ 119 | 120 | # Open the input file 121 | fid = open(filename, "rb") 122 | 123 | # Dictionary with the parsed information 124 | data = { "TOW" : [], 125 | "WNc [w]" : [], 126 | "SVID": [], 127 | "CRCPassed" : [], 128 | "ViterbiCnt" : [], 129 | "signalType" : [], 130 | "word 1": [], 131 | "word 2": [], 132 | "word 3": [], 133 | "word 4": [], 134 | "word 5": [], 135 | "word 6": [], 136 | "word 7": [], 137 | "word 8": [], 138 | "word 9": [], 139 | "word 10": [], 140 | "word 11": [], 141 | "word 12": [], 142 | "word 13": [], 143 | "word 14": [], 144 | "word 15": [], 145 | "word 16": [] 146 | } 147 | 148 | # Local functions 149 | def get_message( fid ) : 150 | """ 151 | Summary : 152 | Identify a message and return the message ID and payload 153 | 154 | Arguments : 155 | fid - pointer to the binary file 156 | 157 | Returns : 158 | messageID - the message ID 159 | payload - the message payload 160 | """ 161 | 162 | # First identify the start of the message 163 | sync1 = fid.read(1) 164 | 165 | if sync1 == b"" : 166 | return None 167 | 168 | sync2 = fid.read(1) 169 | 170 | if sync2 == b"" : 171 | return None 172 | 173 | # Move into the file until the synch pattern is found 174 | while( (int.from_bytes(sync1, 'little') != 36) | \ 175 | (int.from_bytes(sync2, 'little') != 64) ) : 176 | sync1 = sync2 177 | sync2 = fid.read(1) 178 | 179 | if sync2 == b"" : 180 | return None 181 | 182 | # If here, the synchronization pattern has been found 183 | # So, read the message header 184 | header = fid.read(6) 185 | 186 | messageID = int.from_bytes(header[2:4], 'little') 187 | 188 | length = int.from_bytes(header[4:6], 'little') 189 | 190 | # Now read the message 191 | msg = fid.read(length - 8) 192 | 193 | if len(msg) != length - 8 : 194 | return None 195 | # Check here the crc 196 | # To be added 197 | # crc = int.from_bytes(header[2:4], 'little') 198 | 199 | 200 | return messageID, msg 201 | 202 | def get_time_stamp( msg ) : 203 | """ 204 | Summary : 205 | Extract the time stamp (TOW and Week Number) from the message 206 | 207 | Arguments : 208 | msg - byte sequence containtaing the message 209 | 210 | Returns : 211 | ToW - Time of week (in seconds) 212 | WN - week number 213 | """ 214 | ToW = int.from_bytes(msg[:4], 'little') / 1000 215 | WN = int.from_bytes(msg[4:6], 'little') 216 | 217 | return ToW, WN 218 | 219 | # Main processing loop 220 | while True : 221 | msg = get_message( fid ) 222 | 223 | if msg is None : 224 | break 225 | 226 | # Get the message ID 227 | msgID = msg[0] 228 | 229 | # Consider only the GALRawCNAV message 230 | if msgID != 4024 : 231 | continue 232 | 233 | # get the time stamps 234 | ToW, WN = get_time_stamp( msg[1] ) 235 | 236 | data["TOW"].append(ToW) 237 | data["WNc [w]"].append(WN) 238 | 239 | # get the other data 240 | sig_info = struct.unpack('BBBBBB', msg[1][6:12]) 241 | 242 | # Other data value set to default values 243 | data["SVID"].append(sig_info[0] - 70) 244 | data["CRCPassed"].append(sig_info[1]) 245 | data["ViterbiCnt"].append(sig_info[2]) 246 | data["signalType"].append(sig_info[3]) 247 | 248 | # save the 16 words (16 x 32 bits = 512) 249 | cnav_msg = struct.unpack('IIIIIIIIIIIIIIII', msg[1][12:]) 250 | for ii in range(16) : 251 | data[f"word {ii + 1}"].append(cnav_msg[ii]) 252 | 253 | # close the inpu file 254 | fid.close() 255 | 256 | # create the output dataframe 257 | df = pd.DataFrame(data=data) 258 | 259 | return df 260 | 261 | def load_from_Javad(filename : str ) : 262 | """ 263 | Summary : 264 | Load the data from a Javad binary file. 265 | 266 | Arguments : 267 | filename - pathname of the file to be loaded 268 | """ 269 | 270 | # Open the input file 271 | fid = open(filename, "rb") 272 | 273 | # Dictionary with the parsed information 274 | data = { "TOW" : [], 275 | "WNc [w]" : [], 276 | "SVID": [], 277 | "CRCPassed" : [], 278 | "ViterbiCnt" : [], 279 | "signalType" : [], 280 | "word 1": [], 281 | "word 2": [], 282 | "word 3": [], 283 | "word 4": [], 284 | "word 5": [], 285 | "word 6": [], 286 | "word 7": [], 287 | "word 8": [], 288 | "word 9": [], 289 | "word 10": [], 290 | "word 11": [], 291 | "word 12": [], 292 | "word 13": [], 293 | "word 14": [], 294 | "word 15": [], 295 | "word 16": [] 296 | } 297 | 298 | WN = 0 299 | 300 | # read the file line by line 301 | while True : 302 | 303 | line = fid.readline() 304 | if not line : 305 | break 306 | 307 | 308 | if len(line) < 9 : 309 | continue 310 | 311 | try : 312 | header = str(line[:5], 'utf-8') 313 | except : 314 | continue 315 | 316 | # Message containing the data 317 | # This message is used to determine the GPS week 318 | if header == 'RD006' : 319 | year = line[5] + 256 * line[6] 320 | month = line[7] 321 | day = line[8] 322 | 323 | # compute the GPS week 324 | _, WN = tf.DateToGPS(year, month, day, 0) 325 | 326 | continue 327 | 328 | # Message with the E6B navigation bits after Viterbi decoding 329 | if header[:2] == 'ED' : 330 | 331 | # Check if the signal type is correct 332 | # and the message length is as expected 333 | # signal (sig = 6 refers to Galileo E6B) 334 | if (line[10] != 6) or (line[11] != 62 ): 335 | continue 336 | 337 | # Check if the line is too short or broken on two lines 338 | # This is just a work around 339 | while len(line) < 76 : 340 | line1 = fid.readline() 341 | line = line + line1 342 | 343 | # prn 344 | prn = line[5] 345 | data["SVID"].append(prn) 346 | 347 | # Time of receiving message 348 | ToW = int.from_bytes( line[6:10], 'little') 349 | data["TOW"].append(ToW) 350 | data["WNc [w]"].append(WN) 351 | 352 | # Other data value set to default values 353 | data["CRCPassed"].append(True) 354 | data["ViterbiCnt"].append(0) 355 | data["signalType"].append(19) 356 | 357 | # save the 16 words (16 x 32 bits = 512) 358 | for ii in range(15) : 359 | word = int.from_bytes( line[(12 + ii*4):(12 + (ii + 1)*4)], 'big' ) 360 | data[f"word {ii + 1}"].append(word) 361 | 362 | # last word 363 | # 62 is nav message length in bytes 364 | word = int.from_bytes( line[(12 + 15*4):(12 + 62)], 'big' ) << 16 365 | data["word 16"].append(word) 366 | 367 | # close the inpu file 368 | fid.close() 369 | 370 | # create the output dataframe 371 | df = pd.DataFrame(data=data) 372 | 373 | return df 374 | 375 | def load_from_TopCon(filename : str ) : 376 | """ 377 | Summary : 378 | Load the data from a Topcon binary file. 379 | Very similar to the Javad case 380 | 381 | Arguments : 382 | filename - pathname of the file to be loaded 383 | """ 384 | # Open the input file 385 | fid = open(filename, "rb") 386 | 387 | # Dictionary with the parsed information 388 | data = { "TOW" : [], 389 | "WNc [w]" : [], 390 | "SVID": [], 391 | "CRCPassed" : [], 392 | "ViterbiCnt" : [], 393 | "signalType" : [], 394 | "word 1": [], 395 | "word 2": [], 396 | "word 3": [], 397 | "word 4": [], 398 | "word 5": [], 399 | "word 6": [], 400 | "word 7": [], 401 | "word 8": [], 402 | "word 9": [], 403 | "word 10": [], 404 | "word 11": [], 405 | "word 12": [], 406 | "word 13": [], 407 | "word 14": [], 408 | "word 15": [], 409 | "word 16": [] 410 | } 411 | 412 | WN = 0 413 | ToW = 0 414 | year = 0 415 | month = 0 416 | day = 0 417 | daysec = 0 418 | 419 | # read the file line by line 420 | while True : 421 | 422 | line = fid.readline() 423 | if not line : 424 | break 425 | 426 | 427 | if len(line) < 9 : 428 | continue 429 | 430 | try : 431 | header = str(line[:5], 'utf-8') 432 | except : 433 | continue 434 | 435 | # Message containing the data 436 | # This message is used to determine the GPS week 437 | if header == 'RD006' : 438 | year = line[5] + 256 * line[6] 439 | month = line[7] 440 | day = line[8] 441 | 442 | hour = daysec / 3600.0 443 | 444 | # compute the GPS week 445 | ToW, WN = tf.DateToGPS(year, month, day, hour) 446 | 447 | continue 448 | 449 | # Message providing the seconds of the day 450 | if header == '~~005' : 451 | 452 | # seconds of the day 453 | daysec = int.from_bytes( line[5:9], 'little') / 1000.0 454 | 455 | # compute the GPS week 456 | if year != 0 : 457 | hour = daysec / 3600.0 458 | ToW, WN = tf.DateToGPS(year, month, day, hour) 459 | 460 | continue 461 | 462 | if header[:2] == 'MD' : 463 | 464 | # prn 465 | prn = line[6] 466 | data["SVID"].append(prn) 467 | 468 | # Time of receiving message 469 | data["TOW"].append(ToW) 470 | data["WNc [w]"].append(WN) 471 | 472 | # Other data value set to default values 473 | data["CRCPassed"].append(True) 474 | data["ViterbiCnt"].append(0) 475 | data["signalType"].append(19) 476 | 477 | # save the 16 words (16 x 32 bits = 512) 478 | for ii in range(16) : 479 | word = int.from_bytes( line[(8 + ii*4):(8 + (ii + 1)*4)], 'little' ) 480 | data[f"word {ii + 1}"].append(word) 481 | 482 | # close the inpu file 483 | fid.close() 484 | 485 | # create the output dataframe 486 | df = pd.DataFrame(data=data) 487 | 488 | return df 489 | 490 | def load_from_Novatel(filename : str ) : 491 | """ 492 | Summary : 493 | Load the data from a Novatel data file. 494 | 495 | Arguments : 496 | filename - pathname of the file to be loaded 497 | """ 498 | # Open the input file 499 | fid = open(filename, "rb") 500 | 501 | # Dictionary with the parsed information 502 | data = { "TOW" : [], 503 | "WNc [w]" : [], 504 | "SVID": [], 505 | "CRCPassed" : [], 506 | "ViterbiCnt" : [], 507 | "signalType" : [], 508 | "word 1": [], 509 | "word 2": [], 510 | "word 3": [], 511 | "word 4": [], 512 | "word 5": [], 513 | "word 6": [], 514 | "word 7": [], 515 | "word 8": [], 516 | "word 9": [], 517 | "word 10": [], 518 | "word 11": [], 519 | "word 12": [], 520 | "word 13": [], 521 | "word 14": [], 522 | "word 15": [], 523 | "word 16": [] 524 | } 525 | 526 | WN = 0 527 | 528 | # read the file line by line 529 | while True : 530 | 531 | line = fid.readline() 532 | if not line : 533 | break 534 | 535 | if len(line) < 16 : 536 | continue 537 | 538 | # Try to convert the string in a sequence of characters 539 | try : 540 | str_line = str(line, 'utf-8') 541 | except : 542 | continue 543 | 544 | # Check if str_line contains 'GALCNAVRAWPAGE' 545 | if (str_line.find("GALCNAVRAWPAGE") > -1) : 546 | 547 | # This is valid message 548 | 549 | # Get the message time 550 | if (str_line.find("SATTIME") > -1) : 551 | split_line = str_line.split() 552 | time_ind = split_line.index('SATTIME') 553 | WN = int(split_line[time_ind + 1]) 554 | ToW = float(split_line[time_ind + 2]) + 1 555 | else : 556 | continue 557 | 558 | # Now read the next line that contains the PRN info and the payload 559 | line = fid.readline() 560 | 561 | # If this generates an error, it means there is a problem with the 562 | # data stream 563 | str_line = str(line, 'utf-8') 564 | 565 | # Split the data 566 | split_line = str_line.split() 567 | if len(split_line) < 3 : 568 | continue 569 | 570 | prn = int(split_line[2]) 571 | 572 | payload = split_line[-1] 573 | 574 | # Write the extracted information to the dictionary 575 | data["SVID"].append(prn) 576 | data["TOW"].append(ToW) 577 | data["WNc [w]"].append(WN) 578 | 579 | # Other data value set to default values 580 | data["CRCPassed"].append(True) 581 | data["ViterbiCnt"].append(0) 582 | data["signalType"].append(19) 583 | 584 | # Now extract the different words 585 | for ii in range(14) : 586 | word = int(payload[(8*ii):(8*ii + 8)], 16) 587 | data[f"word {ii + 1}"].append(word) 588 | 589 | # last two words 590 | word = int( payload[112:], 16 ) << 16 591 | data["word 15"].append(word) 592 | data["word 16"].append(0) 593 | 594 | # close the inpu file 595 | fid.close() 596 | 597 | # create the output dataframe 598 | df = pd.DataFrame(data=data) 599 | 600 | return df -------------------------------------------------------------------------------- /has_corrections.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sun Jun 20 11:38:28 2021 5 | 6 | @author: daniele 7 | """ 8 | 9 | import abc 10 | import numpy as np 11 | 12 | 13 | def get_bits(body, byte_offset, bit_offset, num_bits) : 14 | """ 15 | Summary: 16 | Function that extracts a sequence of bits from a stream of elements 17 | of GF(2^8) 18 | 19 | Arguments: 20 | body - stream of GF(2^8) elements 21 | byte_offset - the byte offset in the stream 22 | bit_offset - the bit offset in the stream 23 | num_bits - number of bits to extract 24 | 25 | Returns: 26 | val - the return value 27 | byte_offset - the new byte offset 28 | bit_offset - the new bit offset 29 | """ 30 | 31 | # first understand the return type 32 | if num_bits < 9 : 33 | val = np.uint8(0) 34 | elif num_bits > 8 and num_bits < 17 : 35 | val = np.uint16(0) 36 | elif num_bits > 16 and num_bits < 33 : 37 | val = np.uint32(0) 38 | elif num_bits > 32 and num_bits < 65 : 39 | val = np.uint64(0) 40 | else : 41 | # not supported type 42 | return None 43 | 44 | # bit masks 45 | bit_masks = [0x0, 0x1, 0x3, 0x7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF] 46 | 47 | # number of bits in current byte 48 | rem_bits = int(num_bits) 49 | 50 | while rem_bits != 0 : 51 | # Number of bits in the current byte 52 | bits_in_byte = int(min([rem_bits, 8 - bit_offset])) 53 | 54 | # extract the new bits 55 | new_bits = np.uint8(body[byte_offset]) >> (8 - bit_offset - bits_in_byte) 56 | new_bits &= bit_masks[bits_in_byte] 57 | 58 | # add them to the return value 59 | val <<= type(val)(bits_in_byte) 60 | val += type(val)(new_bits) 61 | 62 | # New bit offset 63 | bit_offset = (bit_offset + bits_in_byte) % 8 64 | 65 | # New byte offset 66 | if bit_offset == 0 : 67 | byte_offset += 1 68 | 69 | rem_bits -= bits_in_byte 70 | 71 | return val, byte_offset, bit_offset 72 | 73 | def two_complement( val, nbits) : 74 | """ 75 | Summary : 76 | Interpret an unsigned value (val) as a two's complement number. 77 | 78 | Arguments : 79 | val - the value to be interpreted as two's complement 80 | nbits - number of bits to consider for the evaluation of the complement. 81 | 82 | Returns: 83 | retval - the two's complement of val. 84 | """ 85 | 86 | # if it is a negative number 87 | if (val >> type(val)(nbits - 1)) & 0x1 == 1 : 88 | retval = val - 2**nbits 89 | else: 90 | retval = val 91 | 92 | return retval 93 | 94 | ############################################################################### 95 | class has_mask : 96 | """ 97 | System and signal mask 98 | """ 99 | gal_signals = ["E1-B", "E1-C", "E1-B+E1-C", "E5a-I", "E5a-Q", "E5a-I+E5a-Q",\ 100 | "E5b-I", "E5b-Q", "E5b-I+E5b-Q","E5-I", "E5-Q", "E5-I+E5-Q",\ 101 | "E6-B", "E6-C", "E6-B+E6-C", "Reserved"] 102 | 103 | gps_signals = ["L1 C/A", "Reserved", "Reserved", "L1C(D)", "L1C(P)", "L1C(D+P)", \ 104 | "L2C(M)", "L2C(L)", "L2C(M+L)", "L2P", "Reserved", "L5-I", "L5-Q", \ 105 | "L5-I + L5-Q", "Reserved", "Reserved"] 106 | 107 | def __init__(self) : 108 | # The GNSS ID 109 | # Two values are supported 110 | # 0 - GPS 111 | # 1 - Reserved 112 | # 2 - Galileo 113 | # 3-15 - Reserved 114 | 115 | self.gnss_ID = np.uint8(0) 116 | 117 | # Satellites corrected 118 | # list of PRNs indicating which satellites are corrected 119 | # To be extracted from the satellite mask (40 bits) 120 | self.prns = [] 121 | 122 | # Signals for which corrections are available 123 | # To be extracted from the signal mask (16 bits) 124 | self.signals = [] 125 | 126 | # Cell mask 127 | self.cell_mask_flag = 0 128 | self.cell_mask = [] 129 | 130 | # Nav message 131 | # 0 - I/NAV for GAL or LNAV for GPS 132 | # 1-7 - Reserved 133 | self.nav_message = np.uint8(0) 134 | 135 | def interpret_mask(self, body, byte_offset, bit_offset) : 136 | 137 | # get the GNSS ID 138 | self.gnss_ID, byte_offset, bit_offset = get_bits( body, byte_offset, bit_offset, 4 ) 139 | 140 | # Satellite mask 141 | satmask, byte_offset, bit_offset = get_bits( body, byte_offset, bit_offset, 40 ) 142 | 143 | # Convert it into a list of PRNs 144 | satmask = format(satmask, '040b') 145 | for jj in range(len(satmask)) : 146 | if satmask[jj] == '1' : 147 | self.prns.append(jj + 1) 148 | 149 | # Signal mask 150 | sigmask, byte_offset, bit_offset = get_bits( body, byte_offset, bit_offset, 16 ) 151 | 152 | # Convert it into a list of signals 153 | sigmask = format(sigmask, '016b') 154 | for jj in range(len(sigmask)) : 155 | if sigmask[jj] == '1' : 156 | self.signals.append(jj) 157 | 158 | # Cell mask flag 159 | self.cell_mask_flag, byte_offset, bit_offset = get_bits( body, byte_offset, bit_offset, 1 ) 160 | 161 | # Extract the cell mask if available 162 | if self.cell_mask_flag == 1 : 163 | Nsat = len(self.prns) # Number of satellites 164 | Nsig = len(self.signals) # Number of signals per satellite 165 | 166 | # For each satellite record the signal mask 167 | for ii in range(Nsat) : 168 | cell_mask, byte_offset, bit_offset = get_bits( body, byte_offset, bit_offset, Nsig ) 169 | self.cell_mask.append(cell_mask.copy()) 170 | 171 | # get the nav message corrected in the orbits 172 | self.nav_message, byte_offset, bit_offset = get_bits( body, byte_offset, bit_offset, 3 ) 173 | 174 | return byte_offset, bit_offset 175 | 176 | ############################################################################### 177 | class has_correction(metaclass = abc.ABCMeta) : 178 | """ 179 | Summary : 180 | Parent class defining the basic properties of a HAS correction. 181 | This is an abstract class, which defines basic interfaces 182 | """ 183 | def __init__(self, gnss_ID, prn, validity, info : dict = None) : 184 | """ 185 | Summary : 186 | Object constructor. 187 | 188 | Arguments : 189 | gnss_ID - identifier defining the GNSS of the correction 190 | prn - satellite identifier 191 | validity - validity of the correction in seconds 192 | info - dictionary with the following information 193 | 194 | ToW - time of week (extracted from the navigation message) in seconds 195 | ToH - time of hour extracted from the HAS header in seconds 196 | IOD - Issue of Data extracted from the HAS header 197 | WN - the week number 198 | 199 | Returns: 200 | The correction object. 201 | """ 202 | # GNSS ID 203 | self.gnss_ID = gnss_ID 204 | 205 | # Satellite PRN 206 | self.prn = prn 207 | 208 | # Validity interval 209 | self.validity = validity 210 | 211 | if info is None : 212 | info = {'ToW': -1, 213 | 'WN' : -1, 214 | 'ToH' : -1, 215 | 'IOD' : -1} 216 | 217 | # time of week 218 | self.tow = info['ToW'] 219 | 220 | # time of hour 221 | self.toh = info['ToH'] 222 | 223 | # issue of data 224 | self.IOD = info['IOD'] 225 | 226 | # week number 227 | self.wn = info['WN'] 228 | 229 | # GNSS IOD - This is a system related parameter and should not be confused 230 | # with the correction IOD 231 | self.gnss_IOD = -1 232 | 233 | @abc.abstractmethod 234 | def interpret(self, body, byte_offset, bit_offset) : 235 | """ 236 | Summary : 237 | Abstract method defining how to interpret the body of the has message 238 | and use it to fill the different parts of the correction. 239 | 240 | Arguments : 241 | body - array of bytes 242 | byte_offset - the byte offset in body 243 | bit_offset - the bit offset in body 244 | 245 | Returns: 246 | byte_offset - the new byte offset in body 247 | bit_offset - the new bit offset in body 248 | """ 249 | pass 250 | 251 | def __str__(self) : 252 | """ 253 | Summary : 254 | Build a string representation of the object. 255 | 256 | Arguments : 257 | None. The object its self. 258 | 259 | Returns: 260 | String representing the content of the object 261 | """ 262 | out_str = str(self.tow) + ',' + str(self.wn) + ',' + str(self.toh)\ 263 | + ',' + str(self.IOD) + ',' + str(self.gnss_IOD) + ','\ 264 | + str(self.validity) + ','\ 265 | + str(self.gnss_ID) + ',' + str(self.prn) 266 | 267 | return out_str 268 | 269 | def get_header(self) : 270 | """ 271 | Summary : 272 | Build the string describing the attributes in the class __str__() 273 | method. 274 | 275 | Arguments : 276 | None. The object its self. 277 | 278 | Returns: 279 | String representing the attributes of __str__() 280 | """ 281 | return "ToW,WN,ToH,IOD,gnssIOD,validity,gnssID,PRN" 282 | 283 | class has_orbit_correction(has_correction) : 284 | """ 285 | HAS orbit corrections 286 | """ 287 | 288 | def __init__(self, gnss_ID, prn, validity, info : dict) : 289 | """ 290 | Summary : 291 | Object constructor for the orbit correction 292 | 293 | Arguments : 294 | gnss_ID - identifier defining the GNSS of the correction 295 | prn - satellite identifier 296 | validity - validity of the correction in seconds 297 | info - dictionary with time information 298 | 299 | Returns: 300 | The correction object. 301 | """ 302 | 303 | # Initialize the parent class 304 | super().__init__(gnss_ID, prn, validity, info) 305 | 306 | # Radial correction 307 | self.delta_radial = 0 308 | 309 | # In-track correction 310 | self.delta_in_track = 0 311 | 312 | # Delta Cross-Track 313 | self.delta_cross_track = 0 314 | 315 | def interpret(self, body, byte_offset, bit_offset ) : 316 | """ 317 | Summary : 318 | Actual implementation of the method defining how to interpret the 319 | body of the has message and use it to fill the different parts of 320 | the correction. 321 | 322 | Arguments : 323 | body - array of bytes 324 | byte_offset - the byte offset in body 325 | bit_offset - the bit offset in body 326 | 327 | Returns: 328 | byte_offset - the new byte offset in body 329 | bit_offset - the new bit offset in body 330 | """ 331 | 332 | # get the GNSS IOD - the related number of bits depends on the type of GNSS 333 | if self.gnss_ID == 0 : 334 | # GPS 335 | num_bits = 8 336 | 337 | elif self.gnss_ID == 2 : 338 | # Galileo 339 | num_bits = 10 340 | else : 341 | raise Exception("Unsupported GNSS") 342 | 343 | # GNSS IOD 344 | self.gnss_IOD, byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, num_bits) 345 | 346 | # Delta Radial 347 | # "b1000000000000" indicates data not available. 348 | delta, byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, 13) 349 | 350 | if delta == 4096 : 351 | self.delta_radial = np.nan 352 | else : 353 | self.delta_radial = two_complement(delta, 13) * 0.0025 354 | 355 | # Delta In-Track 356 | # "b100000000000" indicates data not available. 357 | delta, byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, 12) 358 | 359 | if delta == 2048 : 360 | self.delta_in_track = np.nan 361 | else : 362 | self.delta_in_track = two_complement(delta, 12) * 0.008 363 | 364 | # Delta Cross-Track 365 | # "b100000000000" indicates data not available. 366 | delta, byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, 12) 367 | 368 | if delta == 2048 : 369 | self.delta_cross_track = np.nan 370 | else : 371 | self.delta_cross_track = two_complement(delta, 12) * 0.008 372 | 373 | return byte_offset, bit_offset 374 | 375 | def __str__(self) : 376 | """ 377 | Summary : 378 | Build a string representation of the object. 379 | 380 | Arguments : 381 | None. The object its self. 382 | 383 | Returns: 384 | String representing the content of the object 385 | """ 386 | out_str = super().__str__() + ',' + \ 387 | str(self.delta_radial) + ',' + str(self.delta_in_track) + \ 388 | ',' + str(self.delta_cross_track) 389 | 390 | return out_str 391 | 392 | def get_header(self) : 393 | """ 394 | Summary : 395 | Build the string describing the attributes in the class __str__() 396 | method. 397 | 398 | Arguments : 399 | None. The object its self. 400 | 401 | Returns: 402 | String representing the attributes of __str__() 403 | """ 404 | out_str = super().get_header() + ',delta_radial,' +\ 405 | 'delta_in_track,delta_cross_track' 406 | 407 | return out_str 408 | 409 | class has_clock_corr(has_correction) : 410 | """ 411 | Clock orbit corrections 412 | """ 413 | def __init__(self, gnss_ID, prn, validity, sys_mul, info : dict) : 414 | """ 415 | Summary : 416 | Object constructor for the clock correction 417 | 418 | Arguments : 419 | gnss_ID - identifier defining the GNSS of the correction 420 | prn - satellite identifier 421 | validity - validity of the correction in seconds 422 | sys_mul - multiplier at the GNSS level for the clock C0 correction 423 | info - dictionary with time information 424 | 425 | Returns: 426 | The correction object. 427 | """ 428 | # Initialize the parent class 429 | super().__init__(gnss_ID, prn, validity, info) 430 | 431 | # Multiplier for all Delta Clock C0 corrections 432 | self.multiplier = sys_mul 433 | 434 | # Delta Clock C0 435 | self.delta_clock_c0 = 0 436 | 437 | # status 0 - OK, 1 - not available, 2 - shall not be used 438 | self.status = 0 439 | 440 | def interpret(self, body, byte_offset, bit_offset) : 441 | """ 442 | Summary : 443 | Actual implementation of the method defining how to interpret the 444 | body of the has message and use it to fill the different parts of 445 | the correction. 446 | 447 | Arguments : 448 | body - array of bytes 449 | byte_offset - the byte offset in body 450 | bit_offset - the bit offset in body 451 | 452 | Returns: 453 | byte_offset - the new byte offset in body 454 | bit_offset - the new bit offset in body 455 | """ 456 | # Delta Clock C0 457 | # "b1000000000000" indicates data not available 458 | # "b0111111111111" indicates the satellite shall not be used. 459 | delta, byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, 13) 460 | 461 | if delta == 4096 : 462 | self.status = 1 463 | elif delta == 4095 : 464 | self.status = 2 465 | else : 466 | self.delta_clock_c0 = two_complement(delta, 13) * 0.0025 467 | 468 | return byte_offset, bit_offset 469 | 470 | def __str__(self) : 471 | """ 472 | Summary : 473 | Build a string representation of the object. 474 | 475 | Arguments : 476 | None. The object its self. 477 | 478 | Returns: 479 | String representing the content of the object 480 | """ 481 | out_str = super().__str__() + ',' + str(self.multiplier) + ',' + \ 482 | str(self.delta_clock_c0) + ',' + str(self.status) 483 | 484 | return out_str 485 | 486 | def get_header(self) : 487 | """ 488 | Summary : 489 | Build the string describing the attributes in the class __str__() 490 | method. 491 | 492 | Arguments : 493 | None. The object its self. 494 | 495 | Returns: 496 | String representing the attributes of __str__() 497 | """ 498 | out_str = super().get_header() + ',multiplier,delta_clock_c0,' + \ 499 | 'status' 500 | 501 | return out_str 502 | 503 | class has_code_bias(has_correction) : 504 | """ 505 | HAS code bias corrections. 506 | """ 507 | def __init__(self, gnss_ID, prn, validity, signals, info : dict) : 508 | """ 509 | Summary : 510 | Object constructor for the clock correction 511 | 512 | Arguments : 513 | gnss_ID - identifier defining the GNSS of the correction 514 | prn - satellite identifier 515 | validity - validity of the correction in seconds 516 | signals - list of signals for which code biases are available 517 | info - dictionary with time information 518 | 519 | Returns: 520 | The correction object. 521 | """ 522 | 523 | # Initialize the parent class 524 | super().__init__(gnss_ID, prn, validity, info) 525 | 526 | # List of supported signals 527 | self.signals = signals 528 | 529 | # Code biases 530 | self.biases = np.zeros(len(signals)) 531 | 532 | # Availability flags related to the code biases 533 | self.availability_flags = np.ones(len(signals)) 534 | 535 | def interpret(self, body, byte_offset, bit_offset) : 536 | """ 537 | Summary : 538 | Actual implementation of the method defining how to interpret the 539 | body of the has message and use it to fill the different parts of 540 | the correction. 541 | 542 | Arguments : 543 | body - array of bytes 544 | byte_offset - the byte offset in body 545 | bit_offset - the bit offset in body 546 | 547 | Returns: 548 | byte_offset - the new byte offset in body 549 | bit_offset - the new bit offset in body 550 | """ 551 | 552 | for ii in range(len(self.signals)) : 553 | 554 | bias, byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, 11) 555 | 556 | if bias == 1024 : 557 | self.availability_flags[ii] = 0 558 | else : 559 | self.biases[ii] = two_complement(bias, 11) * 0.02 560 | 561 | return byte_offset, bit_offset 562 | 563 | def is_empty(self) : 564 | """ 565 | Summary: 566 | Check if the correction contains actual data or if it is empty. 567 | 568 | Arguments: 569 | None. 570 | 571 | Returns: 572 | True if there are no corrections available. 573 | """ 574 | if len(self.signals) == 0 : 575 | return True 576 | 577 | if len(self.biases) == 0 : 578 | return True 579 | 580 | return False 581 | 582 | def __str__(self) : 583 | """ 584 | Summary : 585 | Build a string representation of the object. 586 | 587 | Arguments : 588 | None. The object its self. 589 | 590 | Returns: 591 | String representing the content of the object 592 | """ 593 | out_str = "" 594 | 595 | if self.is_empty() : 596 | return out_str 597 | 598 | for ii in range(len(self.signals) - 1) : 599 | 600 | sig_str = super().__str__() + ',' + str(self.signals[ii]) + \ 601 | ',' + str(self.biases[ii]) + ',' + \ 602 | str(self.availability_flags[ii]) + '\n' 603 | 604 | out_str = out_str + sig_str 605 | 606 | # Add the last one with return carriage 607 | sig_str = super().__str__() + ',' + str(self.signals[-1]) + \ 608 | ',' + str(self.biases[-1]) + ',' + str(self.availability_flags[-1]) 609 | 610 | out_str = out_str + sig_str 611 | 612 | return out_str 613 | 614 | def get_header(self) : 615 | """ 616 | Summary : 617 | Build the string describing the attributes in the class __str__() 618 | method. 619 | 620 | Arguments : 621 | None. The object its self. 622 | 623 | Returns: 624 | String representing the attributes of __str__() 625 | """ 626 | out_str = super().get_header() + ',signal,code_bias,av_flag' 627 | 628 | return out_str 629 | 630 | class has_phase_bias(has_correction) : 631 | """ 632 | HAS carrier phase corrections. 633 | """ 634 | def __init__(self, gnss_ID, prn, validity, signals, info : dict) : 635 | """ 636 | Summary : 637 | Object constructor for the clock correction 638 | 639 | Arguments : 640 | gnss_ID - identifier defining the GNSS of the correction 641 | prn - satellite identifier 642 | validity - validity of the correction in seconds 643 | signals - list of signals for which carrier phase biases are available 644 | info - dictionary with time information 645 | 646 | Returns: 647 | The correction object. 648 | """ 649 | # Initialize the parent class 650 | super().__init__(gnss_ID, prn, validity, info) 651 | 652 | # List of supported signals 653 | self.signals = signals 654 | 655 | # List of carrier phase biases 656 | self.biases = np.zeros(len(signals)) 657 | 658 | # Phase discontinuity indexes 659 | self.phase_discontinuity_inds = np.zeros(len(signals)) 660 | 661 | # Availability flags related to the carrier phase biases 662 | self.availability_flags = np.ones(len(signals)) 663 | 664 | def interpret(self, body, byte_offset, bit_offset) : 665 | """ 666 | Summary : 667 | Actual implementation of the method defining how to interpret the 668 | body of the has message and use it to fill the different parts of 669 | the correction. 670 | 671 | Arguments : 672 | body - array of bytes 673 | byte_offset - the byte offset in body 674 | bit_offset - the bit offset in body 675 | 676 | Returns: 677 | byte_offset - the new byte offset in body 678 | bit_offset - the new bit offset in body 679 | """ 680 | for ii in range(len(self.signals)) : 681 | bias, byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, 11) 682 | 683 | if bias == 1024 : 684 | self.availability_flags[ii] = 0 685 | else : 686 | self.biases[ii] = two_complement(bias, 11) * 0.01 687 | 688 | self.phase_discontinuity_inds[ii], byte_offset, bit_offset = get_bits(body, byte_offset, bit_offset, 2) 689 | 690 | return byte_offset, bit_offset 691 | 692 | def is_empty(self) : 693 | """ 694 | Summary: 695 | Check if the correction contains actual data or if it is empty. 696 | 697 | Arguments: 698 | None. 699 | 700 | Returns: 701 | True if there are no corrections available. 702 | """ 703 | if len(self.signals) == 0 : 704 | return True 705 | 706 | if len(self.biases) == 0 : 707 | return True 708 | 709 | return False 710 | 711 | def __str__(self) : 712 | """ 713 | Summary : 714 | Build a string representation of the object. 715 | 716 | Arguments : 717 | None. The object its self. 718 | 719 | Returns: 720 | String representing the content of the object 721 | """ 722 | out_str = "" 723 | 724 | if self.is_empty() : 725 | return "" 726 | 727 | for ii in range(len(self.signals) - 1) : 728 | 729 | sig_str = super().__str__() + ',' + str(self.signals[ii]) + \ 730 | ',' + str(self.biases[ii]) + ',' + \ 731 | str(self.availability_flags[ii]) + ',' + \ 732 | str(self.phase_discontinuity_inds[ii]) + '\n' 733 | 734 | out_str = out_str + sig_str 735 | 736 | # Add the last one with return carriage 737 | sig_str = super().__str__() + ',' + str(self.signals[-1]) + \ 738 | ',' + str(self.biases[-1]) + ',' + str(self.availability_flags[-1]) + \ 739 | ',' + str(self.phase_discontinuity_inds[-1]) 740 | 741 | out_str = out_str + sig_str 742 | 743 | return out_str 744 | 745 | def get_header(self) : 746 | """ 747 | Summary : 748 | Build the string describing the attributes in the class __str__() 749 | method. 750 | 751 | Arguments : 752 | None. The object its self. 753 | 754 | Returns: 755 | String representing the attributes of __str__() 756 | """ 757 | out_str = super().get_header() + ',signal,phase_bias,av_flag,phase_discontinuity_ind' 758 | 759 | return out_str -------------------------------------------------------------------------------- /has_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sun Jun 13 08:54:47 2021 5 | 6 | @author: daniele 7 | """ 8 | 9 | import has_message as hm 10 | import has_corrections as hc 11 | import numpy as np 12 | 13 | class has_decoder : 14 | """ 15 | Summary : 16 | Class implementing the decoder for the HAS message 17 | """ 18 | 19 | # static variable 20 | LIMIT_AGE = 120 21 | 22 | # validity intervals as specified by Table 13 of the ICD 23 | validity_t13 = [5, 10, 15, 20, 30, 60, 90, 120, 180, 240, 300, 600, 900, 1800, 3600, -1] 24 | 25 | def __init__(self, ind_offset = 1) : 26 | """ 27 | Summary : 28 | Object constructor. 29 | 30 | Arguments: 31 | ind_offset - index used to take into account potential offsets in the 32 | page indexing 33 | Returns: 34 | """ 35 | # list of HAS messages 36 | self.message_list =[] 37 | 38 | # page index offset 39 | self.ind_offset = ind_offset 40 | 41 | # table with the GNSS IOD for the different satellites 42 | self.gnss_IODs = {} 43 | 44 | def update(self, tow, sep_pages, msg_type, msg_id, msg_size) : 45 | """ 46 | Summary : 47 | Update the decoder using blocks of pages in the format provided by the 48 | Septentrio receiver. All the pages are from the same epoch. 49 | 50 | Arguments: 51 | tow - time of week 52 | sep_page - block of pages with the same TOW. Each page is an array 53 | with 16 32-bit integers representing the HAS message 54 | msg_type - message type 55 | msg_id - messge id 56 | msg_size - message size 57 | 58 | Returns: 59 | Nothing. 60 | """ 61 | has_message = False 62 | decoded_msg = None 63 | 64 | for message in self.message_list : 65 | 66 | # check if this is the correct message 67 | if message.is_message(msg_type, msg_id, msg_size) : 68 | has_message = True 69 | 70 | # update the message 71 | for page in sep_pages : 72 | message.add_page_sep_bytes(page) 73 | 74 | # This is not the correct message 75 | else : 76 | # increase the age of the message 77 | message.increase_age() 78 | 79 | # if the message was not present add it to the list 80 | if not has_message : 81 | message = hm.has_message(msg_type, msg_id, msg_size, self.ind_offset) 82 | 83 | # update the message 84 | for page in sep_pages : 85 | message.add_page_sep_bytes(page) 86 | 87 | # add it to the decoder list 88 | self.message_list.append(message) 89 | 90 | # Do some clean up 91 | for message in self.message_list : 92 | 93 | decoded_msg = None 94 | 95 | # if the message is old, remove it 96 | if message.is_old(self.LIMIT_AGE) : 97 | self.message_list.remove(message) 98 | 99 | # if the message is complete, decode it and remove it from the list 100 | elif message.complete() : 101 | decoded_msg = message.decode() 102 | self.message_list.remove(message) 103 | 104 | return decoded_msg 105 | 106 | def interpret_mt1_header( self, header ) : 107 | """ 108 | Summary: 109 | Interpret the header (32 bits) of a MT1 message 110 | 111 | Arguments: 112 | header - 4 bytes of the message header 113 | 114 | Returns: 115 | header_content - dictionary with the decoded header 116 | """ 117 | # Time of Hour 118 | ToH = (int(header[0]) << 4) + (int(header[1] ) >> 4) 119 | 120 | # Mask ID 121 | maskID = ((int(header[2]) & 0x3) << 3) + (int(header[3] ) >> 5) 122 | 123 | # IOD Set ID 124 | iodID = int(header[3]) & 0x1F 125 | 126 | header_content = {"TOH" : ToH, 127 | "Mask" : ( int(header[1] ) >> 3 ) & 0x1, 128 | "Orbit Corr" : ( int(header[1] ) >> 2 ) & 0x1, 129 | "Clock Full-set" : ( int(header[1] ) >> 1 ) & 0x1, 130 | "Clock Subset" : int(header[1]) & 0x1, 131 | "Code Bias" : ( int(header[2] ) >> 7 ) & 0x1, 132 | "Phase Bias" : ( int(header[2] ) >> 6 ) & 0x1, 133 | "Mask ID" : maskID, 134 | "IOD Set ID" : iodID 135 | } 136 | 137 | return header_content 138 | 139 | ############################################################################### 140 | 141 | def interpret_mt1_mask(self, body, byte_offset = 0, bit_offset = 0) : 142 | """ 143 | Summary: 144 | Interpret the body of a MT1 HAS message as a "Mask" 145 | 146 | Arguments: 147 | body - array of bytes 148 | 149 | Returns: 150 | masks - the interpreted satellite masks 151 | """ 152 | 153 | # Determine the number of system supported 154 | Nsys, byte_offset, bit_offset = hc.get_bits( body, byte_offset, bit_offset, 4 ) 155 | 156 | masks = [] 157 | 158 | # interpret the first mask 159 | for ii in range(Nsys) : 160 | 161 | # create a new mask 162 | mask = hc.has_mask() 163 | byte_offset, bit_offset = mask.interpret_mask(body, byte_offset, bit_offset) 164 | 165 | masks.append(mask) 166 | 167 | # Skip 6 bits - reserved field 168 | _, byte_offset, bit_offset = hc.get_bits( body, byte_offset, bit_offset, 6 ) 169 | 170 | return masks, byte_offset, bit_offset 171 | 172 | 173 | def interpret_mt1_orbit_corrections(self, body, byte_offset, bit_offset, masks, info = None) : 174 | """ 175 | Summary: 176 | Interpret the body of a MT1 HAS message as orbit corrections 177 | 178 | Arguments: 179 | body - array of bytes 180 | byte_offset - the byte offset in body 181 | bit_offset - the bit offset in body 182 | masks - list of mask indicating how to interpret the orbit corrections 183 | info - dictionary with timing information 184 | 185 | Returns: 186 | orbit corrections - list of orbit corrections 187 | byte_offset - the new byte offset in body 188 | bit_offset - the new bit offset in body 189 | """ 190 | 191 | orbit_corrections = [] 192 | 193 | # first get the validity index 194 | vi_index, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 4) 195 | 196 | # convert the the validity index in a validity interval (Table 13 of ICD) 197 | validity = has_decoder.validity_t13[vi_index] 198 | 199 | self.gnss_IODs = {} 200 | 201 | for mask in masks : 202 | # get the gnss ID 203 | gnss = mask.gnss_ID 204 | 205 | # for each satellite in the mask, get an orbit correction 206 | for prn in mask.prns : 207 | # creat the orbit correction 208 | orbit_cor = hc.has_orbit_correction(gnss, prn, validity, info) 209 | 210 | # interpret the message 211 | byte_offset, bit_offset = orbit_cor.interpret(body, byte_offset, bit_offset) 212 | 213 | # add the correction to the list 214 | orbit_corrections.append(orbit_cor) 215 | 216 | # HAS orbit corrections contain the GNSS IOD that is stored into the decoder. 217 | # This information will be used for the other correction types 218 | self.gnss_IODs[str(gnss) + '_' + str(prn)] = orbit_cor.gnss_IOD 219 | 220 | return orbit_corrections, byte_offset, bit_offset 221 | 222 | def interpret_mt1_full_clock_corrections(self, body, byte_offset, bit_offset, masks, info = None) : 223 | """ 224 | Summary: 225 | Interpret the body of a MT1 HAS message as clock full-set corrections 226 | 227 | Arguments: 228 | body - array of bytes 229 | byte_offset - the byte offset in body 230 | bit_offset - the bit offset in body 231 | masks - list of mask indicating how to interpret the orbit corrections 232 | info - dictionary with timing information 233 | 234 | Returns: 235 | full_clock_corr - list of clock corrections 236 | byte_offset - the new byte offset in body 237 | bit_offset - the new bit offset in body 238 | """ 239 | 240 | clock_cors = [] 241 | 242 | # first get the validity index 243 | vi_index, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 4) 244 | 245 | # convert the the validity index in a validity interval (Table 13 of ICD) 246 | validity = has_decoder.validity_t13[vi_index] 247 | 248 | # ...then get the system multiplier 249 | sys_mul = np.zeros(len(masks)) 250 | 251 | for ii in range(len(masks)) : 252 | sys_mul[ii], byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 2) 253 | 254 | sys_mul += 1 255 | 256 | # now get the clock corrections 257 | for ii, mask in enumerate(masks) : 258 | # get the gnss ID 259 | gnss = mask.gnss_ID 260 | 261 | # for each satellite in the mask, get a clock full-set correction 262 | for prn in mask.prns : 263 | # creat the orbit correction 264 | clock_cor = hc.has_clock_corr(gnss, prn, validity, sys_mul[ii], info) 265 | 266 | # interpret the message 267 | byte_offset, bit_offset = clock_cor.interpret(body, byte_offset, bit_offset) 268 | 269 | # if the GNSS IOD is available, set its value in the corrections 270 | str_ID = str(gnss) + '_' + str(prn) 271 | if str_ID in self.gnss_IODs : 272 | clock_cor.gnss_IOD = self.gnss_IODs[str_ID] 273 | 274 | # add the correction to the list 275 | clock_cors.append(clock_cor) 276 | 277 | return clock_cors, byte_offset, bit_offset 278 | 279 | def interpret_mt1_subset_clock_corrections(self, body, byte_offset, bit_offset, masks, info = None) : 280 | """ 281 | Summary: 282 | Interpret the body of a MT1 HAS message as clock full-set corrections 283 | 284 | Arguments: 285 | body - array of bytes 286 | byte_offset - the byte offset in body 287 | bit_offset - the bit offset in body 288 | masks - list of mask indicating how to interpret the orbit corrections 289 | info - dictionary with timing information 290 | 291 | Returns: 292 | full_clock_corr - list of clock corrections 293 | byte_offset - the new byte offset in body 294 | bit_offset - the new bit offset in body 295 | """ 296 | clock_cors = [] 297 | 298 | # first get the validity index 299 | vi_index, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 4) 300 | 301 | # convert the the validity index in a validity interval (Table 13 of ICD) 302 | validity = has_decoder.validity_t13[vi_index] 303 | 304 | # then get the actual number of GNSSs 305 | Nsys, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 4) 306 | 307 | # loop over the different GNSS 308 | for ii in range(Nsys) : 309 | 310 | # get the GNSS ID 311 | gnssID, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 4) 312 | 313 | # find the related mask 314 | for mask in masks : 315 | if mask.gnss_ID == gnssID : 316 | break 317 | 318 | signals = mask.prns 319 | Nsig = len(signals) 320 | 321 | # get the system multiplier 322 | sys_mul, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 2) 323 | sys_mul += 1 324 | 325 | # get the signal mask 326 | sig_mask, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, Nsig) 327 | sig_mask = bin(sig_mask)[2:].zfill(Nsig) 328 | 329 | # loop over the different signals 330 | for kk in range(len(signals)) : 331 | 332 | if sig_mask[kk] == '1' : 333 | 334 | # creat the orbit correction 335 | clock_cor = hc.has_clock_corr(gnssID, signals[kk], validity, sys_mul, info) 336 | 337 | # interpret the message 338 | byte_offset, bit_offset = clock_cor.interpret(body, byte_offset, bit_offset) 339 | 340 | # if the GNSS IOD is available, set its value in the corrections 341 | str_ID = str(gnssID) + '_' + str(signals[kk]) 342 | if str_ID in self.gnss_IODs : 343 | clock_cor.gnss_IOD = self.gnss_IODs[str_ID] 344 | 345 | # add the correction to the list 346 | clock_cors.append(clock_cor) 347 | # end loop on the different signals 348 | # end loop on the different GNSS 349 | return clock_cors, byte_offset, bit_offset 350 | 351 | def interpret_mt1_code_biases(self, body, byte_offset, bit_offset, masks, info = None ) : 352 | 353 | code_biases = [] 354 | 355 | # first get the validity index 356 | vi_index, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 4) 357 | 358 | # convert the the validity index in a validity interval (Table 13 of ICD) 359 | validity = has_decoder.validity_t13[vi_index] 360 | 361 | # loop over the different masks 362 | for mask in masks : 363 | gnss_id = mask.gnss_ID 364 | 365 | # loop over the differnt PRNs 366 | for ii, prn in enumerate(mask.prns) : 367 | 368 | # determine the number of biases/signals 369 | if mask.cell_mask_flag == 0 : 370 | signals = mask.signals 371 | else : 372 | signals = [] 373 | signal_mask = format(mask.cell_mask[ii], f"0{len(mask.signals)}b") 374 | 375 | for kk in range(len(mask.signals)) : 376 | if signal_mask[kk] == '1' : 377 | signals.append(mask.signals[kk]) 378 | 379 | # Now create the new bias 380 | cbias = hc.has_code_bias(gnss_id, prn, validity, signals, info) 381 | 382 | # ... and interpret the message 383 | byte_offset, bit_offset = cbias.interpret(body, byte_offset, bit_offset) 384 | 385 | # if the GNSS IOD is available, set its value in the corrections 386 | str_ID = str(gnss_id) + '_' + str(prn) 387 | if str_ID in self.gnss_IODs : 388 | cbias.gnss_IOD = self.gnss_IODs[str_ID] 389 | 390 | code_biases.append(cbias) 391 | # end loop on the prns 392 | #end loop on the GNSS 393 | 394 | return code_biases, byte_offset, bit_offset 395 | 396 | def interpret_mt1_phase_biases(self, body, byte_offset, bit_offset, masks, info = None) : 397 | 398 | phase_biases = [] 399 | 400 | # first get the validity index 401 | vi_index, byte_offset, bit_offset = hc.get_bits(body, byte_offset, bit_offset, 4) 402 | 403 | # convert the the validity index in a validity interval (Table 13 of ICD) 404 | validity = has_decoder.validity_t13[vi_index] 405 | 406 | # loop over the different masks 407 | for mask in masks : 408 | gnss_id = mask.gnss_ID 409 | 410 | # loop over the differnt PRNs 411 | for ii, prn in enumerate(mask.prns) : 412 | 413 | # determine the number of biases/signals 414 | if mask.cell_mask_flag == 0 : 415 | signals = mask.signals 416 | else : 417 | signals = [] 418 | signal_mask = format(mask.cell_mask[ii], f"0{len(mask.signals)}b") 419 | 420 | for kk in range(len(mask.signals)) : 421 | if signal_mask[kk] == '1' : 422 | signals.append(mask.signals[kk]) 423 | 424 | # Now create the new bias 425 | pbias = hc.has_phase_bias(gnss_id, prn, validity, signals, info) 426 | 427 | # ... and interpret the message 428 | byte_offset, bit_offset = pbias.interpret(body, byte_offset, bit_offset) 429 | 430 | # if the GNSS IOD is available, set its value in the corrections 431 | str_ID = str(gnss_id) + '_' + str(prn) 432 | if str_ID in self.gnss_IODs : 433 | pbias.gnss_IOD = self.gnss_IODs[str_ID] 434 | 435 | phase_biases.append(pbias) 436 | # end loop on the prns 437 | #end loop on the GNSS 438 | 439 | return phase_biases, byte_offset, bit_offset 440 | -------------------------------------------------------------------------------- /has_encoding_matrix.csv: -------------------------------------------------------------------------------- 1 | 1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 2 | 0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 3 | 0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 4 | 0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 5 | 0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 6 | 0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 7 | 0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 8 | 0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 9 | 0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 10 | 0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 11 | 0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 12 | 0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 13 | 0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 14 | 0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 15 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 16 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 17 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 18 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0 19 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0 20 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0 21 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0 22 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0 23 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0 24 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0 25 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0 26 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0 27 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0 28 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0 29 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0 30 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0 31 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0 32 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 33 | 19,143,180,59,221,29,49,45,231,9,73,73,159,2,158,136,212,218,14,113,215,20,187,55,137,181,203,113,97,135,14,251 34 | 27,27,1,50,255,109,251,156,148,151,85,21,74,116,250,77,60,203,113,196,213,23,202,125,31,252,90,1,176,226,44,252 35 | 98,153,190,38,223,28,28,149,170,219,41,235,236,175,113,182,239,31,74,241,127,121,207,137,205,70,88,218,250,129,99,5 36 | 95,235,199,105,168,182,233,133,201,135,171,89,50,230,115,227,21,122,41,226,93,59,20,36,30,150,134,240,34,91,183,83 37 | 172,171,163,123,27,81,14,251,24,56,15,35,244,148,24,35,96,195,47,232,148,85,117,91,39,5,74,71,104,116,14,128 38 | 117,108,167,111,201,61,244,13,5,236,75,124,11,233,60,127,101,117,144,13,51,70,138,247,188,171,120,104,141,220,39,86 39 | 243,8,122,204,147,89,112,127,204,217,20,179,8,167,203,254,95,38,22,249,215,127,101,46,99,252,183,17,8,122,191,32 40 | 90,195,11,73,110,20,55,185,206,241,12,193,185,72,141,27,97,29,251,144,6,109,129,203,222,64,164,49,173,37,167,169 41 | 164,41,177,10,34,58,123,165,122,70,108,145,240,246,208,134,248,114,237,129,149,218,70,63,105,5,184,222,9,255,213,153 42 | 211,255,215,20,146,60,12,202,1,95,234,192,175,223,81,99,59,136,191,82,138,174,112,1,21,14,137,7,4,238,50,246 43 | 220,94,230,67,248,163,186,77,68,20,1,180,150,94,127,36,154,47,101,114,172,174,172,248,130,250,55,68,17,106,3,123 44 | 110,20,220,172,53,110,224,20,10,192,59,46,159,96,14,203,214,144,215,141,13,190,175,232,55,123,104,223,79,38,146,169 45 | 164,29,102,221,199,97,1,114,215,130,93,166,31,208,248,5,40,197,96,173,136,209,149,17,74,236,131,18,231,29,214,172 46 | 251,94,49,176,56,250,251,10,237,114,111,176,78,90,148,97,69,174,3,178,4,16,151,192,36,202,212,81,210,20,219,216 47 | 116,79,18,201,172,40,33,232,54,187,32,61,5,227,55,18,43,107,202,220,141,66,224,166,158,176,61,11,143,232,112,47 48 | 187,194,174,69,228,144,68,94,189,124,254,101,65,91,176,76,117,203,236,169,202,251,11,110,242,80,181,94,162,92,111,54 49 | 29,150,209,144,66,224,111,137,81,38,230,100,15,45,7,31,208,240,210,18,111,85,199,64,247,215,164,75,231,34,69,82 50 | 191,102,106,86,63,166,105,80,243,169,231,39,86,171,77,223,72,220,171,98,179,115,160,191,202,89,192,20,178,54,121,137 51 | 254,252,23,88,159,236,167,50,34,70,225,175,28,89,25,150,163,25,241,87,152,213,166,176,237,50,249,60,144,205,27,81 52 | 138,9,193,221,141,92,54,239,124,193,92,251,33,190,134,68,160,220,80,210,146,184,240,135,188,129,101,218,102,213,132,199 53 | 184,160,40,202,31,235,178,121,225,205,231,122,61,178,191,195,55,13,2,41,245,69,128,182,5,90,7,28,79,58,11,43 54 | 247,8,171,147,180,87,67,121,151,143,177,155,64,107,163,222,211,152,178,184,68,211,218,210,252,37,84,189,44,186,133,134 55 | 31,50,155,253,213,220,84,174,239,85,87,105,214,81,160,211,90,32,239,171,171,238,177,234,36,233,216,77,44,173,205,253 56 | 113,18,35,135,205,43,156,23,127,169,162,160,15,49,202,100,165,163,175,30,199,19,141,197,211,200,134,41,215,154,34,31 57 | 204,239,127,208,89,187,30,192,37,152,221,214,211,49,93,9,93,38,25,9,6,86,219,250,25,161,185,32,98,177,32,121 58 | 72,7,24,67,1,245,154,234,84,179,37,96,222,33,64,228,78,254,194,19,197,60,60,241,58,151,184,179,233,70,85,97 59 | 253,151,182,118,101,136,118,241,195,26,152,14,225,28,193,165,140,82,138,36,216,2,152,228,117,234,180,94,11,25,50,148 60 | 20,35,254,1,198,250,222,43,98,131,180,54,101,212,227,212,85,247,217,50,117,7,116,145,101,136,176,12,83,1,146,170 61 | 145,235,144,178,16,181,198,59,220,241,197,242,187,44,243,109,86,53,21,48,83,149,252,147,181,124,48,89,151,149,227,188 62 | 214,115,72,209,6,224,24,39,114,233,248,204,31,222,125,2,236,241,19,132,104,150,172,254,222,170,104,161,199,252,179,230 63 | 241,67,229,75,108,250,81,179,127,247,83,66,159,206,107,96,58,217,252,157,139,17,235,115,5,174,191,230,233,49,241,241 64 | 165,246,113,208,142,14,235,211,178,85,75,239,238,96,147,129,143,18,30,123,124,195,21,230,104,198,220,56,202,53,246,99 65 | 219,121,50,105,81,61,239,218,41,238,236,242,77,40,161,123,92,58,122,26,3,147,12,163,109,207,110,216,66,41,93,220 66 | 56,105,223,38,38,53,34,72,93,91,133,135,1,232,7,61,70,61,102,124,94,21,181,225,227,23,51,104,159,94,117,98 67 | 200,107,25,252,122,136,229,62,85,8,171,117,186,197,183,103,52,41,91,19,211,165,97,52,227,241,116,70,115,251,56,164 68 | 99,62,142,10,191,175,135,155,202,184,151,52,17,239,5,26,201,44,159,38,76,235,82,145,61,162,223,9,169,204,77,189 69 | 197,14,41,244,99,82,51,75,53,246,248,215,70,118,32,124,79,180,4,127,169,157,105,103,85,151,125,63,246,69,228,179 70 | 55,161,79,12,207,40,253,100,230,119,111,97,76,61,94,122,5,74,200,112,206,160,19,75,142,167,222,9,180,99,57,177 71 | 17,80,149,28,144,190,229,240,26,182,124,100,217,51,52,9,182,169,42,94,114,239,69,95,173,11,101,72,64,50,3,135 72 | 12,91,119,248,135,229,140,37,129,209,39,237,182,202,102,204,89,159,208,66,154,204,54,66,32,13,61,13,184,70,75,128 73 | 117,204,87,187,74,161,64,143,219,117,162,84,197,171,98,1,138,76,204,242,153,72,19,180,165,172,112,31,199,12,21,19 74 | 24,225,130,141,144,160,197,221,109,80,74,157,237,227,1,143,161,216,190,28,103,248,231,29,74,248,192,160,226,203,254,14 75 | 242,17,183,221,223,54,147,94,222,19,137,147,116,241,4,34,163,217,140,42,34,191,244,240,48,18,110,84,212,155,159,85 76 | 198,3,198,145,91,104,40,111,171,25,48,170,91,222,108,67,99,147,168,118,148,82,76,9,226,178,78,148,151,183,234,136 77 | 237,10,198,207,133,149,88,94,250,23,24,49,14,86,242,63,235,232,176,37,91,230,60,107,210,175,217,195,113,111,148,57 78 | 252,70,251,156,71,58,104,35,181,22,29,18,45,124,115,246,91,204,171,171,10,8,109,87,86,26,6,194,111,15,44,249 79 | 61,247,189,11,255,205,190,159,73,215,216,211,50,194,165,173,247,237,123,131,188,226,189,197,112,84,126,46,193,255,184,53 80 | 40,156,37,206,118,220,97,4,164,201,150,153,5,88,33,143,80,1,230,22,33,31,14,175,218,151,224,19,52,213,244,149 81 | 7,121,65,169,163,244,187,17,112,237,46,113,109,50,57,188,171,241,132,47,144,234,210,48,167,146,6,41,127,185,80,151 82 | 33,85,209,187,99,27,241,145,182,43,152,91,166,94,114,169,45,163,104,175,26,115,76,130,55,152,136,45,135,225,32,216 83 | 116,149,25,41,167,115,192,226,173,224,121,202,238,11,51,244,227,3,199,183,144,92,131,125,220,163,111,87,243,189,133,212 84 | 160,202,250,200,192,43,249,18,14,151,249,96,181,91,160,155,39,28,47,110,5,38,203,203,1,103,73,198,63,163,145,49 85 | 100,7,242,101,230,151,67,247,146,170,239,129,240,215,250,144,17,158,47,155,183,246,28,5,202,8,216,253,69,13,144,119 86 | 186,166,166,145,230,236,133,44,96,122,206,139,96,30,65,96,251,202,46,177,105,85,144,33,232,28,135,70,64,24,189,122 87 | 125,253,144,215,58,109,158,6,140,237,28,168,63,148,208,125,70,43,60,183,25,111,239,227,103,164,69,30,44,240,238,236 88 | 79,231,215,32,107,20,43,26,230,83,183,70,84,250,132,244,30,68,74,255,253,232,200,251,43,161,44,134,187,133,145,204 89 | 21,229,206,84,62,194,60,102,75,4,220,56,176,209,192,112,8,94,248,15,74,182,177,114,195,206,113,105,159,63,57,165 90 | 112,108,180,230,202,246,252,111,117,175,210,10,195,231,143,229,10,202,230,244,135,102,250,118,242,55,43,125,231,167,135,71 91 | 205,154,65,115,150,138,189,176,159,48,250,135,228,77,78,173,208,178,71,189,8,130,129,62,19,152,204,112,34,15,42,112 92 | 195,133,16,131,217,207,15,17,168,72,182,124,156,4,38,75,208,55,40,147,80,134,226,57,75,233,92,24,247,205,149,27 93 | 128,91,2,15,14,219,62,231,152,107,5,251,73,170,42,255,5,28,181,87,240,145,152,73,251,215,147,35,202,183,79,5 94 | 95,9,5,213,129,103,46,167,187,181,27,117,34,67,118,184,92,144,42,29,251,180,252,115,222,160,23,59,219,107,129,127 95 | 34,145,97,163,240,99,224,52,91,27,163,13,24,220,81,216,61,25,80,27,25,185,99,100,162,201,57,38,169,202,171,224 96 | 155,178,152,248,234,66,116,165,4,232,10,178,59,197,10,91,34,238,48,229,220,24,121,14,142,75,92,140,53,106,227,201 97 | 74,184,197,204,104,42,159,160,168,203,23,245,157,180,35,108,4,247,100,221,252,211,44,40,161,48,91,177,109,16,224,231 98 | 226,80,154,253,172,137,170,25,31,36,56,228,57,78,159,182,128,235,244,155,5,145,21,196,90,100,238,164,152,28,19,89 99 | 18,25,164,149,142,135,198,151,60,180,76,80,230,139,21,246,110,97,210,120,168,133,5,145,244,247,37,98,209,145,37,68 100 | 248,116,245,46,159,233,159,253,83,98,58,194,2,110,157,178,162,165,254,26,224,145,178,152,114,92,76,237,158,173,14,194 101 | 231,91,11,41,98,144,242,73,175,207,52,108,221,155,179,74,98,154,77,47,145,115,196,31,141,207,26,157,128,99,69,145 102 | 75,176,108,107,23,148,51,54,134,194,17,234,222,226,184,52,25,140,39,93,210,10,104,38,9,43,85,10,104,43,222,237 103 | 92,94,46,231,10,36,227,154,49,80,209,2,137,25,108,20,131,193,227,149,192,55,22,75,103,122,104,231,206,70,68,7 104 | 121,214,117,143,206,89,179,32,21,14,178,51,248,135,228,243,2,191,235,169,138,172,49,147,211,75,49,34,221,124,108,159 105 | 185,39,183,74,227,158,201,236,236,6,9,181,104,219,67,64,140,148,86,111,106,201,187,196,168,45,71,181,163,15,149,111 106 | 15,111,192,134,62,204,46,57,198,220,244,251,221,182,220,133,4,232,180,36,154,117,97,116,109,32,152,53,121,42,47,255 107 | 87,1,11,170,17,250,238,55,59,146,185,145,190,62,12,21,70,84,123,167,251,10,125,123,66,246,196,139,109,220,185,22 108 | 71,74,17,6,15,146,107,234,137,157,221,246,241,146,72,115,22,129,144,3,158,222,200,152,18,68,90,188,142,192,24,146 109 | 126,156,188,60,66,222,98,216,17,255,152,216,248,200,14,74,65,139,46,19,154,57,21,115,8,118,158,217,234,177,111,160 110 | 47,142,147,67,44,227,21,168,151,216,89,62,250,165,74,185,147,22,5,138,55,242,24,57,100,167,83,58,175,115,63,33 111 | 73,144,57,155,60,182,188,241,254,163,68,197,171,184,17,18,242,11,197,242,162,153,183,129,64,242,52,164,231,5,160,210 112 | 202,242,96,114,134,254,154,128,117,242,17,246,223,18,112,174,3,235,3,87,136,108,179,77,236,98,152,166,151,130,13,52 113 | 59,228,148,40,210,184,99,13,92,252,250,25,191,183,111,210,135,47,238,31,34,63,59,150,219,190,29,132,221,4,135,219 114 | 65,3,105,33,78,229,48,7,5,17,117,115,16,20,101,108,249,218,89,162,68,88,31,83,78,141,9,81,249,115,114,99 115 | 219,157,199,113,160,253,4,1,253,89,168,204,209,214,213,141,177,76,178,93,218,171,151,169,216,233,37,13,43,26,27,88 116 | 1,175,221,243,223,150,131,20,195,95,120,137,81,97,19,52,129,138,123,79,185,78,132,36,16,192,99,216,25,165,45,183 117 | 123,99,4,20,155,224,253,96,2,165,255,216,84,34,11,83,58,203,206,214,133,224,22,122,211,12,130,206,202,170,225,179 118 | 55,31,34,33,47,208,79,170,205,64,60,102,67,47,10,81,42,63,183,186,103,140,110,52,147,33,69,246,69,95,214,180 119 | 78,217,117,166,51,55,232,219,136,176,59,71,7,54,250,207,62,19,105,137,20,2,4,201,69,77,35,123,71,98,9,88 120 | 1,58,153,65,8,5,73,248,25,42,145,26,218,183,243,27,195,5,36,148,109,128,45,183,112,93,199,222,111,201,85,165 121 | 112,120,107,177,223,192,59,26,235,253,252,71,225,141,233,214,97,1,189,40,28,65,204,234,55,132,184,203,80,87,113,43 122 | 247,192,115,208,207,151,104,240,244,133,129,128,125,183,156,136,198,206,190,7,69,58,222,158,160,23,138,2,251,165,232,252 123 | 98,117,101,84,61,44,230,6,198,187,59,63,121,152,178,208,42,229,79,62,188,233,226,157,46,249,179,10,249,202,36,193 124 | 210,77,203,244,98,21,100,71,96,65,54,182,156,230,250,224,97,97,31,13,209,19,108,22,14,81,255,241,196,144,48,171 125 | 130,162,74,188,56,12,24,172,87,250,78,57,164,215,95,252,182,219,141,135,187,37,83,188,187,162,34,103,11,133,124,229 126 | 196,155,245,4,123,227,238,196,192,201,155,47,214,115,221,199,165,240,196,144,236,254,136,213,193,9,247,63,140,105,154,46 127 | 168,253,206,153,244,90,190,188,118,131,197,151,204,138,190,46,116,159,121,214,81,142,12,49,8,186,199,229,247,216,224,39 128 | 35,18,213,92,18,32,163,180,130,116,180,242,103,130,93,241,167,10,104,181,54,135,118,39,89,7,169,11,99,104,47,45 129 | 157,150,134,244,214,20,46,134,50,218,163,99,173,61,240,43,35,238,145,233,16,104,165,150,124,224,137,40,96,163,243,130 130 | 83,94,239,60,225,202,211,119,171,212,59,66,104,180,180,154,216,159,161,81,129,234,220,73,126,135,22,73,32,199,236,64 131 | 180,51,88,137,101,242,22,92,8,209,99,140,86,232,224,9,185,92,56,176,178,232,11,157,180,56,55,7,44,122,96,192 132 | 193,20,57,242,98,80,139,154,221,134,21,167,176,203,20,58,108,40,168,11,136,9,214,200,135,126,245,4,168,194,142,20 133 | 97,223,113,66,240,219,163,213,247,105,91,200,228,152,156,102,140,2,240,50,129,133,160,93,174,246,89,111,195,22,26,78 134 | 70,8,143,72,73,69,52,183,169,243,7,53,53,120,43,2,105,112,241,117,239,48,104,246,141,176,208,220,126,224,229,157 135 | 159,27,28,198,131,35,183,49,168,168,102,146,77,18,157,130,200,86,133,151,5,132,76,243,194,4,55,182,159,191,21,13 136 | 199,26,140,14,238,2,67,91,6,205,170,100,199,87,74,59,207,195,16,130,205,225,88,2,88,88,210,48,97,114,249,174 137 | 221,62,67,44,76,233,250,18,23,177,178,213,175,134,50,222,206,224,25,32,152,125,204,99,56,175,235,226,50,129,168,28 138 | 249,207,146,253,136,29,143,209,20,235,30,29,26,151,85,116,134,62,72,44,92,53,101,226,57,136,158,222,10,192,41,227 139 | 174,229,7,70,206,29,89,189,213,188,33,212,151,193,254,218,239,38,5,110,143,97,37,81,142,18,93,184,110,93,251,91 140 | 52,86,100,126,146,223,48,62,75,108,70,219,245,33,187,154,183,167,3,107,238,39,158,207,110,84,216,51,15,116,120,71 141 | 205,222,123,163,14,210,148,124,206,14,57,19,53,123,136,153,175,15,42,88,151,235,192,90,170,4,175,131,108,231,249,143 142 | 148,139,48,211,158,147,117,33,102,77,237,218,77,54,170,68,39,24,6,237,106,137,131,98,25,203,36,104,92,38,238,241 143 | 165,147,185,5,22,252,130,247,32,76,241,81,118,178,107,64,171,15,223,129,12,34,141,142,121,218,185,163,68,128,225,124 144 | 23,231,58,82,90,211,40,239,63,155,129,60,128,142,31,64,164,157,221,125,225,114,37,76,217,172,3,27,146,193,82,144 145 | 88,207,100,97,177,177,65,193,199,91,12,22,17,189,51,16,199,144,46,188,87,110,210,240,211,202,253,98,143,190,114,1 146 | 19,215,123,95,188,172,128,108,38,206,18,69,137,19,35,187,196,29,158,95,107,67,213,229,121,102,1,140,3,8,176,137 147 | 254,80,166,73,150,111,173,219,30,147,134,90,126,134,161,248,199,149,48,98,165,13,150,197,183,129,198,253,8,124,37,152 148 | 192,42,26,56,12,149,104,49,152,50,118,99,251,83,191,154,145,109,86,254,190,138,28,230,102,101,198,8,70,104,191,253 149 | 113,205,59,6,8,242,213,43,224,222,197,129,5,28,200,123,236,104,226,167,146,6,233,104,223,138,10,55,146,240,231,109 150 | 41,164,95,124,213,29,32,127,210,194,190,165,202,223,58,3,138,33,84,114,225,165,197,72,206,32,180,154,57,8,204,102 151 | 132,124,62,144,115,15,9,136,217,163,11,119,222,6,194,64,125,170,127,248,166,74,7,152,84,50,72,24,24,123,86,214 152 | 134,57,102,153,222,197,231,129,183,241,40,128,43,111,140,103,38,43,154,52,249,56,182,33,235,152,83,3,178,91,75,9 153 | 139,5,68,152,226,43,97,191,13,246,202,19,147,57,117,48,93,98,85,68,21,77,50,36,148,159,69,141,77,121,37,59 154 | 218,35,129,104,183,103,180,64,135,243,110,82,44,229,61,124,225,211,61,172,216,110,173,55,22,43,189,188,227,32,38,163 155 | 26,166,237,51,2,49,255,9,59,85,142,19,204,119,216,15,196,197,79,10,236,140,159,216,166,123,78,138,105,238,188,120 156 | 91,94,229,234,63,179,33,38,122,164,161,122,132,60,152,233,156,189,47,52,17,194,93,130,145,157,169,53,34,202,4,6 157 | 106,94,193,127,30,113,21,207,78,76,15,10,31,136,95,143,43,122,153,20,252,105,127,239,147,8,29,146,110,23,238,36 158 | 22,92,183,30,142,237,219,104,197,87,160,227,70,87,224,149,103,38,159,198,144,22,65,13,1,94,91,66,183,101,242,51 159 | 66,178,17,94,151,227,231,143,59,115,189,74,80,32,215,221,170,119,9,201,172,75,71,225,3,127,106,13,3,150,74,255 160 | 87,76,214,123,201,83,193,254,141,111,22,216,15,179,154,30,30,250,228,26,22,60,67,93,215,152,155,121,85,166,5,115 161 | 246,147,7,89,171,183,133,26,210,65,50,75,127,233,103,26,2,138,114,163,147,164,140,162,174,239,28,220,93,46,46,36 162 | 22,192,122,216,168,88,29,248,16,203,173,222,7,55,129,173,242,15,111,45,39,121,140,254,76,99,188,67,249,86,203,243 163 | 131,18,135,57,186,240,43,197,42,40,229,131,81,252,75,102,247,115,212,10,127,71,22,239,234,248,154,217,173,54,141,178 164 | 36,104,231,153,223,236,110,81,143,97,248,53,135,40,74,153,203,40,1,209,108,98,114,3,143,173,122,159,51,191,68,35 165 | 111,152,170,153,65,127,209,208,212,169,111,246,131,193,189,31,103,250,231,20,74,234,76,133,117,110,181,111,128,138,112,66 166 | 146,12,235,186,103,104,193,4,124,188,140,74,193,7,180,13,137,74,65,20,68,11,96,99,119,68,85,70,200,201,49,183 167 | 123,240,167,34,210,88,3,34,18,26,28,44,151,178,109,244,3,195,14,236,222,29,83,158,148,107,6,248,84,123,141,175 168 | 206,13,29,60,189,200,145,127,137,172,44,42,120,212,73,113,213,246,23,79,33,122,139,95,45,214,19,71,155,51,175,147 169 | 109,154,79,11,165,113,9,15,99,246,224,96,187,67,214,195,151,146,87,229,1,146,10,7,70,252,199,225,112,35,146,236 170 | 79,247,176,255,183,139,55,141,239,188,172,186,156,126,83,242,160,149,243,148,175,240,53,30,207,128,116,4,68,217,66,176 171 | 2,167,119,216,190,219,119,23,20,182,254,238,157,225,233,140,234,214,251,20,65,154,174,78,113,255,137,147,44,69,183,7 172 | 121,136,140,214,241,237,76,180,152,43,84,28,20,147,28,118,154,214,252,177,11,45,156,43,214,93,180,195,169,158,111,108 173 | 58,35,174,240,216,249,14,203,170,179,2,125,200,204,43,95,83,141,228,29,32,40,85,10,4,156,168,85,172,180,172,21 174 | 114,171,242,238,47,124,59,125,65,23,39,150,161,226,5,209,61,231,91,15,64,57,58,233,229,192,112,67,243,149,98,151 175 | 33,32,3,8,36,151,121,17,218,26,98,82,65,146,162,149,64,53,126,112,58,163,159,106,238,218,218,91,237,109,12,234 176 | 37,190,149,41,64,68,119,19,153,51,235,147,203,136,225,145,52,164,112,134,242,179,185,57,179,177,210,34,165,113,40,14 177 | 242,44,232,202,123,230,119,236,16,231,234,50,122,215,111,194,189,76,240,228,184,42,191,174,20,235,39,70,86,220,37,131 178 | 64,190,225,105,2,122,16,3,38,255,79,66,166,97,192,141,229,219,13,65,91,86,37,100,207,90,214,150,47,118,157,109 179 | 41,149,44,166,186,23,168,186,250,4,159,47,9,124,71,11,124,40,231,157,7,108,149,132,194,48,100,70,152,181,74,28 180 | 249,59,57,146,2,235,113,131,188,6,171,48,224,49,175,1,83,140,128,210,225,170,116,187,222,114,1,81,174,106,29,1 181 | 19,118,143,2,79,31,218,92,100,181,79,226,175,226,175,39,213,137,130,241,5,245,17,67,50,107,185,112,48,41,100,230 182 | 241,134,224,140,191,179,174,113,4,225,15,245,177,126,87,178,31,224,132,12,254,124,136,206,184,66,126,55,56,198,36,38 183 | 48,196,26,73,218,118,123,137,168,15,159,113,154,253,55,144,239,187,25,57,59,60,63,148,47,2,154,195,208,32,63,18 184 | 11,43,62,251,191,45,35,203,140,42,121,233,87,190,201,82,228,103,71,184,123,78,40,6,227,199,165,59,95,91,220,223 185 | 13,53,76,103,206,252,97,243,120,229,154,201,166,244,46,208,14,246,41,210,152,81,184,156,192,91,123,48,223,215,21,243 186 | 131,9,114,15,5,150,143,185,33,64,203,180,70,93,136,201,138,143,45,76,128,248,62,219,136,116,162,30,222,16,12,108 187 | 58,217,47,14,1,13,117,8,167,10,105,226,96,158,229,203,236,157,189,204,221,163,128,168,244,194,129,67,113,195,34,118 188 | 169,119,204,119,80,22,46,55,120,70,39,68,156,140,150,247,116,237,35,82,233,43,126,138,204,151,134,110,159,171,125,51 189 | 66,13,58,37,254,61,28,122,100,206,172,205,247,250,12,171,200,100,194,117,56,50,122,222,132,178,163,208,47,190,132,112 190 | 195,10,135,248,143,167,184,176,98,179,72,42,214,23,145,9,214,47,254,22,152,182,82,194,171,126,118,119,87,192,36,181 191 | 93,162,212,56,55,138,174,1,117,22,129,122,212,161,92,220,178,53,119,177,111,233,133,194,58,192,183,57,167,247,152,81 192 | 138,170,159,30,237,244,80,230,79,150,12,155,244,118,126,1,234,205,124,84,116,79,204,164,206,86,151,148,99,226,190,68 193 | 248,236,70,21,20,138,236,107,34,17,24,130,201,124,96,217,85,33,82,180,204,77,120,81,71,102,237,95,104,31,125,89 194 | 18,3,24,73,102,63,197,209,78,137,121,112,128,123,39,9,1,180,24,222,135,76,217,252,97,234,39,97,42,97,38,42 195 | 228,45,188,152,234,51,166,35,216,41,188,76,213,212,244,206,205,116,5,211,100,181,104,188,63,244,47,236,48,88,208,80 196 | 153,156,164,77,144,52,216,195,138,50,122,239,93,117,149,33,44,104,51,87,193,80,43,126,57,230,104,125,215,242,31,247 197 | 207,155,49,11,124,188,131,180,170,150,37,109,38,174,75,104,12,226,139,143,126,241,233,148,116,99,20,212,10,62,17,173 198 | 232,186,3,220,51,92,23,165,204,6,50,129,26,97,116,90,252,80,42,40,241,242,12,139,40,65,144,183,117,126,246,228 199 | 215,126,89,118,198,245,143,230,46,91,46,26,241,207,245,100,215,96,65,70,148,160,228,189,127,47,223,252,61,144,111,95 200 | 120,41,21,204,241,163,28,92,171,179,152,237,125,79,247,139,126,208,125,246,189,108,137,210,156,75,238,104,210,1,141,24 201 | 181,108,111,71,59,212,1,131,225,115,37,14,100,77,222,171,164,193,64,145,241,64,162,123,150,194,113,2,25,6,145,13 202 | 199,48,251,125,111,186,180,237,180,132,113,39,91,126,21,120,230,175,135,71,203,21,156,236,208,12,20,118,213,244,64,42 203 | 228,248,143,123,222,58,35,82,228,211,177,68,130,15,241,252,188,147,30,76,253,249,49,249,47,69,201,223,39,167,69,54 204 | 29,201,235,177,124,218,197,238,93,127,73,43,46,238,83,94,96,57,138,224,138,98,197,122,96,10,177,55,102,167,190,120 205 | 91,89,138,236,189,205,202,28,157,194,139,189,188,222,1,98,205,25,211,241,251,164,179,216,51,91,216,202,159,197,77,4 206 | 76,93,179,102,191,201,9,126,167,185,251,178,251,180,156,27,21,130,33,10,138,171,114,111,198,221,80,1,83,185,253,134 207 | 31,137,206,229,32,215,202,228,232,101,97,35,255,234,127,236,159,230,245,56,25,32,201,66,153,211,32,73,144,210,206,133 208 | 42,86,219,213,217,111,135,80,70,49,102,98,210,232,158,138,9,31,131,127,79,143,146,160,50,78,110,170,123,133,183,166 209 | 69,223,198,190,49,54,2,163,119,185,60,107,37,131,9,62,145,184,181,28,147,95,19,12,166,4,235,241,135,215,47,217 210 | 103,126,39,5,127,60,220,60,120,40,162,39,65,138,112,7,160,101,210,27,244,193,20,21,219,135,56,69,78,58,189,32 211 | 90,87,125,20,167,248,82,21,141,69,253,119,45,1,160,160,152,226,184,84,228,78,63,186,229,248,223,190,249,99,231,171 212 | 130,42,80,10,216,201,245,154,5,23,74,242,101,102,184,166,246,34,14,32,226,16,14,239,23,73,139,71,68,184,143,50 213 | 81,169,211,130,94,168,242,140,46,186,180,233,222,1,120,13,77,60,3,41,157,45,250,153,104,220,182,172,103,226,153,121 214 | 72,154,94,239,83,242,137,6,24,184,7,9,225,44,112,193,74,238,216,9,229,167,71,208,89,230,197,188,101,67,6,216 215 | 116,252,214,166,243,67,41,154,58,78,234,85,188,76,65,246,139,100,138,7,54,163,87,118,142,205,17,26,98,95,39,242 216 | 144,255,15,174,25,182,1,220,175,11,41,141,69,69,174,46,120,208,177,158,130,66,119,3,235,143,255,5,149,42,138,165 217 | 112,233,174,39,48,209,136,82,207,75,221,255,118,18,27,139,84,186,104,189,22,174,14,176,131,31,106,243,139,173,146,244 218 | 250,254,133,76,108,59,53,147,15,200,135,17,138,131,147,99,199,233,75,71,240,26,199,232,60,27,173,69,39,246,92,48 219 | 119,210,114,33,191,38,98,22,244,162,249,182,30,234,188,43,61,164,212,142,73,23,155,62,96,128,111,104,167,146,203,65 220 | 167,152,96,47,165,177,203,192,142,135,92,7,61,156,32,137,220,99,13,180,186,52,77,237,74,147,251,15,108,122,59,28 221 | 249,181,52,222,139,244,215,224,198,114,40,243,200,5,79,102,209,44,203,56,200,23,44,99,183,250,162,206,231,158,210,112 222 | 195,177,63,246,116,210,113,123,248,17,244,174,232,40,110,74,27,54,182,31,213,70,119,148,22,77,62,118,73,8,4,227 223 | 174,223,121,235,197,225,150,67,127,80,219,62,36,51,65,225,209,187,13,144,188,232,86,67,248,61,152,24,198,30,51,118 224 | 169,227,202,33,181,210,194,212,51,158,125,246,64,200,59,83,94,208,5,226,181,74,53,92,39,155,121,119,196,28,160,34 225 | 124,154,149,143,36,8,222,81,182,28,217,58,223,4,195,230,121,181,17,97,174,39,223,245,163,115,72,29,9,250,221,93 226 | 94,129,132,118,175,123,131,87,207,57,77,136,126,101,29,176,73,215,180,68,41,126,101,135,219,224,57,29,241,38,251,65 227 | 167,177,51,217,242,161,150,33,207,188,199,179,3,252,175,40,71,23,126,212,112,84,36,19,243,40,155,89,25,44,143,44 228 | 142,157,145,41,142,233,158,158,64,158,34,89,115,91,16,81,46,212,130,142,166,58,205,243,193,255,109,107,83,94,185,217 229 | 103,181,101,82,232,131,3,160,69,31,133,57,115,220,168,30,207,218,190,44,102,244,113,203,36,224,195,195,212,238,52,182 230 | 104,138,170,151,231,202,217,205,81,42,246,108,123,2,40,96,196,95,144,98,49,43,23,184,181,141,105,31,176,224,164,81 231 | 138,159,183,96,66,36,16,145,131,178,48,236,226,217,221,117,86,187,22,179,167,17,14,54,180,217,218,74,69,245,169,120 232 | 91,206,220,176,108,243,52,201,226,28,70,196,123,18,54,236,230,47,81,109,168,137,192,19,127,143,11,161,226,230,31,19 233 | 24,207,128,6,155,134,151,169,43,105,35,121,125,93,184,219,76,180,221,129,248,201,38,206,237,34,227,219,92,238,20,4 234 | 76,30,37,108,85,239,66,35,18,15,80,26,63,117,31,162,172,3,140,4,250,168,31,250,208,3,41,58,66,122,214,223 235 | 13,114,121,124,89,22,163,146,144,123,191,224,85,156,229,6,254,190,77,25,36,208,94,171,60,104,191,188,222,202,52,249 236 | 61,6,137,137,31,211,146,84,248,242,181,113,192,186,69,59,7,72,9,101,14,204,101,246,140,62,12,151,191,78,125,45 237 | 157,136,146,168,3,25,221,183,210,160,37,98,46,154,200,51,233,78,211,136,192,80,238,133,173,53,176,141,252,127,213,208 238 | 236,37,13,175,18,251,87,187,224,204,128,5,91,147,115,122,151,89,90,163,65,38,17,122,231,248,212,192,124,138,107,170 239 | 145,19,150,65,190,97,199,178,76,115,138,198,136,18,180,253,248,247,187,179,194,161,221,246,94,254,64,61,91,186,104,69 240 | 235,120,75,39,150,196,72,209,145,27,180,77,11,2,154,155,125,233,102,2,252,239,45,119,156,67,142,249,160,160,43,116 241 | 143,165,24,101,222,187,133,80,114,98,164,11,16,227,43,133,145,213,75,107,148,34,89,73,28,136,140,131,231,105,2,209 242 | 255,184,148,30,2,59,196,206,224,101,11,205,173,175,148,17,245,251,207,74,117,102,216,250,162,252,162,141,19,22,115,134 243 | 31,58,43,194,88,106,56,41,88,34,189,211,128,188,100,228,149,6,140,214,89,223,4,232,12,183,1,187,28,146,97,11 244 | 173,159,50,163,30,151,172,58,118,11,139,20,227,150,135,213,107,120,100,176,68,197,190,248,82,15,225,61,55,196,240,250 245 | 8,42,165,143,186,179,64,44,100,15,30,158,136,10,240,220,181,174,221,223,195,144,160,79,89,146,43,90,157,51,97,249 246 | 61,3,209,85,236,48,55,183,70,6,193,208,190,103,211,46,221,3,25,245,200,43,37,8,104,91,246,3,89,13,132,120 247 | 91,121,64,214,89,93,32,238,196,217,242,53,71,78,136,226,189,164,233,98,238,230,250,56,65,83,137,141,171,250,231,62 248 | 133,122,163,187,119,181,55,152,138,23,49,26,211,59,150,19,144,166,205,184,82,209,107,20,157,165,177,216,27,103,147,81 249 | 138,114,71,105,110,180,111,127,214,105,13,43,148,113,228,203,37,239,239,238,125,114,244,74,24,241,242,146,130,94,46,79 250 | 85,108,150,69,191,198,106,86,228,219,78,42,73,10,92,242,16,3,18,27,228,216,36,149,19,179,28,6,226,38,163,82 251 | 191,46,144,17,234,91,79,85,44,28,26,143,24,237,106,132,165,28,88,162,186,248,45,92,31,189,164,172,255,51,125,111 252 | 15,105,201,161,101,197,235,191,127,28,238,232,231,198,234,172,192,193,60,42,87,165,80,226,245,151,8,214,96,118,19,23 253 | 84,157,205,255,217,251,101,194,230,208,26,232,23,201,46,29,123,221,11,53,196,102,220,130,2,70,240,1,178,74,188,195 254 | 244,120,86,42,110,203,209,158,119,115,207,5,104,140,138,113,25,153,59,171,105,67,136,70,30,10,203,80,13,200,172,216 255 | 116,64,52,174,54,126,16,194,162,33,33,157,176,197,225,12,59,55,253,228,148,47,179,185,24,138,253,20,142,55,172,88 256 | -------------------------------------------------------------------------------- /has_message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Wed Jun 9 08:47:52 2021 5 | 6 | @author: daniele 7 | """ 8 | 9 | import numpy as np 10 | 11 | # use the "galois" package for operations on GF(2^8) and Reed-Solomon decoding 12 | import galois 13 | 14 | 15 | class has_message : 16 | """ 17 | Summary : 18 | 19 | Container class for a HAS message. A message is made of several pages, 20 | each page is made of 53 bytes (424 bits). Each byte has to be interpreted 21 | as an element of GF(2^8) 22 | """ 23 | 24 | # Static members - these variables should never be touched (after initialization)! 25 | 26 | # Finite field for decoding operations 27 | GF256 = galois.GF(2**8) 28 | 29 | # Reed-Solomon encoding matrix 30 | H = np.genfromtxt('has_encoding_matrix.csv', delimiter=',', dtype=np.uint8) 31 | 32 | def __init__(self, mtype, mid, size, ind_offset = 1) : 33 | """ 34 | Summary : 35 | Object constructor. 36 | 37 | Arguments : 38 | mtype - type of the message (only MT1 is currently supported) 39 | mid - ID of the message 40 | msize - size of the message expressed in number of pages 41 | ind_offset - during the first testing phase page indexes started as zero, 42 | it was then updated to 1. This offset takes into account 43 | this effect. 44 | Returns: 45 | Nothing. 46 | """ 47 | 48 | # Set the message type 49 | self.mtype = mtype 50 | 51 | # Set the message ID 52 | self.id = mid 53 | 54 | # Set the size of message in pages 55 | self.size = size 56 | 57 | # Allocate the matrix containing the different pages 58 | self.pages = np.zeros((size, 53), dtype = np.uint8) 59 | 60 | # Array with the page ID 61 | self.page_ids = - np.ones(size, dtype = np.uint8) 62 | 63 | # Index pointing to the next available page slot 64 | self.page_index = 0 65 | 66 | # Age of the message: tells for how many epochs the message was not 67 | # updated. To be used by the decoder to remove messages 68 | self.age = 0 69 | 70 | self.ind_offset = ind_offset 71 | 72 | def add_page( self, page_id, page ) : 73 | """ 74 | Summary : 75 | Add a new page to the message 76 | 77 | Arguments : 78 | page_id - the page ID 79 | page - array with the 53 bytes of the new page 80 | 81 | Returns : 82 | True if the update was correctly performed 83 | """ 84 | # In any case reset the age of the message 85 | self.age = 0 86 | 87 | # Add a page only if the message is not complete 88 | if self.page_index == self.size : 89 | return False 90 | 91 | # Add a page only if it is not present in the message 92 | if page_id not in self.page_ids : 93 | # Add the page to the message 94 | self.page_ids[self.page_index] = page_id 95 | self.pages[self.page_index, :] = page 96 | self.page_index += 1 97 | 98 | return True 99 | else : 100 | return False 101 | 102 | def add_page_sep_bytes(self, page_as_sep_bytes) : 103 | """ 104 | Summary : 105 | A Septentrio receiver provides the CNAV page as an array of 16 32-bit 106 | integers (512 bits) including headers and other unecessary elements. 107 | This function update the a HAS message using the bytes provided by the 108 | Septentrio receiver. 109 | 110 | Arguments : 111 | page_as_sep_bytes - array with 16 32-bit integers representing the HAS 112 | message 113 | Returns : 114 | True if the update was correctly performed 115 | """ 116 | 117 | # Extract the header 118 | HAS_Header = ( (page_as_sep_bytes[0] & 0x3FFFF) << 6 ) + \ 119 | ( page_as_sep_bytes[1] >> 26) 120 | 121 | mtype = ( HAS_Header >> 18 ) & 0x3 122 | 123 | if mtype != self.mtype : 124 | return False 125 | 126 | mid = ( HAS_Header >> 13 ) & 0x1F 127 | 128 | if mid != self.id : 129 | return False 130 | 131 | msize = (( HAS_Header >> 8 ) & 0x1F) + 1 132 | 133 | if msize != self.size : 134 | return False 135 | 136 | # If here, everything is fine and we can start building the actual page 137 | # in bytes 138 | page_id = (HAS_Header) & 0xFF 139 | 140 | page = np.zeros(53, dtype = np.uint16) 141 | 142 | # Treat the first 32 bit integer differently 143 | page[0] = ( page_as_sep_bytes[1] >> 18 ) & 0xFF 144 | page[1] = ( page_as_sep_bytes[1] >> 10 ) & 0xFF 145 | page[2] = ( page_as_sep_bytes[1] >> 2 ) & 0xFF 146 | 147 | # two bits are left 148 | rem_bits = ( page_as_sep_bytes[1] & 0x3 ) << 6 149 | 150 | # Process the other bytes (last excluded) 151 | for ii in range(2, 14) : 152 | page[3 + (ii - 2) * 4] = rem_bits + (( page_as_sep_bytes[ii] >> 26 ) & 0x3F) 153 | page[4 + (ii - 2) * 4] = ( page_as_sep_bytes[ii] >> 18 ) & 0xFF 154 | page[5 + (ii - 2) * 4] = ( page_as_sep_bytes[ii] >> 10 ) & 0xFF 155 | page[6 + (ii - 2) * 4] = ( page_as_sep_bytes[ii] >> 2 ) & 0xFF 156 | rem_bits = ( page_as_sep_bytes[ii] & 0x3 ) << 6 157 | 158 | # Two more bytes to be extracted from the last 32 bit integer 159 | page[51] = rem_bits + (( page_as_sep_bytes[14] >> 26 ) & 0x3F) 160 | page[52] = ( page_as_sep_bytes[14] >> 18 ) & 0xFF 161 | 162 | # Now we are ready for updating the message 163 | success = self.add_page(page_id, page ) 164 | 165 | return success 166 | 167 | def increase_age(self) : 168 | """ 169 | Summary : 170 | Increase the age of the message 171 | 172 | Arguments : 173 | None. 174 | 175 | Returns : 176 | Nothing. 177 | """ 178 | self.age += 1 179 | 180 | def is_old(self, limit_age) : 181 | """ 182 | Summary : 183 | Tell if the message is old 184 | 185 | Arguments : 186 | limit_age - the maximum age allowed for the message 187 | 188 | Returns : 189 | True if the message is old. 190 | """ 191 | if self.age > limit_age : 192 | return True 193 | else : 194 | return False 195 | 196 | def complete(self) : 197 | """ 198 | Summary : 199 | Check if the message is complete, i.e. if all the pages required have been 200 | collected 201 | 202 | Arguments : 203 | None. 204 | 205 | Returns : 206 | True if the message is complete 207 | """ 208 | return (self.size == self.page_index) 209 | 210 | def is_message(self, msg_type, msg_id, msg_size) : 211 | """ 212 | Summary : 213 | Check if this message is the one identified by msg_type, msg_id, msg_size 214 | 215 | Arguments : 216 | msg_type - the message type 217 | msg_id - the message id 218 | msg_size - the message size 219 | 220 | Returns : 221 | True if this is the correct message 222 | """ 223 | is_message = (self.mtype == msg_type) and (self.id == msg_id) and \ 224 | (self.size == msg_size) 225 | 226 | return is_message 227 | 228 | def decode(self) : 229 | """ 230 | Summary : 231 | Decode the message. 232 | 233 | Arguments : 234 | None. 235 | 236 | Returns : 237 | The deconded message 238 | """ 239 | 240 | if not self.complete() : 241 | return None 242 | 243 | # For the moment only MT1 messages are supported 244 | if self.mtype != 1 : 245 | return None 246 | 247 | # If here, perform decoding 248 | # Get the reduced encoding matrix 249 | HR = self.GF256(self.H[self.page_ids - self.ind_offset, 0:self.size]) 250 | 251 | try : 252 | HRinv = np.linalg.inv(HR) 253 | except : 254 | return None 255 | 256 | msg = HRinv @ self.GF256(self.pages) 257 | 258 | return msg 259 | 260 | -------------------------------------------------------------------------------- /has_widgets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on 19 October 2022 5 | 6 | @author: 7 | Daniele Borio 8 | """ 9 | import os 10 | from ipywidgets import (VBox, Dropdown, HTML, Layout, RadioButtons, Button ) 11 | 12 | # Main file with actual parsing routines 13 | from process_cnav import parse_data 14 | 15 | 16 | class process_button_widget(VBox) : 17 | """ 18 | Summary: 19 | Simple widget with a single button to start the processing. 20 | """ 21 | def __init__(self, filename, options) : 22 | """ 23 | Summary : 24 | Object constructor. 25 | 26 | Arguments: 27 | filename - filename of the file to process 28 | options - list with the options defining the processing parameters 29 | 30 | Returns: 31 | The process_button_widget. 32 | """ 33 | self.filename = filename 34 | self.options = options 35 | 36 | self.wb_process = Button( 37 | description='Parse', 38 | disabled=False, 39 | icon='Initialize' 40 | ) 41 | 42 | @self.wb_process.on_click 43 | def process_on_click(b) : 44 | _type = None 45 | 46 | # Get the _rx type 47 | if self.options[0] == "Septentrio" : 48 | rx = "sep" 49 | 50 | if len(self.options) > 1 : 51 | if self.options[1] == 'decimal' : 52 | _type = "txt" 53 | elif self.options[1] == 'binary': 54 | _type = "bin" 55 | else : 56 | _type = "hexa" 57 | 58 | elif self.options[0] == "Novatel" : 59 | rx = "nov" 60 | elif self.options[0] == "Javad" : 61 | rx = "jav" 62 | else : 63 | raise Exception("Unsupported Receiver Type") 64 | 65 | # Check if filename is directory or a file 66 | if os.path.isdir(filename) : 67 | files = os.listdir(filename) 68 | 69 | for f in files : 70 | full_path = filename + f 71 | # process all the files 72 | parse_data(full_path, rx, _type, True) 73 | else : 74 | # it is a filename 75 | # Start the processing 76 | parse_data(filename, rx, _type, True) 77 | 78 | super().__init__([self.wb_process], 79 | layout=Layout(border='1px solid black')) 80 | 81 | class receiver_type_widget(VBox) : 82 | """ 83 | Summary: 84 | Simple widget used to select the input data type. 85 | The selection is based on the type of receiver used for the data 86 | collection. 87 | 88 | The following receivers are currently supported: 89 | Septentrio - Galileo CNAV message 90 | NovAtel - GALCNAVRAWPAGE message in ASCII format 91 | Javad - ED message 92 | """ 93 | 94 | # List of recevier currently supported 95 | rx_list = ["Javad", 96 | "Novatel", 97 | "Septentrio"] 98 | 99 | def __init__(self) : 100 | """ 101 | Summary : 102 | Object constructor. 103 | 104 | Arguments: 105 | None. 106 | 107 | Returns: 108 | The receiver_type_widget. 109 | """ 110 | 111 | # Create a drop-down menu with the list of supported receivers 112 | self.rx_ddmenu = Dropdown( 113 | options = receiver_type_widget.rx_list, 114 | description="Rx Type:", 115 | placeholder="rx_type", 116 | disabled=False ) 117 | 118 | self.other_elements = None 119 | 120 | def on_down_change(change) : 121 | 122 | # Do something only if "Septentrio" is selected 123 | if self.rx_ddmenu.value == "Septentrio" : 124 | # Add a format type (radio button) 125 | radio_input = RadioButtons( 126 | options=['binary','hexadecimal', 'decimal'], 127 | description = 'File Type', 128 | disabled = False) 129 | 130 | self.children = [HTML(value = "Receiver type:"), 131 | self.rx_ddmenu, radio_input] 132 | else: 133 | self.other_elements = None 134 | self.children = [HTML(value = "Receiver type:"), 135 | self.rx_ddmenu] 136 | 137 | self.rx_ddmenu.observe(on_down_change, 'value') 138 | 139 | super().__init__([HTML(value = "Receiver type:"), 140 | self.rx_ddmenu], 141 | layout=Layout(border='1px solid black')) 142 | 143 | 144 | def get_options(self) : 145 | """ 146 | Summary : 147 | Obtain the options set in through the widget. 148 | 149 | Arguments: 150 | None. 151 | 152 | Returns: 153 | List with the options. 154 | """ 155 | 156 | options = [self.rx_ddmenu.value] 157 | if options[ 0 ] == "Septentrio" : 158 | if len(self.children) == 3 : 159 | options.append(self.children[-1].value) 160 | else : 161 | raise Exception("Missing parameter") 162 | 163 | return options 164 | -------------------------------------------------------------------------------- /manual/GHASP_user_manual_Feb23.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borioda/HAS-decoding/1a216c0ee6c94d3ed1e1c361a382a355374618ec/manual/GHASP_user_manual_Feb23.pdf -------------------------------------------------------------------------------- /plot/clk_adev.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Mon Jan 30 17:05:49 2023 4 | 5 | @author: Daniele 6 | """ 7 | 8 | import pandas as pd 9 | import matplotlib.pyplot as plt 10 | import numpy as np 11 | import allantools as at 12 | 13 | 14 | import matplotlib as mp 15 | 16 | 17 | def clk_adev( filename ) : 18 | 19 | fsize = 16 20 | mp.rc('xtick', labelsize=fsize) 21 | mp.rc('ytick', labelsize=fsize) 22 | 23 | 24 | plt.style.use('ggplot') 25 | gnss_ids = ["G", "", "E"] 26 | gnss_spell = ["GPS", "", "Galileo"] 27 | 28 | # load the csv file with the clock corrections 29 | fname = filename[:filename.rfind(".")] 30 | df = pd.read_csv(filename) 31 | 32 | # determine which GNSS is present in the correction file 33 | GNSS = np.unique(df["gnssID"].values) 34 | 35 | ###################### SOME INFO ##################### 36 | # light speed [m/sec] 37 | v_light = 299792458 38 | 39 | # Tau for ADEV 40 | tau = np.logspace(1, 5, 40) 41 | 42 | ####################################################### 43 | 44 | # make different plots for each gnss 45 | for gnss in GNSS : 46 | # filter by gnss 47 | gnss_df = df[df["gnssID"] == gnss] 48 | 49 | # Create a new plot 50 | fig, ax = plt.subplots() 51 | fig.set_size_inches((12, 8)) 52 | # Determine the list of prn 53 | sats = np.unique(gnss_df["PRN"].values) 54 | 55 | # Make a plot for each satellite 56 | for sat in sats : 57 | sat_df = gnss_df[gnss_df["PRN"] == sat] 58 | 59 | # build the time index 60 | time = np.floor( sat_df["ToW"].values / 3600 ) * 3600 + sat_df["ToH"].values 61 | 62 | # build the clock correction 63 | clk_corr = sat_df["delta_clock_c0"].values * sat_df["multiplier"].values 64 | 65 | # remove duplicated values 66 | time, ind = np.unique(time, return_index = True) 67 | clk_corr = clk_corr[ind] / v_light 68 | 69 | # measurement rate 70 | Ts = np.median(np.diff(time)) 71 | rate = 1 / Ts 72 | 73 | t, ad, ade, adn = at.oadev( clk_corr, rate = rate, \ 74 | data_type = "phase", taus = tau[:-3]) 75 | 76 | 77 | # ADEV can also be computed from frequency measurements 78 | # Uncomment to test 79 | # clk_freq = np.diff(clk_corr) / Ts 80 | # t, ad, ade, adn = at.oadev( clk_corr, rate = rate, \ 81 | # data_type = "phase", taus = tau[:-3]) 82 | 83 | # finally plot the result 84 | ax.loglog(t, ad, "-.", label = f"{gnss_ids[gnss]}{sat}") 85 | 86 | # Some cosmetics for the plots 87 | # ax.tick_params(axis='x', labelrotation = 45) 88 | ax.set_xlabel("Averaging Interval [s]", fontsize = fsize) 89 | ax.set_ylabel("ADEV", fontsize = fsize) 90 | ax.legend(loc=(1.05, 0), fontsize = 14, ncol=2) 91 | ax.autoscale(tight=True) 92 | ax.set_title(f"{gnss_spell[gnss]}", fontsize = fsize) 93 | plt.tight_layout() 94 | 95 | # Save as pdf 96 | plt.savefig(f"{fname}_{gnss_spell[gnss]}_adev.png", bbox_inches="tight") 97 | 98 | if __name__ == "__main__": 99 | 100 | # Set the input file name 101 | filename = "SEPT293_GALRawCNAV.zip_has_clk.csv" 102 | clk_adev( filename ) -------------------------------------------------------------------------------- /plot/plot_all.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Thu Feb 2 11:59:27 2023 5 | 6 | @author: daniele 7 | """ 8 | 9 | from plot_orb import plot_orb 10 | from plot_clk import plot_clk 11 | from plot_cb import plot_cb 12 | from plot_cp import plot_cp 13 | from clk_adev import clk_adev 14 | 15 | 16 | basename = "SEPT271k.22__has" 17 | 18 | # Orbits 19 | filename = basename + "_orb.csv" 20 | plot_orb(filename) 21 | 22 | # Clocks 23 | filename = basename + "_clk.csv" 24 | plot_clk(filename) 25 | clk_adev(filename) 26 | 27 | # Code bias 28 | filename = basename + "_cb.csv" 29 | plot_cb(filename) 30 | 31 | # Carrier phase bias 32 | filename = basename + "_cp.csv" 33 | plot_cp(filename) 34 | 35 | -------------------------------------------------------------------------------- /plot/plot_cb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jan 23 13:10:31 2023 5 | 6 | @author: daniele 7 | """ 8 | 9 | import pandas as pd 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | import matplotlib as mp 14 | 15 | 16 | def plot_cb(filename, sig_off = 0) : 17 | 18 | fsize = 16 19 | mp.rc('xtick', labelsize=fsize) 20 | mp.rc('ytick', labelsize=fsize) 21 | 22 | 23 | plt.style.use('ggplot') 24 | gnss_ids = ["G", "", "E"] 25 | gnss_spell = ["GPS", "", "Galileo"] 26 | gnss_ind = [1, -1, 0] 27 | 28 | # Signal table - indentifies the different signals 29 | signal_table = [["E1-B I/NAV OS", "L1 C/A"], 30 | ["E1-C", ""], 31 | ["E1-B + E1-C", ""], 32 | ["E5a-I F/NAV OS", "L1C(D)"], 33 | ["E5a-Q", "L1C(P)"], 34 | ["E5a-I+E5a-Q", "L1C(D+P)"], 35 | ["E5b-I I/NAV OS", "L2 CM"], 36 | ["E5b-Q", "L2 CL"], 37 | ["E5b-I+E5b-Q", "L2 CM+CL"], 38 | ["E5-I", "L2 P"], 39 | ["E5-Q", "Reserved"], 40 | ["E5-I + E5-Q", "L5 I"], 41 | ["E6-B C/NAV HAS", "L5 Q"], 42 | ["E6-C", "L5 I + L5 Q"], 43 | ["E6-B + E6-C", ""], 44 | ["", ""]] 45 | 46 | # load the csv file with the code bias corrections 47 | fname = filename[:filename.rfind(".")] 48 | df = pd.read_csv(filename) 49 | 50 | # determine which GNSS is present in the correction file 51 | GNSS = np.unique(df["gnssID"].values) 52 | 53 | # make different plots for each gnss 54 | for gnss in GNSS : 55 | # filter by gnss 56 | gnss_df = df[df["gnssID"] == gnss] 57 | 58 | # determine the number of unique signals in the file 59 | signals = np.unique(gnss_df["signal"].values) 60 | 61 | # Create a new plot with as many subplots as signals 62 | fig, ax = plt.subplots(nrows=len(signals), ncols = 1) 63 | fig.set_size_inches((8, 8)) 64 | 65 | # Loop on the different signals 66 | for ii, signal in enumerate(signals) : 67 | signal_df = gnss_df[gnss_df["signal"] == signal] 68 | 69 | # Determine the list of prn 70 | sats = np.unique(signal_df["PRN"].values) 71 | 72 | # Make a plot for each satellite 73 | for sat in sats : 74 | sat_df = signal_df[signal_df["PRN"] == sat] 75 | 76 | # build the time index 77 | time = np.floor( sat_df["ToW"].values / 3600 ) * 3600 + sat_df["ToH"].values 78 | 79 | # build the code bias 80 | code_bias = sat_df["code_bias"].values 81 | 82 | # remove duplicated values 83 | time, ind = np.unique(time, return_index = True) 84 | code_bias = code_bias[ind] 85 | 86 | # finally plot the result 87 | ax[ii].plot(time, code_bias, ".--", label = f"{gnss_ids[gnss]}{sat}") 88 | 89 | ax[ii].set_ylabel("Code bias [m]", fontsize = fsize) 90 | ax[ii].autoscale(tight=True) 91 | ax[ii].set_title(f"{gnss_spell[gnss]} - {signal_table[signal - sig_off][gnss_ind[gnss]]}", fontsize = fsize) 92 | if ax[ii] != ax[-1] : 93 | ax[ii].set_xticks([]) 94 | 95 | 96 | # Some cosmetics for the plots 97 | ax[-1].tick_params(axis='x', labelrotation = 45) 98 | ax[-1].set_xlabel("Time of Week [s]", fontsize = fsize) 99 | ax[-1].legend(loc=(1.05, 0), fontsize = 14, ncol=2) 100 | 101 | # plt.tight_layout() 102 | 103 | # Save as pdf 104 | plt.savefig(f"{fname}_{gnss_spell[gnss]}.png", bbox_inches="tight") 105 | 106 | if __name__ == "__main__": 107 | 108 | # Set the input file name 109 | filename = "SEPT293_GALRawCNAV.zip_has_cb.csv" 110 | plot_cb( filename, 1 ) -------------------------------------------------------------------------------- /plot/plot_clk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jan 23 13:10:31 2023 5 | 6 | @author: daniele 7 | """ 8 | 9 | import pandas as pd 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | import matplotlib as mp 14 | 15 | 16 | 17 | def plot_clk( filename ) : 18 | 19 | fsize = 16 20 | mp.rc('xtick', labelsize=fsize) 21 | mp.rc('ytick', labelsize=fsize) 22 | 23 | 24 | plt.style.use('ggplot') 25 | gnss_ids = ["G", "", "E"] 26 | gnss_spell = ["GPS", "", "Galileo"] 27 | 28 | # load the csv file with the clock corrections 29 | fname = filename[:filename.rfind(".")] 30 | df = pd.read_csv(filename) 31 | 32 | # determine which GNSS is present in the correction file 33 | GNSS = np.unique(df["gnssID"].values) 34 | 35 | # make different plots for each gnss 36 | for gnss in GNSS : 37 | # filter by gnss 38 | gnss_df = df[df["gnssID"] == gnss] 39 | 40 | # Create a new plot 41 | fig, ax = plt.subplots() 42 | fig.set_size_inches((12, 8)) 43 | # Determine the list of prn 44 | sats = np.unique(gnss_df["PRN"].values) 45 | 46 | # Make a plot for each satellite 47 | for sat in sats : 48 | sat_df = gnss_df[gnss_df["PRN"] == sat] 49 | 50 | # build the time index 51 | time = np.floor( sat_df["ToW"].values / 3600 ) * 3600 + sat_df["ToH"].values 52 | 53 | # build the clock correction 54 | clk_corr = sat_df["delta_clock_c0"].values * sat_df["multiplier"].values 55 | 56 | # remove duplicated values 57 | time, ind = np.unique(time, return_index = True) 58 | clk_corr = clk_corr[ind] 59 | 60 | # finally plot the result 61 | ax.plot(time, clk_corr, ".", label = f"{gnss_ids[gnss]}{sat}") 62 | 63 | # Some cosmetics for the plots 64 | ax.tick_params(axis='x', labelrotation = 45) 65 | ax.set_xlabel("Time of Week [s]", fontsize = fsize) 66 | ax.set_ylabel("Clock Corrections [m]", fontsize = fsize) 67 | ax.legend(loc=(1.05, 0), fontsize = 14, ncol=2) 68 | ax.autoscale(tight=True) 69 | ax.set_title(f"{gnss_spell[gnss]}", fontsize = fsize) 70 | plt.tight_layout() 71 | 72 | # Save as pdf 73 | plt.savefig(f"{fname}_{gnss_spell[gnss]}.png", bbox_inches="tight") 74 | 75 | if __name__ == "__main__": 76 | 77 | # Set the input file name 78 | filename = "SEPT293_GALRawCNAV.zip_has_clk.csv" 79 | plot_clk( filename ) 80 | -------------------------------------------------------------------------------- /plot/plot_cp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jan 23 13:10:31 2023 5 | 6 | @author: daniele 7 | """ 8 | 9 | import pandas as pd 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | import matplotlib as mp 14 | 15 | def plot_cp(filename, sig_off = 0) : 16 | 17 | fsize = 16 18 | mp.rc('xtick', labelsize=fsize) 19 | mp.rc('ytick', labelsize=fsize) 20 | 21 | 22 | plt.style.use('ggplot') 23 | gnss_ids = ["G", "", "E"] 24 | gnss_spell = ["GPS", "", "Galileo"] 25 | gnss_ind = [1, -1, 0] 26 | 27 | # Signal table - indentifies the different signals 28 | signal_table = [["E1-B I/NAV OS", "L1 C/A"], 29 | ["E1-C", ""], 30 | ["E1-B + E1-C", ""], 31 | ["E5a-I F/NAV OS", "L1C(D)"], 32 | ["E5a-Q", "L1C(P)"], 33 | ["E5a-I+E5a-Q", "L1C(D+P)"], 34 | ["E5b-I I/NAV OS", "L2 CM"], 35 | ["E5b-Q", "L2 CL"], 36 | ["E5b-I+E5b-Q", "L2 CM+CL"], 37 | ["E5-I", "L2 P"], 38 | ["E5-Q", "Reserved"], 39 | ["E5-I + E5-Q", "L5 I"], 40 | ["E6-B C/NAV HAS", "L5 Q"], 41 | ["E6-C", "L5 I + L5 Q"], 42 | ["E6-B + E6-C", ""], 43 | ["", ""]] 44 | 45 | # load the csv file with the code bias corrections 46 | fname = filename[:filename.rfind(".")] 47 | df = pd.read_csv(filename) 48 | 49 | # determine which GNSS is present in the correction file 50 | GNSS = np.unique(df["gnssID"].values) 51 | 52 | # make different plots for each gnss 53 | for gnss in GNSS : 54 | # filter by gnss 55 | gnss_df = df[df["gnssID"] == gnss] 56 | 57 | # determine the number of unique signals in the file 58 | signals = np.unique(gnss_df["signal"].values) 59 | 60 | # Create a new plot with as many subplots as signals 61 | fig, ax = plt.subplots(nrows=len(signals), ncols = 1) 62 | fig.set_size_inches((8, 8)) 63 | 64 | # Loop on the different signals 65 | for ii, signal in enumerate(signals) : 66 | signal_df = gnss_df[gnss_df["signal"] == signal] 67 | 68 | # Determine the list of prn 69 | sats = np.unique(signal_df["PRN"].values) 70 | 71 | # Make a plot for each satellite 72 | for sat in sats : 73 | sat_df = signal_df[signal_df["PRN"] == sat] 74 | 75 | # build the time index 76 | time = np.floor( sat_df["ToW"].values / 3600 ) * 3600 + sat_df["ToH"].values 77 | 78 | # build the code bias 79 | phase_bias = sat_df["phase_bias"].values 80 | 81 | # remove duplicated values 82 | time, ind = np.unique(time, return_index = True) 83 | phase_bias = phase_bias[ind] 84 | 85 | # finally plot the result 86 | ax[ii].plot(time, phase_bias, ".--", label = f"{gnss_ids[gnss]}{sat}") 87 | 88 | ax[ii].set_ylabel("Phase bias\n [cycles]", fontsize = fsize) 89 | ax[ii].autoscale(tight=True) 90 | ax[ii].set_title(f"{gnss_spell[gnss]} - {signal_table[signal - sig_off][gnss_ind[gnss]]}", fontsize = fsize) 91 | if ax[ii] != ax[-1] : 92 | ax[ii].set_xticks([]) 93 | 94 | 95 | # Some cosmetics for the plots 96 | ax[-1].tick_params(axis='x', labelrotation = 45) 97 | ax[-1].set_xlabel("Time of Week [s]", fontsize = fsize) 98 | ax[-1].legend(loc=(1.05, 0), fontsize = 14, ncol=2) 99 | 100 | # plt.tight_layout() 101 | 102 | # Save as pdf 103 | plt.savefig(f"{fname}_{gnss_spell[gnss]}.png", bbox_inches="tight") 104 | 105 | 106 | if __name__ == "__main__": 107 | 108 | # Set the input file name 109 | filename = "SEPT293_GALRawCNAV.zip_has_cp.csv" 110 | plot_cp( filename, 1 ) -------------------------------------------------------------------------------- /plot/plot_orb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Jan 23 14:34:40 2023 5 | 6 | @author: daniele 7 | """ 8 | 9 | import pandas as pd 10 | import matplotlib.pyplot as plt 11 | import numpy as np 12 | 13 | import matplotlib as mp 14 | 15 | 16 | def plot_orb(filename) : 17 | 18 | fsize = 16 19 | mp.rc('xtick', labelsize=fsize) 20 | mp.rc('ytick', labelsize=fsize) 21 | 22 | 23 | plt.style.use('ggplot') 24 | gnss_ids = ["G", "", "E"] 25 | gnss_spell = ["GPS", "", "Galileo"] 26 | 27 | # load the csv file with the clock corrections 28 | fname = filename[:filename.rfind(".")] 29 | df = pd.read_csv(filename) 30 | 31 | # determine which GNSS is present in the correction file 32 | GNSS = np.unique(df["gnssID"].values) 33 | 34 | # make different plots for each gnss 35 | for gnss in GNSS : 36 | # filter by gnss 37 | gnss_df = df[df["gnssID"] == gnss] 38 | 39 | # Create a new plot 40 | fig, ax = plt.subplots(nrows=3, ncols=1, sharex = True) 41 | fig.set_size_inches((14, 9)) 42 | # Determine the list of prn 43 | sats = np.unique(gnss_df["PRN"].values) 44 | 45 | # Make a plot for each satellite 46 | for sat in sats : 47 | sat_df = gnss_df[gnss_df["PRN"] == sat] 48 | 49 | # build the time index 50 | time = np.floor( sat_df["ToW"].values / 3600 ) * 3600 + sat_df["ToH"].values 51 | 52 | # extract the corrections 53 | radial = sat_df["delta_radial"].values 54 | in_track = sat_df["delta_in_track"].values 55 | cross_track = sat_df["delta_cross_track"].values 56 | 57 | # remove duplicated values 58 | time, ind = np.unique(time, return_index = True) 59 | radial = radial[ind] 60 | in_track = in_track[ind] 61 | cross_track = cross_track[ind] 62 | 63 | # finally plot the result 64 | ax[0].plot(time, radial, ".", label = f"{gnss_ids[gnss]}{sat}") 65 | ax[1].plot(time, in_track, ".", label = f"{gnss_ids[gnss]}{sat}") 66 | ax[2].plot(time, cross_track, ".", label = f"{gnss_ids[gnss]}{sat}") 67 | 68 | # Some cosmetics for the plots 69 | ax[2].tick_params(axis='x', labelrotation = 45) 70 | ax[2].set_xlabel("Time of Week [s]", fontsize = fsize) 71 | 72 | ax[0].set_ylabel("Radial [m]", fontsize = fsize) 73 | ax[1].set_ylabel("In Track [m]", fontsize = fsize) 74 | ax[2].set_ylabel("Cross Track [m]", fontsize = fsize) 75 | 76 | ax[2].legend(loc=(1.05, 0), fontsize = 14, ncol=2) 77 | 78 | ax[0].autoscale(tight=True) 79 | ax[1].autoscale(tight=True) 80 | ax[2].autoscale(tight=True) 81 | 82 | ax[0].set_title(f"{gnss_spell[gnss]}", fontsize = fsize) 83 | 84 | # plt.tight_layout() 85 | 86 | # Save as pdf 87 | plt.savefig(f"{fname}_{gnss_spell[gnss]}.png", bbox_inches="tight") 88 | 89 | if __name__ == "__main__": 90 | 91 | # Set the input file name 92 | filename = "SEPT293_GALRawCNAV.zip_has_orb.csv" 93 | plot_orb( filename ) -------------------------------------------------------------------------------- /process_cnav.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on 31st May 2021 5 | 6 | @author: 7 | Daniele Borio 8 | """ 9 | 10 | import numpy as np 11 | import data_loading as dl 12 | import has_decoder as hd 13 | 14 | # Import the right library depending on the environment 15 | import sys 16 | if 'ipykernel_launcher.py' in sys.argv[0] : 17 | from tqdm.notebook import tqdm 18 | else : 19 | from tqdm import tqdm 20 | 21 | 22 | def parse_data( filename, _rx, _type = None, _page_offset = 1) : 23 | 24 | """ 25 | Summary : 26 | Main function performing the actual parsing. 27 | It calls several objects in carge of the different operations. 28 | 29 | Arguments: 30 | filename - string specifying the path name of the file to be parsed 31 | _rx - specify the receiver used to generate the input file. The following 32 | options are currently supported: 33 | 34 | sep - Septentrio receiver 35 | nov - Novatel GALCNAVRAWPAGE - ASCII format 36 | jav - Javad ED message 37 | 38 | _type - in the case of a Septentrio receiver, different input data formats can 39 | be used. In particular, Binary SBF files are first parsed to 40 | extract the Galileo CNAV message. Depending on the parser version, 41 | hexadecimal or decimal data input can be found. 42 | The options supported are: 43 | hexa - hexadecimal format 44 | txt - decimal format 45 | 46 | _page_offset - during the initial HAS test phase, page numbering was starting from 1, now 47 | it is starting from 0. The page offset allows one to account for this 48 | convention. It should be set to 1 unless data from the very initial test phase 49 | are used. 50 | """ 51 | print("Process started") 52 | 53 | if _rx == "sep" : 54 | 55 | if _type == "bin" : 56 | df = dl.load_from_binary_Septentrio(filename) 57 | else : 58 | df = dl.load_from_parsed_Septentrio(filename, _type) 59 | 60 | elif _rx == "nov" : 61 | df = dl.load_from_Novatel(filename) 62 | 63 | elif _rx == "jav" : 64 | df = dl.load_from_Javad(filename) 65 | 66 | else : 67 | raise Exception("Unsupported Receiver format") 68 | 69 | print("Data loaded ...\n") 70 | 71 | # Now compute the HAS page type (bits from 14 to 38) 72 | HAS_Header = ( (df["word 1"].values & 0x3FFFF) << 6 ) + \ 73 | (df["word 2"].values >> 26) 74 | 75 | # Find non-dummy elements 76 | non_dummy = np.argwhere((HAS_Header != 0xAF3BC3) & (df["CRCPassed"].values == 1)).flatten() 77 | 78 | # Select only non-dummy 79 | df_valid = df.iloc[non_dummy].copy() 80 | 81 | 82 | 83 | HAS_Header = HAS_Header[non_dummy] 84 | 85 | # Extract valid information from the header 86 | df_valid["HAS_status"] = (HAS_Header >> 22) & 0x3 87 | df_valid["Message_Type"] = ( HAS_Header >> 18 ) & 0x3 88 | df_valid["Message_ID"] = ( HAS_Header >> 13 ) & 0x1F 89 | df_valid["Message_Size"] = (( HAS_Header >> 8 ) & 0x1F) + 1 90 | df_valid["Page_ID"] = (HAS_Header) & 0xFF 91 | 92 | # Allocate the decoder 93 | decoder = hd.has_decoder(_page_offset) 94 | 95 | valid_tows = np.unique(df_valid['TOW']) 96 | 97 | masks = None 98 | 99 | # different files where to save the different corrections 100 | orbit_filename = filename.split('__')[0] + '_has_orb.csv' 101 | clock_filename = filename.split('__')[0] + '_has_clk.csv' 102 | cbias_filename = filename.split('__')[0] + '_has_cb.csv' 103 | pbias_filename = filename.split('__')[0] + '_has_cp.csv' 104 | 105 | orbit_file = open(orbit_filename, 'w') 106 | clock_file = open(clock_filename, 'w') 107 | cbias_file = open(cbias_filename, 'w') 108 | pbias_file = open(pbias_filename, 'w') 109 | 110 | orbit_header = False 111 | clock_header = False 112 | cbias_header = False 113 | pbias_header = False 114 | 115 | # Masks have to be propagated between different loops 116 | masks = None 117 | 118 | for hh in tqdm(range(len(valid_tows))) : 119 | 120 | tow = valid_tows[hh] 121 | 122 | tow_ind = np.argwhere(df_valid['TOW'].values == tow).flatten() 123 | 124 | 125 | page_block = [] 126 | for ii in tow_ind : 127 | page = np.array([df_valid["word %d" % kk].iloc[ii] for kk in range(1, 17)], dtype=np.uint32) 128 | page_block.append(page) 129 | 130 | msg_type = df_valid['Message_Type'].iloc[tow_ind[0]] 131 | msg_id = df_valid['Message_ID'].iloc[tow_ind[0]] 132 | msg_size = df_valid['Message_Size'].iloc[tow_ind[0]] 133 | 134 | # also get the week number 135 | week = df_valid['WNc [w]'].iloc[tow_ind[0]] 136 | 137 | msg = decoder.update(tow, page_block, msg_type, msg_id, msg_size) 138 | 139 | 140 | if msg is not None : 141 | header = decoder.interpret_mt1_header(msg.flatten()[0:4]) 142 | 143 | info = {'ToW' : int(tow), 144 | 'WN' : week, 145 | 'ToH' : header['TOH'], 146 | 'IOD' : header['IOD Set ID']} 147 | # Check on timing information 148 | # if (info['ToW'] % 3600) < info['ToH'] : 149 | # print('Invalid ToH') 150 | # continue 151 | 152 | body = msg.flatten()[4:] 153 | byte_offset = 0 154 | bit_offset = 0 155 | 156 | if header["Mask"] == 1 : 157 | try : 158 | masks, byte_offset, bit_offset = decoder.interpret_mt1_mask(body) 159 | except : 160 | continue 161 | 162 | if masks is None : 163 | continue 164 | 165 | if header['Orbit Corr'] == 1 : 166 | try : 167 | cors, byte_offset, bit_offset = decoder.interpret_mt1_orbit_corrections(body,\ 168 | byte_offset, bit_offset, masks, info) 169 | except : 170 | continue 171 | 172 | if len(cors) == 0 : 173 | continue 174 | 175 | # print the corrections to file 176 | if not orbit_header : 177 | orbit_file.write(cors[0].get_header() + '\n') 178 | orbit_header = True 179 | 180 | for cor in cors : 181 | cor_str = cor.__str__() + '\n' 182 | orbit_file.write(cor_str) 183 | 184 | if header['Clock Full-set'] == 1 : 185 | try : 186 | cors, byte_offset, bit_offset = decoder.interpret_mt1_full_clock_corrections(body,\ 187 | byte_offset, bit_offset, masks, info) 188 | except : 189 | continue 190 | 191 | if len(cors) == 0 : 192 | continue 193 | 194 | # print the corrections to file 195 | if not clock_header : 196 | clock_file.write(cors[0].get_header() + '\n') 197 | clock_header = True 198 | 199 | for cor in cors : 200 | cor_str = cor.__str__() + '\n' 201 | clock_file.write(cor_str) 202 | 203 | if header['Clock Subset'] == 1 : 204 | # This block needs an "external" mask 205 | if masks is not None : 206 | try : 207 | cors, byte_offset, bit_offset = decoder.interpret_mt1_subset_clock_corrections(body,\ 208 | byte_offset, bit_offset, masks, info) 209 | except : 210 | continue 211 | 212 | if len(cors) == 0 : 213 | continue 214 | 215 | # print the corrections to file 216 | if not clock_header : 217 | clock_file.write(cors[0].get_header() + '\n') 218 | clock_header = True 219 | 220 | for cor in cors : 221 | cor_str = cor.__str__() + '\n' 222 | clock_file.write(cor_str) 223 | 224 | if header['Code Bias'] == 1 : 225 | try : 226 | cors, byte_offset, bit_offset = decoder.interpret_mt1_code_biases(body,\ 227 | byte_offset, bit_offset, masks, info) 228 | except : 229 | continue 230 | 231 | if len(cors) == 0 : 232 | continue 233 | 234 | # print the corrections to file 235 | if not cbias_header : 236 | cbias_file.write(cors[0].get_header() + '\n') 237 | cbias_header = True 238 | 239 | for cor in cors : 240 | if cor.is_empty() : 241 | continue 242 | 243 | cor_str = cor.__str__() + '\n' 244 | cbias_file.write(cor_str) 245 | 246 | if header['Phase Bias'] == 1 : 247 | try: 248 | cors, byte_offset, bit_offset = decoder.interpret_mt1_phase_biases(body,\ 249 | byte_offset, bit_offset, masks, info) 250 | except: 251 | continue 252 | 253 | if len(cors) == 0 : 254 | continue 255 | 256 | # print the corrections to file 257 | if not pbias_header : 258 | pbias_file.write(cors[0].get_header() + '\n') 259 | pbias_header = True 260 | 261 | for cor in cors : 262 | if cor.is_empty() : 263 | continue 264 | 265 | cor_str = cor.__str__() + '\n' 266 | pbias_file.write(cor_str) 267 | 268 | orbit_file.close() 269 | clock_file.close() 270 | cbias_file.close() 271 | pbias_file.close() 272 | 273 | 274 | if __name__ == "__main__": 275 | 276 | # Set the input file name 277 | filename = "../data/SEPT267.sbf" 278 | 279 | # Receiver options 280 | # _rx = "sep" 281 | # _rx = "jav" 282 | _rx = "sep" 283 | 284 | 285 | # If Septentrio is selected, different options are supported 286 | # depending on the Septentrio parser 287 | 288 | # Not relevant for other receivers 289 | # _type = "txt" 290 | # _type = "hexa" 291 | _type = "bin" 292 | 293 | # Should always be set to 1 294 | _page_offset = 1 295 | 296 | # Finally parse the data 297 | parse_data( filename, _rx, _type, _page_offset ) 298 | 299 | -------------------------------------------------------------------------------- /reed_solomon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on 5 | 6 | @author: 7 | """ 8 | 9 | import galois 10 | import numpy as np 11 | 12 | def get_poly_gen( gf2m, k ) : 13 | 14 | """ 15 | Summary: 16 | Funtion that produces the generating polynomial for Reed-Solomon 17 | encoding 18 | 19 | Arguments: 20 | gf2m - struct containing the elements for the different operations 21 | in GF(2^m) 22 | k - number of input words 23 | 24 | Returns: 25 | polygen - vector containing the coefficients in GF(2^m) of the 26 | generating polynomial 27 | 28 | History: 29 | Mar 27/19 - Function created by Daniele Borio. 30 | Jun 31 | Remarks: 32 | 1) polygen will be used to generate a RS( 2^m - 1, k ) code 33 | 2) polygen is obtained by considering consecutive powers of alpha, 34 | the generating elements 35 | """ 36 | 37 | n = gf2m.order - 1 38 | 39 | # Degree of the generating polynomial 40 | pdeg = n - k 41 | 42 | # allocate the first term of the generating polynomial 43 | pgen = gf2m.Ones(2) 44 | 45 | # Initialize the polynomial as (z + alpha) 46 | pgen[0] = gf2m.primitive_element 47 | 48 | polygen = galois.Poly(pgen.copy(), order="asc") 49 | 50 | # Now perform the multiplications by (z + alpha^ii) 51 | for ii in range(pdeg - 1) : 52 | pgen[0] *= gf2m.primitive_element 53 | p = galois.Poly(pgen, order="asc") 54 | 55 | polygen *= p 56 | 57 | return polygen 58 | 59 | def rs_encode( gf2m, message, polygen, order = "asc" ) : 60 | """ 61 | Summary: 62 | Function that encodes in a systematic way a message using the 63 | Reed-Solomon code defined by polygen on GF(2^m) 64 | 65 | Arguments: 66 | gf2m - struct containing the elements for the different operations 67 | in GF(2^m) 68 | message - vector containing the message to encode 69 | polygen - the generating polynomial 70 | 71 | Returns: 72 | cwords - the encoded message 73 | """ 74 | 75 | # first check that all the elements are compatible 76 | msg_len = gf2m.order - len(polygen.coeffs) 77 | 78 | if msg_len != len(message) : 79 | print("Wrong message length, it should be %d" % msg_len) 80 | return None 81 | 82 | # allocate the encoded message 83 | if order == "asc" : 84 | coeffs = np.concatenate((np.zeros(len(polygen.coeffs) - 1, dtype=type(message[0])), message)) 85 | else : 86 | coeffs = np.concatenate((message, np.zeros(len(polygen.coeffs) - 1, dtype=type(message[0])))) 87 | 88 | msg_poly = galois.Poly(coeffs, order=order, field=gf2m) 89 | 90 | # get the reminder 91 | rpol = msg_poly % polygen 92 | 93 | # add the reminder with the generating polynomial 94 | msg_poly += rpol 95 | 96 | return msg_poly.coeffs 97 | 98 | def get_encoding_matrix( gf2m, polygen ) : 99 | 100 | # Build the generating matrix from 'polygen' 101 | n = gf2m.order - 1 102 | k = gf2m.order - len(polygen.coeffs) 103 | 104 | G = gf2m.Zeros( (n, k) ) 105 | 106 | gcol = gf2m.Zeros( n ) 107 | gcol[:(n-k+1)] = np.flip(polygen.coeffs) 108 | 109 | for ii in range(k) : 110 | G[:, ii] = np.roll(gcol, ii) 111 | 112 | 113 | Gk = G[(n-k):n,:] 114 | InvGk = np.linalg.inv( Gk ) 115 | 116 | # Finally find H 117 | H = gf2m.Zeros( G.shape ) 118 | 119 | for ii in range(k) : 120 | H[n - k + ii, ii] = 1 121 | 122 | H[:(n-k),:] = G[:(n-k),:] @ InvGk 123 | 124 | return H 125 | 126 | -------------------------------------------------------------------------------- /test_rd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Sat Jun 12 13:18:29 2021 5 | 6 | @author: daniele 7 | """ 8 | 9 | import galois 10 | import reed_solomon as rd 11 | import numpy as np 12 | 13 | 14 | gf2m = galois.GF(2**8) 15 | k = 32 16 | 17 | p = rd.get_poly_gen( gf2m, k ) 18 | 19 | # Coefficients can be accessed as 20 | # p.coeffs 21 | 22 | message = np.array([71, 12, 25, 210, 178, 81, 243, 9, 112, 98, 196, 203, 48, 125, 23 | 114, 165, 181, 193, 71, 174, 168, 42, 31, 128, 245, 87, 150, 24 | 58, 192, 66, 130, 179], dtype = np.uint8) 25 | 26 | enc_msg = rd.rs_encode( gf2m, message, p, "desc" ) 27 | 28 | H = rd.get_encoding_matrix(gf2m, p) 29 | 30 | enc_msg2 = np.flip(H @ gf2m(np.flip(message))) 31 | 32 | H1 = np.fliplr(np.flipud(H)) 33 | 34 | # print the matrix to file 35 | fout = open("has_encoding_matrix.csv", "w") 36 | 37 | for ii in range(H1.shape[0]) : 38 | for jj in range(H1.shape[1]) : 39 | if jj != 0 : 40 | fout.write(',') 41 | fout.write(str(int(H1[ii,jj]))) 42 | fout.write('\n') 43 | 44 | fout.close() -------------------------------------------------------------------------------- /timefun.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Fri Jan 31 08:43:00 2018 4 | 5 | @author: daniele 6 | """ 7 | 8 | import math 9 | import numpy as np 10 | import pandas as pd 11 | 12 | def datetimeToGps( time ) : 13 | val = pd.to_datetime(time) 14 | 15 | year = val.year 16 | month = val.month 17 | day = val.day 18 | hours = val.hour 19 | minutes = val.minute 20 | seconds = val.second 21 | 22 | tow, gpsweek = DateToGPS( year, month, day, hours ) 23 | tow += minutes * 60 + seconds 24 | 25 | return tow, gpsweek 26 | 27 | def vecDT2Gps( time ) : 28 | return np.vectorize(datetimeToGps)(time) 29 | 30 | """ 31 | DateToMjd - Converts a date into the modified julian day 32 | 33 | input: 34 | year, month, day and hour 35 | 36 | output: 37 | the mjd - modified julian day 38 | 39 | """ 40 | def DateToMjd( year, month, day, hour ): 41 | 42 | # year, month and day are considered as unsigned integers 43 | # hour can be a float 44 | if month <= 2: 45 | year -= 1 46 | month += 12 47 | 48 | p1 = math.floor( 365.25 * ( float( year ) + 4716.0 ) ) 49 | p2 = math.floor( 30.6001 * ( float( month ) + 1 ) ) 50 | 51 | # compute the julian day 52 | julian = p1 + p2 + float(day) + float(hour) / 24 - 1537.5 53 | 54 | mdj = julian - 2400000.5 55 | 56 | return mdj 57 | 58 | """ 59 | DateToGps - Converts a date into GPS week and GPS tow 60 | 61 | input: 62 | year, month, day and hour 63 | 64 | output: 65 | GpsWeek - the GPS week 66 | tow - the time of week 67 | 68 | """ 69 | def DateToGPS( year, month, day, hour ): 70 | # First get the mdj 71 | mdj = DateToMjd( year, month, day, hour ) 72 | 73 | # Compute the 'GPS days' 74 | gpsDays = mdj - DateToMjd( 1980, 1, 6, 0.0 ) 75 | 76 | # Get the GPS week 77 | GpsWeek = int( gpsDays / 7 ) 78 | 79 | # Finally compute the Tow 80 | tow = int(( gpsDays - 7 * GpsWeek ) * 86400 + 0.5) 81 | 82 | return tow, GpsWeek 83 | 84 | """ 85 | GpsToDate - Converts GPS week and GPS tow into a date 86 | 87 | input: 88 | GpsWeek - the GPS week 89 | tow - the time of week 90 | 91 | output: 92 | year, month, day and hour 93 | """ 94 | def GpsToDate( GpsWeek, GpsSeconds ) : 95 | mjd = GpsToMjd( GpsWeek, GpsSeconds ) 96 | year, month, day, hour = MjdToDate( mjd ) 97 | 98 | return year, month, day, hour 99 | 100 | """ 101 | Converts the modified Julian day into a date 102 | """ 103 | def MjdToDate( Mjd ) : 104 | 105 | # Get the Julian day from the modified julian day 106 | jd = Mjd + 2400000.5; 107 | 108 | # Take only the integer part (integer Julian day) 109 | jdi = int( jd ) 110 | 111 | # Fractional part of the day 112 | jdf = jd - float( jdi ) + 0.5; 113 | 114 | # Really the next calendar day? 115 | if jdf >= 1.0 : 116 | jdf = jdf - 1 117 | jdi = jdi + 1 118 | 119 | # Extract the hour from the fractional part of the day 120 | hour = jdf * 24.0 121 | l = int( jdi + 68569 ) 122 | n = ( 4 * l ) / 146097 123 | 124 | l = l - ((146097 * n + 3) / 4) 125 | year = int(4000 * (l + 1) ) / 1461001 126 | 127 | l = l - (1461 * year ) / 4 + 31 128 | month = int(80 * l ) / 2447 129 | 130 | day = l - (2447 * month) / 80 131 | 132 | l = month / 11 133 | 134 | month = month + 2 - 12 * l 135 | year = 100 * (n - 49) + year + l 136 | 137 | return year, month, day, hour 138 | 139 | """ 140 | Convert GPS Week and Seconds to Modified Julian Day. 141 | Ignores UTC leap seconds. 142 | """ 143 | 144 | def GpsToMjd ( GpsWeek, GpsSeconds ) : 145 | 146 | 147 | GpsDays = 7 * float( GpsWeek ) + ( GpsSeconds / 86400) 148 | 149 | Mjd = DateToMjd( 1980, 1, 6, 0 ) + GpsDays 150 | 151 | return Mjd 152 | 153 | --------------------------------------------------------------------------------