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
[](https://pypi.org/project/pygnssutils/)
[](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",