diff --git a/README.md b/README.md index 9df99d8..0cfeef6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ pygnssutils [Current Status](#currentstatus) | [Installation](#installation) | +[gnssreader](#gnssreader) | [gnssstreamer CLI](#gnssstreamer) | [gnssserver CLI](#gnssserver) | [gnssntripclient CLI](#gnssntripclient) | @@ -16,12 +17,14 @@ pygnssutils is an original series of Python GNSS utility classes and CLI tools b - [pyubx2](https://github.com/semuconsulting/pyubx2) - UBX parsing and generation library - [pysbf2](https://github.com/semuconsulting/pysbf2) - SBF parsing and generation library +- [pyqgc](https://github.com/semuconsulting/pyqgc) - QGC parsing and generation library - [pynmeagps](https://github.com/semuconsulting/pynmeagps) - NMEA parsing and generation library - [pyrtcm](https://github.com/semuconsulting/pyrtcm) - RTCM3 parsing library - [pyspartn](https://github.com/semuconsulting/pyspartn) - SPARTN parsing library Originally developed in support of the [PyGPSClient](https://github.com/semuconsulting/PyGPSClient) GUI GNSS application, the utilities provided by pygnssutils can also be used in their own right: +1. `GNSSReader` class. This is essentially an amalgamation of the `*Reader` classes in all the subsidiary parsers listed above, allowing the user to seamlessly stream any of NMEA, UBX, SBF, QGC, RTCM3 and SPARTN message protocols concurrently from a single stream. 1. `GNSSStreamer` class and its associated [`gnssstreamer`](#gnssstreamer) (*formerly `gnssdump`*) CLI utility. This is essentially a configurable bidirectional input/output wrapper around the [`pyubx2.UBXReader`](https://github.com/semuconsulting/pyubx2#reading) class with flexible message formatting, filtering and output handling options for NMEA, UBX, SBF and RTCM3 protocols (**NB:** UBX and SBF protocols are mutually exclusive). 1. `GNSSSocketServer` class and its associated [`gnssserver`](#gnssserver) CLI utility. This implements a TCP Socket Server for GNSS data streams which is also capable of being run as a simple NTRIP Server/Caster. 1. `GNSSNTRIPClient` class and its associated [`gnssntripclient`](#gnssntripclient) CLI utility. This implements @@ -58,7 +61,7 @@ Contributions welcome - please refer to [CONTRIBUTING.MD](https://github.com/sem [![PyPI version](https://img.shields.io/pypi/v/pygnssutils.svg?style=flat)](https://pypi.org/project/pygnssutils/) [![PyPI downloads](https://github.com/semuconsulting/pygpsclient/blob/master/images/clickpy_top10.svg?raw=true)](https://clickpy.clickhouse.com/dashboard/pygnssutils) -`pygnssutils` is compatible with Python 3.9-3.13. +`pygnssutils` is compatible with Python>=3.10. In the following, `python3` & `pip` refer to the Python 3 executables. You may need to substitute `python` for `python3`, depending on your particular environment (*on Windows it's generally `python`*). **It is strongly recommended that** the Python 3 binaries (\Scripts or /bin) and site_packages directories are included in your PATH (*most standard Python 3 installation packages will do this automatically if you select the 'Add to PATH' option during installation*). @@ -85,6 +88,17 @@ For [Conda](https://docs.conda.io/en/latest/) users, `pygnssutils` is also avail conda install -c conda-forge pygnssutils ``` +--- +## GNSSReader class + +``` +class pygnssutils.gnssreader.GNSSReader(**kwargs) +``` + +`GNSSReader` is an amalgamation of the individual `*Reader` classes from the parser libraries listed above, utilising the same input arguments (`protfilter`, `quitonerror`, etc). It allows the user to seamlessly stream any of NMEA, UBX, SBF, QGC, RTCM3 and SPARTN message protocols concurrently from a single GNSS binary data stream. + +Refer to the [Sphinx API documentation](https://www.semuconsulting.com/pygnssutils/pygnssutils.html#module-pygnssutils.gnssreader) for further details. + --- ## GNSSStreamer and gnssstreamer CLI (*formerly gnssdump*) @@ -92,9 +106,7 @@ conda install -c conda-forge pygnssutils class pygnssutils.gnssstreamer.GNSSStreamer(**kwargs) ``` -`gnssstreamer` (*formerly `gnssdump`*) is a command line utility for concurrent bidirectional communication with a GNSS datastream - typically a GNSS receiver. It supports NMEA, UBX, SBF, RTCM3, SPARTN, NTRIP and MQTT protocols. - -**NB:** Currently, `gnsssstreamer` can parse data streams containing *either* UBX *or* SBF messages, but not both at the same time. If both are included in `protfilter`, UBX will take precedence over SBF. +`gnssstreamer` (*formerly `gnssdump`*) is a command line utility for concurrent bidirectional communication with a GNSS datastream - typically a GNSS receiver. It supports NMEA, UBX, SBF, QGC, RTCM3, SPARTN, NTRIP and MQTT protocols - individual protocols can be filtered via the `protfilter` arguments. - The CLI utility can acquire data from any one of the following sources: - `port`: serial port e.g. `COM3` or `/dev/ttyACM1` (can specify `--baudrate` and `--timeout`) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3ee7bbd..2ac27c7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,10 +1,14 @@ # pygnssutils -### RELEASE 1.1.17 +### RELEASE 1.1.18 -CHANGES: +ENHANCEMENTS: + +1. Add support for Quectel QGC protocol. + +### RELEASE 1.1.17 -1. Min pyubx2 ver updated to 1.2.58 - incorporates several minor fixes and additional support for proprietary Quectel NMEA message definitions. +1. Update minimum versions of pyubx2 and pynmeagps to cater for various fixes and new message types. ### RELEASE 1.1.16 diff --git a/docs/pygnssutils.rst b/docs/pygnssutils.rst index eaaf8ea..7f8fc35 100644 --- a/docs/pygnssutils.rst +++ b/docs/pygnssutils.rst @@ -52,6 +52,14 @@ pygnssutils.gnssntripclient\_cli module :undoc-members: :show-inheritance: +pygnssutils.gnssreader module +----------------------------- + +.. automodule:: pygnssutils.gnssreader + :members: + :undoc-members: + :show-inheritance: + pygnssutils.gnssserver module ----------------------------- diff --git a/pyproject.toml b/pyproject.toml index d960e01..a693937 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ description = "GNSS Command Line Utilities" license = "BSD-3-Clause" license-files = ["LICENSE"] readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", @@ -23,7 +23,6 @@ classifiers = [ "Intended Audience :: Science/Research", "Intended Audience :: End Users/Desktop", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -42,6 +41,7 @@ dependencies = [ "pyubx2>=1.2.58", "pysbf2>=1.0.0", "pyubxutils>=1.0.3", + "pyqgc>=0.1.1", ] [project.scripts] @@ -83,10 +83,10 @@ deploy = [{ include-group = "build" }, { include-group = "test" }] version = { attr = "pygnssutils._version.__version__" } [tool.black] -target-version = ['py39'] +target-version = ['py310'] [tool.isort] -py_version = 39 +py_version = 310 profile = "black" [tool.bandit] @@ -103,7 +103,7 @@ skips = [ jobs = 0 recursive = "y" reports = "y" -py-version = "3.9" +py-version = "3.10" fail-under = "9.8" fail-on = "E,F" clear-cache-post-run = "y" diff --git a/src/pygnssutils/_version.py b/src/pygnssutils/_version.py index 8ca4af4..0682ba0 100644 --- a/src/pygnssutils/_version.py +++ b/src/pygnssutils/_version.py @@ -8,4 +8,4 @@ :license: BSD 3-Clause """ -__version__ = "1.1.17" +__version__ = "1.1.18" diff --git a/src/pygnssutils/gnssreader.py b/src/pygnssutils/gnssreader.py new file mode 100644 index 0000000..db569bd --- /dev/null +++ b/src/pygnssutils/gnssreader.py @@ -0,0 +1,488 @@ +""" +gnssreader.py + +Generic GNSS class. + +Reads and parses individual UBX, SBF, QGC, NMEA or RTCM3 messages from any viable +data stream which supports a read(n) -> bytes method. + +It is essentially an amalgamation of the Reader classes in the separate pyubx2, pynmeagps, +pyrtcm, pysbf2 and pyqgc packages. + +Returns both the raw binary data (as bytes) and the parsed data. + +- 'protfilter' governs which protocols (NMEA, UBX, SBF, QGC or RTCM3) are processed +- 'quitonerror' governs how errors are handled +- 'msgmode' indicates the type of UBX datastream (output GET, input SET, query POLL). + If msgmode is set to SETPOLL, input/query mode will be automatically detected by parser. + +Created on 6 Oct 2025 + +:author: semuadmin (Steve Smith) +:copyright: semuadmin © 2020 +:license: BSD 3-Clause +""" + +# pylint: disable=too-many-positional-arguments + +from logging import getLogger +from socket import socket + +from pynmeagps import ( + NMEA_HDR, + NMEAMessageError, + NMEAParseError, + NMEAReader, + NMEAStreamError, + NMEATypeError, + SocketWrapper, +) +from pyqgc import ( + QGC_HDR, + QGCMessageError, + QGCParseError, + QGCReader, + QGCStreamError, + QGCTypeError, +) +from pyrtcm import ( + RTCMMessageError, + RTCMParseError, + RTCMReader, + RTCMStreamError, + RTCMTypeError, +) +from pysbf2 import ( + SBF_HDR, + SBFMessageError, + SBFParseError, + SBFReader, + SBFStreamError, + SBFTypeError, +) +from pyubx2 import ( + ERR_LOG, + ERR_RAISE, + GET, + POLL, + SET, + SETPOLL, + UBX_HDR, + VALCKSUM, + UBXMessageError, + UBXParseError, + UBXReader, + UBXStreamError, + UBXTypeError, +) + +NMEA_PROTOCOL = 1 +"""NMEA Protocol""" +UBX_PROTOCOL = 2 +"""UBX Protocol""" +RTCM3_PROTOCOL = 4 +"""RTCM3 Protocol""" +SBF_PROTOCOL = 8 +"""RTCM3 Protocol""" +QGC_PROTOCOL = 16 +"""RTCM3 Protocol""" + + +class StreamError(Exception): + """Stream Error Class.""" + + +class GNSSReader: + """ + GNSSReader class. + """ + + def __init__( + self, + datastream, + msgmode: int = GET, + validate: int = VALCKSUM, + protfilter: int = NMEA_PROTOCOL + | UBX_PROTOCOL + | RTCM3_PROTOCOL + | SBF_PROTOCOL + | QGC_PROTOCOL, + quitonerror: int = ERR_LOG, + parsebitfield: bool = True, + labelmsm: int = 1, + bufsize: int = 4096, + parsing: bool = True, + errorhandler: object = None, + ): + """Constructor. + + :param datastream stream: input data stream + :param int msgmode: 0=GET, 1=SET, 2=POLL, 3=SETPOLL (0) + :param int validate: VALCKSUM (1) = Validate checksum, + VALNONE (0) = ignore invalid checksum (1) + :param int protfilter: NMEA_PROTOCOL (1), UBX_PROTOCOL (2), RTCM3_PROTOCOL (4), + SBF_PROTOCOL (8), QGC_PROTOCOL (16), Can be OR'd (7) + :param int quitonerror: ERR_IGNORE (0) = ignore errors, ERR_LOG (1) = log continue, + ERR_RAISE (2) = (re)raise (1) + :param bool parsebitfield: 1 = parse bitfields, 0 = leave as bytes (1) + :param int labelmsm: RTCM3 MSM label type 1 = RINEX, 2 = BAND (1) + :param int bufsize: socket recv buffer size (4096) + :param bool parsing: True = parse data, False = don't parse data (output raw only) (True) + :param object errorhandler: error handling object or function (None) + :raises: UBXStreamError (if mode is invalid) + """ + # pylint: disable=too-many-arguments + + if isinstance(datastream, socket): + self._stream = SocketWrapper(datastream, bufsize=bufsize) + else: + self._stream = datastream + self._protfilter = protfilter + self._quitonerror = quitonerror + self._errorhandler = errorhandler + self._validate = validate + self._parsebf = parsebitfield + self._labelmsm = labelmsm + self._msgmode = msgmode + self._parsing = parsing + self._logger = getLogger(__name__) + + if self._msgmode not in (GET, SET, POLL, SETPOLL): + raise ValueError( + f"Invalid stream mode {self._msgmode} - must be 0, 1, 2 or 3" + ) + + def __iter__(self): + """Iterator.""" + + return self + + def __next__(self) -> tuple: + """ + Return next item in iteration. + + :return: tuple of (raw_data as bytes, parsed_data as UBXMessage) + :rtype: tuple + :raises: StopIteration + + """ + + (raw_data, parsed_data) = self.read() + if raw_data is None and parsed_data is None: + raise StopIteration + return (raw_data, parsed_data) + + def read(self) -> tuple: + """ + Read a single NMEA, UBX or RTCM3 message from the stream buffer + and return both raw and parsed data. + + 'protfilter' determines which protocols are parsed. + 'quitonerror' determines whether to raise, log or ignore parsing errors. + + :return: tuple of (raw_data as bytes, parsed_data as UBXMessage, NMEAMessage or RTCMMessage) + :rtype: tuple + :raises: Exception (if invalid or unrecognised protocol in data stream) + """ + + parsing = True + while parsing: # loop until end of valid message or EOF + try: + + raw_data = None + parsed_data = None + byte1 = self._read_bytes(1) # read the first byte + # if not UBX, SBF, QGC, NMEA or RTCM3, discard and continue + if byte1 not in (b"\xb5", b"\x24", b"\x51", b"\xd3"): + continue + byte2 = self._read_bytes(1) + bytehdr = byte1 + byte2 + # if it's a UBX message (b'\xb5\x62') + if bytehdr == UBX_HDR: + (raw_data, parsed_data) = self._parse_ubx(bytehdr) + # if protocol filter passes UBX, return message, + # otherwise discard and continue + if self._protfilter & UBX_PROTOCOL: + parsing = False + else: + continue + # if it's an NMEA message (b'\x24\x..) + elif bytehdr in NMEA_HDR: + (raw_data, parsed_data) = self._parse_nmea(bytehdr) + # if protocol filter passes NMEA, return message, + # otherwise discard and continue + if self._protfilter & NMEA_PROTOCOL: + parsing = False + else: + continue + # if it's an SBF message (b'\x24\x40') + elif bytehdr in SBF_HDR: + (raw_data, parsed_data) = self._parse_sbf(bytehdr) + # if protocol filter passes SBF, return message, + # otherwise discard and continue + if self._protfilter & SBF_PROTOCOL: + parsing = False + else: + continue + # if it's an QGCmessage (b'\x51\x47') + elif bytehdr in QGC_HDR: + (raw_data, parsed_data) = self._parse_qgc(bytehdr) + # if protocol filter passes QGC, return message, + # otherwise discard and continue + if self._protfilter & QGC_PROTOCOL: + parsing = False + else: + continue + # if it's a RTCM3 message + # (byte1 = 0xd3; byte2 = 0b000000**) + elif byte1 == b"\xd3" and (byte2[0] & ~0x03) == 0: + (raw_data, parsed_data) = self._parse_rtcm3(bytehdr) + # if protocol filter passes RTCM, return message, + # otherwise discard and continue + if self._protfilter & RTCM3_PROTOCOL: + parsing = False + else: + continue + # unrecognised protocol header + else: + raise ValueError(f"Unknown protocol header {bytehdr}.") + + except EOFError: + return (None, None) + except ( + UBXMessageError, + UBXTypeError, + UBXParseError, + UBXStreamError, + NMEAMessageError, + NMEATypeError, + NMEAParseError, + NMEAStreamError, + RTCMMessageError, + RTCMParseError, + RTCMStreamError, + RTCMTypeError, + SBFMessageError, + SBFParseError, + SBFStreamError, + SBFTypeError, + QGCMessageError, + QGCParseError, + QGCStreamError, + QGCTypeError, + StreamError, + ) as err: + if self._quitonerror: + self._do_error(err) + continue + + return (raw_data, parsed_data) + + def _parse_ubx(self, hdr: bytes) -> tuple: + """ + Parse remainder of UBX message. + + :param bytes hdr: UBX header (b'\\xb5\\x62') + :return: tuple of (raw_data as bytes, parsed_data as UBXMessage or None) + :rtype: tuple + """ + + # read the rest of the UBX message from the buffer + byten = self._read_bytes(4) + clsid = byten[0:1] + msgid = byten[1:2] + lenb = byten[2:4] + leni = int.from_bytes(lenb, "little", signed=False) + byten = self._read_bytes(leni + 2) + plb = byten[0:leni] + cksum = byten[leni : leni + 2] + raw_data = hdr + clsid + msgid + lenb + plb + cksum + # only parse if we need to (filter passes UBX) + if (self._protfilter & UBX_PROTOCOL) and self._parsing: + parsed_data = UBXReader.parse( + raw_data, + validate=self._validate, + msgmode=self._msgmode, + parsebitfield=self._parsebf, + ) + else: + parsed_data = None + return (raw_data, parsed_data) + + def _parse_sbf(self, hdr: bytes) -> tuple: + """ + Parse remainder of SBF message. + + :param bytes hdr: SBF header (b'\\x24\\x40') + :return: tuple of (raw_data as bytes, parsed_data as SBFMessage or None) + :rtype: tuple + """ + + # read the rest of the SBF message from the buffer + byten = self._read_bytes(6) + crc = byten[0:2] + msgid = byten[2:4] + lenb = byten[4:6] + # lenb includes 8 byte header + leni = int.from_bytes(lenb, "little", signed=False) - 8 + plb = self._read_bytes(leni) + raw_data = hdr + crc + msgid + lenb + plb + # only parse if we need to (filter passes SBF) + if (self._protfilter & SBF_PROTOCOL) and self._parsing: + parsed_data = SBFReader.parse( + raw_data, + validate=self._validate, + parsebitfield=self._parsebf, + ) + else: + parsed_data = None + return (raw_data, parsed_data) + + def _parse_qgc(self, hdr: bytes) -> tuple: + """ + Parse remainder of QGC message. + + :param bytes hdr: QGC header (b'\\x51\\x47') + :return: tuple of (raw_data as bytes, parsed_data as QGCMessage or None) + :rtype: tuple + """ + + # read the rest of the QGC message from the buffer + byten = self._read_bytes(4) + msggrp = byten[0:1] + msgid = byten[1:2] + lenb = byten[2:4] + leni = int.from_bytes(lenb, "little", signed=False) + byten = self._read_bytes(leni + 2) + plb = byten[0:leni] + cksum = byten[leni : leni + 2] + raw_data = hdr + msggrp + msgid + lenb + plb + cksum + # only parse if we need to (filter passes QGC) + if (self._protfilter & QGC_PROTOCOL) and self._parsing: + parsed_data = QGCReader.parse( + raw_data, + msgmode=self._msgmode, + validate=self._validate, + parsebitfield=self._parsebf, + ) + else: + parsed_data = None + return (raw_data, parsed_data) + + def _parse_nmea(self, hdr: bytes) -> tuple: + """ + Parse remainder of NMEA message (using pynmeagps library). + + :param bytes hdr: NMEA header (b'\\x24\\x..') + :return: tuple of (raw_data as bytes, parsed_data as NMEAMessage or None) + :rtype: tuple + """ + + # read the rest of the NMEA message from the buffer + byten = self._read_line() # NMEA protocol is CRLF-terminated + raw_data = hdr + byten + # only parse if we need to (filter passes NMEA) + if (self._protfilter & NMEA_PROTOCOL) and self._parsing: + # invoke pynmeagps parser + parsed_data = NMEAReader.parse( + raw_data, + validate=self._validate, + msgmode=self._msgmode, + ) + else: + parsed_data = None + return (raw_data, parsed_data) + + def _parse_rtcm3(self, hdr: bytes) -> tuple: + """ + Parse any RTCM3 data in the stream (using pyrtcm library). + + :param bytes hdr: first 2 bytes of RTCM3 header + :return: tuple of (raw_data as bytes, parsed_stub as RTCMMessage) + :rtype: tuple + """ + + hdr3 = self._read_bytes(1) + size = hdr3[0] | (hdr[1] << 8) + payload = self._read_bytes(size) + crc = self._read_bytes(3) + raw_data = hdr + hdr3 + payload + crc + # only parse if we need to (filter passes RTCM) + if (self._protfilter & RTCM3_PROTOCOL) and self._parsing: + # invoke pyrtcm parser + parsed_data = RTCMReader.parse( + raw_data, + validate=self._validate, + labelmsm=self._labelmsm, + ) + else: + parsed_data = None + return (raw_data, parsed_data) + + def _read_bytes(self, size: int) -> bytes: + """ + Read a specified number of bytes from stream. + + :param int size: number of bytes to read + :return: bytes + :rtype: bytes + :raises: UBXStreamError if stream ends prematurely + """ + + data = self._stream.read(size) + if len(data) == 0: # EOF + raise EOFError() + if 0 < len(data) < size: # truncated stream + raise StreamError( + "Serial stream terminated unexpectedly. " + f"{size} bytes requested, {len(data)} bytes returned." + ) + return data + + def _read_line(self) -> bytes: + """ + Read bytes until LF (0x0a) terminator. + + :return: bytes + :rtype: bytes + :raises: UBXStreamError if stream ends prematurely + """ + + data = self._stream.readline() # NMEA protocol is CRLF-terminated + if len(data) == 0: + raise EOFError() # pragma: no cover + if data[-1:] != b"\x0a": # truncated stream + raise StreamError( + "Serial stream terminated unexpectedly. " + f"Line requested, {len(data)} bytes returned." + ) + return data + + def _do_error(self, err: Exception): + """ + Handle error. + + :param Exception err: error + :raises: Exception if quitonerror = ERR_RAISE (2) + """ + + if self._quitonerror == ERR_RAISE: + raise err from err + if self._quitonerror == ERR_LOG: + # pass to error handler if there is one + # else just log + if self._errorhandler is None: + self._logger.error(err) + else: + self._errorhandler(err) + + @property + def datastream(self) -> object: + """ + Getter for stream. + + :return: data stream + :rtype: object + """ + + return self._stream diff --git a/src/pygnssutils/gnssstreamer.py b/src/pygnssutils/gnssstreamer.py index 00c1554..b741326 100644 --- a/src/pygnssutils/gnssstreamer.py +++ b/src/pygnssutils/gnssstreamer.py @@ -19,22 +19,18 @@ from threading import Event, Thread from time import time -from pynmeagps import NMEAMessage, NMEAParseError -from pyrtcm import RTCMMessage, RTCMParseError -from pysbf2 import SBFMessage, SBFParseError, SBFReader +from pynmeagps import NMEAMessage +from pyqgc import QGCMessage +from pyrtcm import RTCMMessage +from pysbf2 import SBFMessage from pyubx2 import ( CARRSOLN, ERR_RAISE, FIXTYPE, GET, LASTCORRECTIONAGE, - NMEA_PROTOCOL, - RTCM3_PROTOCOL, - UBX_PROTOCOL, VALCKSUM, UBXMessage, - UBXParseError, - UBXReader, hextable, ) from serial import Serial @@ -52,10 +48,17 @@ FORMAT_PARSEDSTRING, VERBOSITY_MEDIUM, ) +from pygnssutils.gnssreader import ( + NMEA_PROTOCOL, + QGC_PROTOCOL, + RTCM3_PROTOCOL, + SBF_PROTOCOL, + UBX_PROTOCOL, + GNSSReader, +) from pygnssutils.helpers import format_json, set_logging SLEEPTIME = 1 -SBF_PROTOCOL = 8 class GNSSStreamer: @@ -88,7 +91,11 @@ def __init__( parsebitfield: bool = True, outformat: int = FORMAT_PARSED, quitonerror: int = ERR_RAISE, - protfilter: int = NMEA_PROTOCOL | UBX_PROTOCOL | RTCM3_PROTOCOL, + protfilter: int = NMEA_PROTOCOL + | UBX_PROTOCOL + | RTCM3_PROTOCOL + | SBF_PROTOCOL + | QGC_PROTOCOL, msgfilter: str = "", limit: int = 0, outqueue: Queue = None, @@ -112,7 +119,7 @@ def __init__( 16 = parsed as string, 32 = JSON (can be OR'd) (1) :param int quitonerror: 0 = ignore errors, 1 = log errors and continue, \ 2 = (re)raise errors (1) - :param int protfilter: 1 = NMEA, 2 = UBX, 4 = RTCM3, 8 = SBF (can be OR'd) (7) + :param int protfilter: 1 = NMEA, 2 = UBX, 4 = RTCM3, 8 = SBF, 16 = QGC (can be OR'd) (31) :param str msgfilter: comma-separated string of message identities to include in output \ e.g. 'NAV-PVT,GNGSA'. A periodicity clause can be added e.g. NAV-SAT(10), signifying \ the minimum period in seconds between successive messages of this type ("") @@ -147,10 +154,7 @@ def __init__( if not 0 < self._outformat < 64: raise ParameterError(f"format {self._outformat} cannot exceed 63") self._quitonerror = int(quitonerror) - protfilter = int(protfilter) - if protfilter & UBX_PROTOCOL and protfilter & SBF_PROTOCOL: - protfilter ^= SBF_PROTOCOL # UBX takes precedence over SBF - self._protfilter = protfilter + self._protfilter = int(protfilter) self._limit = int(limit) self._outqueue = outqueue self._inqueue = inqueue @@ -292,22 +296,15 @@ def _read_loop( :param dict kwargs: user-defined keyword arguments """ - # UBX and SBF are mutually exclusive protocols - if protfilter & SBF_PROTOCOL: - ubr = SBFReader( - stream, - validate=self._validate, - quitonerror=self._quitonerror, - parsebitfield=self._parsebitfield, - ) - else: - ubr = UBXReader( - stream, - msgmode=self._msgmode, - validate=self._validate, - quitonerror=self._quitonerror, - parsebitfield=self._parsebitfield, - ) + ubr = GNSSReader( + stream, + msgmode=self._msgmode, + # protfilter=protfilter, # messages filtered externally + validate=self._validate, + quitonerror=self._quitonerror, + parsebitfield=self._parsebitfield, + ) + while not stopevent.is_set(): try: @@ -343,12 +340,7 @@ def _read_loop( raise ParameterError() from err except OSError: # thread terminated while reading break - except ( - NMEAParseError, - UBXParseError, - RTCMParseError, - SBFParseError, - ) as err: + except Exception as err: # pylint disable=broad-exception-caught self._errcount += 1 self.logger.error(f"Error parsing data stream {err}") continue @@ -415,6 +407,8 @@ def _filtered(self, parsed_data: object) -> bool: protocol = RTCM3_PROTOCOL elif isinstance(parsed_data, SBFMessage): protocol = SBF_PROTOCOL + elif isinstance(parsed_data, QGCMessage): + protocol = QGC_PROTOCOL else: return True diff --git a/src/pygnssutils/gnssstreamer_cli.py b/src/pygnssutils/gnssstreamer_cli.py index f5a8a6d..47d4dc1 100644 --- a/src/pygnssutils/gnssstreamer_cli.py +++ b/src/pygnssutils/gnssstreamer_cli.py @@ -37,8 +37,6 @@ from types import FunctionType from pynmeagps import SocketWrapper -from pysbf2 import SBFReader -from pyubx2 import ERR_LOG, SETPOLL, UBXReader from pyubxutils.ubxsimulator import UBXSimulator from serial import Serial, SerialException @@ -74,6 +72,7 @@ ) from pygnssutils.gnssmqttclient import GNSSMQTTClient from pygnssutils.gnssntripclient import GNSSNTRIPClient +from pygnssutils.gnssreader import ERR_LOG, SETPOLL, GNSSReader from pygnssutils.gnssstreamer import GNSSStreamer from pygnssutils.helpers import parse_url, set_common_args from pygnssutils.socket_server import runserver @@ -202,7 +201,7 @@ def runreader(stream: object, output: Queue): """ try: - ubr = UBXReader(stream, msgmode=SETPOLL, quitonerror=ERR_LOG) + ubr = GNSSReader(stream, msgmode=SETPOLL, quitonerror=ERR_LOG) for raw, _ in ubr: if raw is not None: output.put(raw) @@ -476,9 +475,9 @@ def main(): ap.add_argument( "--protfilter", required=False, - help="1 = NMEA, 2 = UBX, 4 = RTCM3, 8 = SBF (can be OR'd; UBX and SBF are mutually exclusive, UBX takes precedence over SBF)", + help="1 = NMEA, 2 = UBX, 4 = RTCM3, 8 = SBF, 16 = QGC (can be OR'd)", type=int, - default=7, + default=31, ) ap.add_argument( "--msgfilter",