diff --git a/HAS_Converter.py b/HAS_Converter.py index 9eb9478..d09aa26 100644 --- a/HAS_Converter.py +++ b/HAS_Converter.py @@ -10,7 +10,7 @@ def printHelp(): print("-t arg : Target stream to decode messages to") print("-f opt : Format to convert HAS messages to. Options are [1:IGS, 2:RTCM3]") print("-i opt : Input mode, specifying the type of input stream. Options are :", - "\n [1:SBF File, 2:BINEX File, 3:SBF Serial, 4:BINEX Serial, 5:SBF TCP, 6:BINEX TCP]") + "\n [1:SBF File, 2:BINEX File, 3:SBF Serial, 4:BINEX Serial, 5:SBF TCP, 6:BINEX TCP, 7:NOV-A File]") print("-o opt : Output mode, specifying the type of output stream. Options are:", "\n [1:TCP, 2:File, 3:PPPWiz File, 4:PPPWiz Stream]") print("-p arg : Optional for TCP output. If not set, uses port 6947") diff --git a/README.md b/README.md index a44ee3d..b5ab773 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Parameters for the `HAS_Converter` are as follows: * `source`: The source. Can be a filename/path or portname. * `target`: The output target. Can be a filename/path or an IP address for a TCP server. * `out_format`: The format of the output. Options are [1:IGS, 2:RTCM3] -* `modeIn`: Optional. Determining the mode of input. If not set, looks for file endings. Options are [1:SBF File, 2:BINEX File, 3:SBF Serial, 4:BINEX Serial, 5:SBF TCP, 6:BINEX TCP] +* `modeIn`: Optional. Determining the mode of input. If not set, looks for file endings. Options are [1:SBF File, 2:BINEX File, 3:SBF Serial, 4:BINEX Serial, 5:SBF TCP, 6:BINEX TCP, 7:Novatel ASCII] * `modeOut`: Optional. Determining the mode of output. If not set, decides based on all-numeric IP addresses (excl. dots)/localhost or not. Options are: [1:TCP, 2:File, 3:PPPWiz File, 4:PPPWiz Stream] * `x`: Optional parameter. Used to indicate a maximum number of navigation messages to read. This includes all GNSS messages and is not limited to Galileo HAS messages. * `port`: Optional parameter for TCP output. If not set, uses port 6947 @@ -64,7 +64,7 @@ This command sets up a decoder with exactly the same settings as shown in the pr * -s arg : Source stream to decode messages from * -t arg : Target stream to decode messages to * -f opt : Format to convert HAS messages to. Options are [1:IGS, 2:RTCM3] -* -i opt : Input mode, specifying the type of input stream. Options are : [1:SBF File, 2:BINEX File, 3:SBF Serial, 4:BINEX Serial, 5:SBF TCP, 6:BINEX TCP] +* -i opt : Input mode, specifying the type of input stream. Options are : [1:SBF File, 2:BINEX File, 3:SBF Serial, 4:BINEX Serial, 5:SBF TCP, 6:BINEX TCP, 7:Novatel ASCII] * -o opt : Output mode, specifying the type of output stream. Options are: [1:TCP, 2:File, 3:PPPWiz File, 4:PPPWiz Stream] * -p arg : Optional for TCP output. If not set, uses port 6947 * -b arg : Optional for serial input, specifying the baudrate of the stream. If not set, uses 115200 diff --git a/Tests/NovAsciiTestRecordings.zip b/Tests/NovAsciiTestRecordings.zip new file mode 100644 index 0000000..697b470 Binary files /dev/null and b/Tests/NovAsciiTestRecordings.zip differ diff --git a/Tests/ReadMe.md b/Tests/ReadMe.md index c5f9250..eed1dea 100644 --- a/Tests/ReadMe.md +++ b/Tests/ReadMe.md @@ -10,3 +10,7 @@ True location (converted from DMS) for FGI-Otaniemi Septentrio Antenna: * trueLat = 60.182260; * trueLong = 24.828537; * trueHeight = 47.248; + +NovAsciiTestRecordings.zip contains two test recordings that were collected at University of Vaasa campus with Novatel PwrPak7 receiver. +- gal_e6_raw_cnav_pages_2023-09-22_093700_UTC.txt with receiver firmware 7.08.10 +- gal_e6_raw_cnav_pages_2023-10-04_071500_UTC.txt with receiver firmware 7.09.00 diff --git a/galileo_has_decoder/conv.py b/galileo_has_decoder/conv.py index e6e5725..5932d08 100644 --- a/galileo_has_decoder/conv.py +++ b/galileo_has_decoder/conv.py @@ -5,6 +5,7 @@ VER DATE AUTHOR 1.0 09/12/2021 Oliver Horst / FGI +Modified 2023-09-29 Jaakko Yliaho / Uwasa to add NOV_Reader ''' from galileo_has_decoder.sbf_reading import SBF_Reader @@ -12,6 +13,7 @@ from galileo_has_decoder.tcp_sbf_reading import TCP_SBF_Reader from galileo_has_decoder.tcp_binex_reading import TCP_Binex_Reader from galileo_has_decoder.serial_reading import Serial_SBF_Reader, Serial_Binex_Reader +from galileo_has_decoder.nov_reading import NOV_Reader from galileo_has_decoder.tcp_server import TCP_Server from galileo_has_decoder.ssr_converter import SSR_Converter from galileo_has_decoder.file_write import File_Writer, PPP_Wiz_Writer @@ -66,6 +68,9 @@ def __init__(self, source, target, outFormat, modeIn=None, modeOut=None, port=No elif modeIn == 6: inp = "BINEX TCP stream on " + str(source) self.reader = TCP_Binex_Reader(source) + elif modeIn == 7: + inp = "Novatel GALCNAVRAWPAGE ASCII file" + self.reader = NOV_Reader(source, skip=float(skip)) #Target Initialization if modeOut == None: if target.replace(".", "").isnumeric() or target == 'localhost': @@ -110,7 +115,7 @@ def convertAll(self, compact=True, HRclk=False, lowerUDI=True, verbose=0): #Convert all messages available from the source if verbose != 0 and self.converter != None: self.converter.setVerbose(verbose) - if self.modeIn == 1 or self.modeIn == 2 or self.modeIn == 5 or self.modeIn == 6: + if self.modeIn == 1 or self.modeIn == 2 or self.modeIn == 5 or self.modeIn == 6 or self.modeIn == 7: self.reader.read(converter=self.converter, output=self.output, compact=compact, HRclk=HRclk, verbose=verbose) elif self.modeIn == 3 or self.modeIn == 4: self.reader.read(converter=self.converter, output=self.output, compact=compact, HRclk=HRclk, verbose=verbose) @@ -120,7 +125,7 @@ def convertX(self, x, compact=True, HRclk=False, lowerUDI=True, verbose=0): if verbose != 0 and self.converter != None: self.converter.setVerbose(verbose) #Convert X messages from the source - if self.modeIn == 1 or self.modeIn == 2 or self.modeIn == 5 or self.modeIn == 6: + if self.modeIn == 1 or self.modeIn == 2 or self.modeIn == 5 or self.modeIn == 6 or self.modeIn == 7: self.reader.read(converter=self.converter, output=self.output, x=x, compact=compact, HRclk=HRclk, verbose=verbose) elif self.modeIn == 3 or self.modeIn == 4: self.reader.read(converter=self.converter, output=self.output, x=x, compact=compact, HRclk=HRclk, verbose=verbose) @@ -130,7 +135,7 @@ def convertUntil(self, s, compact=True, HRclk=False, lowerUDI=True, verbose=0): if verbose != 0 and self.converter != None: self.converter.setVerbose(verbose) #Convert messages from the source for s seconds - if self.modeIn == 1 or self.modeIn == 2: + if self.modeIn == 1 or self.modeIn == 2 or self.modeIn == 7: raise Mode_Error("ERROR: Timed constraint not available for file reading.") elif self.modeIn >= 3 and self.modeIn <= 6: self.reader.read(converter=self.converter, output=self.output, mode="t", x=s, compact=compact, HRclk=HRclk, verbose=verbose) diff --git a/galileo_has_decoder/nov_reading.py b/galileo_has_decoder/nov_reading.py new file mode 100644 index 0000000..3171e4c --- /dev/null +++ b/galileo_has_decoder/nov_reading.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python + +''' +Novatel GALCNAVRAWPAGE file reading class + +VER DATE AUTHOR +1.0.x 2023-09-29 Jaakko Yliaho / Uwasa +''' + +import struct +import math +import binascii +from galileo_has_decoder.utils import bits2Bytes, gpst2time +from galileo_has_decoder.has_classes import HAS_Storage +from galileo_has_decoder.ssr_converter import SSR_Converter +from galileo_has_decoder.tcp_server import TCP_Server + +class FileError(Exception): + #Base File Error class + pass + +class NOV_Reader: + file = None + has_storage = None + def __init__(self, path, msgnum=0, skip=0): + self.file = open(path, "r") + lines = self.file.readlines() + self.lines = lines[int(skip):] + self.numberOfRawPages = len(self.lines) + self.has_storage = HAS_Storage() + self.msgnum = msgnum + self.pppWiz = False + + # Calculate CRC for single byte + @staticmethod + def crc(byte): + for j in range(8): + if ((byte & 1) & 0xFF): + byte = ((byte >> 1)) ^ 0xEDB88320 + else: + byte = (byte >> 1) + return byte + + # Calculate CRC for bytestream + @staticmethod + def CalculateBlockCRC32(bytestream): + result = 0x00000000 + for byte in bytestream: + temp1 = (result >> 8) & 0x00FFFFFF + temp2 = NOV_Reader.crc((result ^ byte ) & 0xFF) + result = temp1 ^ temp2 + return result + + def read(self, path=None, converter=None, output=None, mode='m', x=None, compact=True, HRclk=False, lowerUDI=True, verbose=0): + if mode != 'm': + raise Exception("File Reading does only support message-number constraints") + if x != None: self.msgnum = x + + if converter is not None: + if converter.pppWiz: + self.output = output + self.pppWiz = True + + cnavs = 0 + hasnum=0 + currentLineNumber=0 + + for logMessage in self.lines: + logMessageFields=logMessage.split(";") + if len(logMessageFields) != 2: + if verbose >= 5: + print("NOV-A Reader: Invalid line format on line " + str(currentLineNumber+1)) + currentLineNumber+=1 + continue + + headerFields=logMessageFields[0].split(",") + if len(headerFields) != 10: + if verbose >= 5: + print("NOV-A Reader: Invalid line format, wrong number of header fields, on line " + str(currentLineNumber+1)) + currentLineNumber+=1 + continue + + dataCrcFields=logMessageFields[1].split("*") + if len(dataCrcFields) != 2: + if verbose >= 5: + print("NOV-A Reader: Invalid line format, CRC missing, on line " + str(currentLineNumber+1)) + currentLineNumber+=1 + continue + + dataFields=dataCrcFields[0].split(",") + crcField=str.rstrip(dataCrcFields[1]) + + headerMessage=headerFields[0] + if headerMessage != "#GALCNAVRAWPAGEA" and "#GALCNAVRAWPAGEA" in headerMessage: + headerMessage="#GALCNAVRAWPAGEA" + + if headerMessage != "#GALCNAVRAWPAGEA": + if verbose >= 6: + print("NOV-A Reader, line "+str(currentLineNumber)+", not a raw C/NAV page: "+str(headerMessage)) + currentLineNumber+=1 + continue + + # Compute and check log message CRC + # C/NAV raw data CRC has already been checked by the receiver. + # Only C/NAV messages that pass CRC validation are outputted by logging + messageToCheck=headerMessage[1:]+','+','.join(headerFields[1:])+';'+','.join(dataFields) + calculatedCrc=NOV_Reader.CalculateBlockCRC32(bytes(messageToCheck, 'utf-8')) + if (binascii.unhexlify(crcField) != int.to_bytes(calculatedCrc, 4, 'big')): + if verbose >= 5: + print("NOV-A Reader: Invalid CRC on line " + str(currentLineNumber+1)) + currentLineNumber+=1 + continue + + if len(dataFields) != 5 and len(dataFields) != 4: + if verbose >= 5: + print("NOV-A Reader: Invalid line format, wrong number of data fields, on line " + str(currentLineNumber+1)) + currentLineNumber+=1 + continue + + headerPort=headerFields[1] + headerSequence=headerFields[2] + headerIdleTime=headerFields[3] + headerTimeStatus=headerFields[4] + headerWeek=headerFields[5] + headerSeconds=headerFields[6] + headerReceiverStatus=headerFields[7] + headerReserved=headerFields[8] + headerReceiverSWVersion=headerFields[9] + + dataSignalChannel=dataFields[0] + dataPRN=dataFields[1] + dataMessageID=dataFields[2] + dataPageID="" + dataRawCNAV="" + + # SW version 17022(7.08.14) implemented new ASCII logging format with four data fields before raw C/NAV data + # Before version 17022 it was only three data fields before C/NAV raw data + # Check that logged software version matches the number of data fields logged + if int(headerReceiverSWVersion) < 17022: + if len(dataFields) != 4: + if verbose >= 5: + print("NOV-A Reader: Invalid line format, wrong number of data fields, on line " + str(currentLineNumber+1)) + currentLineNumber+=1 + continue + + dataPageID="" + dataRawCNAV=dataFields[3] + else: + dataPageID=dataFields[3] + dataRawCNAV=dataFields[4] + + # All the checks done. This is an OK raw C/NAV page, increase the counter + cnavs+=1 + + rawCNAVBinaryString=''.join(format(x, '08b') for x in binascii.unhexlify(dataRawCNAV)) + # Remove the two extra bits at the end that are added for the ascii-hex representation of 464 bits instead of the C/NAV 462 + rawCNAVBinaryString=rawCNAVBinaryString[0:-2] + + if verbose >= 6: + print("NOV-A Reader, line "+str(currentLineNumber+1)+", header: "+str(headerFields)+", data: "+str(dataFields)+", crc: "+crcField+", bin: "+rawCNAVBinaryString) + + if self.has_storage.feedMessage(rawCNAVBinaryString, float(headerSeconds), verbose=verbose): + hasnum+=1 + if converter != None: + decoded_msg = self.has_storage.lastMessage + tow = self.has_storage.lastMessage_tow + if verbose >= 6: + print("NOV-A Reader, decoded_msg "+str(decoded_msg)+", tow: "+str(tow)+", tow2: "+str(float(headerSeconds))) + converted = converter.convertMessage(decoded_msg, compact=compact, HRclk=HRclk, tow=tow, lowerUDI=lowerUDI, verbose=verbose) + if output != None and converted != None: + for msg_conv in converted: + msg_bytes = bits2Bytes(msg_conv) + gpst2time(int(headerWeek), float(headerSeconds)) + if self.pppWiz: + output.write(msg_bytes, 2, 1, gpst2time(int(headerWeek), float(headerSeconds))) + else: + output.write(msg_bytes) + + currentLineNumber+=1 + continue + + if verbose>=1: + print("Out of "+str(currentLineNumber)+" messages, "+str(cnavs)+" were C/Nav messages. "+str(hasnum) + +" HAS messages have successfully been decoded and converted.") diff --git a/galileo_has_decoder/readers.py b/galileo_has_decoder/readers.py index 074c8ab..e5e73cc 100644 --- a/galileo_has_decoder/readers.py +++ b/galileo_has_decoder/readers.py @@ -5,10 +5,12 @@ VER DATE AUTHOR 1.0 09/12/2021 Oliver Horst / FGI +Modified 2023-09-29 Jaakko Yliaho / Uwasa to add NOV_Reader ''' from galileo_has_decoder.sbf_reading import SBF_Reader from galileo_has_decoder.binex_reading import Binex_Reader from galileo_has_decoder.tcp_sbf_reading import TCP_SBF_Reader from galileo_has_decoder.tcp_binex_reading import TCP_Binex_Reader -from galileo_has_decoder.serial_reading import Serial_SBF_Reader, Serial_Binex_Reader \ No newline at end of file +from galileo_has_decoder.serial_reading import Serial_SBF_Reader, Serial_Binex_Reader +from galileo_has_decoder.nov_reading import NOV_Reader \ No newline at end of file