├── 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 |
--------------------------------------------------------------------------------