Repository: semuconsulting/pyrtcm (language: python)

Issue:
Message Type 1230 Parsing
**Describe the bug**

In rtcmtypes_get.py there is a TODO for message 1230

        # TODO verify that each iteration of this group only ever contains
        # one of either DF423, DF424, DF425 or FD426?

This is not the case. 

Using a Trimble SPS855 or R750 data stream as an example, DF422 is 15, which indicate that aDF423-DF426 is present. 

message print.
<RTCM(1230, DF002=1230, DF003=4, DF421=0, DF001_3=0, DF422=15, DF423_01=19.06, DF423_02=19.06, DF423_03=19.06, DF423_04=19.06)>


{'DF001_3': 'Reserved',
 'DF002': 'Message Number',
 'DF003': 'Reference Station ID',
 'DF421': 'GLONASS Code-Phase bias indicator',
 'DF422': 'GLONASS FDMA signals mask',
 'group': ('_NBias', {'DF423': 'GLONASS L1 C/A Code-Phase Bias'})}

You will notice that DF423 has 4 repeats, DF423_01 to DF423_04. DF423 is not a repeating value, and DF422 is not a number of items indicator. It is a bit mask of the fields following.


If you remove # from the definition of NBIAS so DF DF424-DF426 are defined I think it does not parse 1230 correctly. 

        "group": (
            NBIAS,
            {
                "DF423": "GLONASS L1 C/A Code-Phase Bias",
                "DF424": "GLONASS L1 P Code-Phase Bias",
                "DF425": "GLONASS L2 C/A Code-Phase Bias",
                "DF426": "GLONASS L2 P Code-Phase Bias",
            },

message print.
<RTCM(1230, DF002=1230, DF003=11, DF421=0, DF001_3=0, DF422=15, DF423_01=19.06, DF424_01=19.06, DF425_01=19.06, DF426_01=19.06, DF423_02=0, DF424_02=0, DF425_02=0, DF426_02=0, DF423_03=0, DF424_03=0, DF425_03=0, DF426_03=0, DF423_04=0, DF424_04=0, DF425_04=0, DF426_04=0)>

You see the same issue with _01-_04, but it is does have DF423-DF426.


{'DF001_3': 'Reserved',
 'DF002': 'Message Number',
 'DF003': 'Reference Station ID',
 'DF421': 'GLONASS Code-Phase bias indicator',
 'DF422': 'GLONASS FDMA signals mask',
 'group': ('_NBias',
           {'DF423': 'GLONASS L1 C/A Code-Phase Bias',
            'DF424': 'GLONASS L1 P Code-Phase Bias',
            'DF425': 'GLONASS L2 C/A Code-Phase Bias',
            'DF426': 'GLONASS L2 P Code-Phase Bias'})}

**To Reproduce**

SPS855.COM port 28003 has a RTCM V3.x stream showing this behaviour. The dump above is from that stream. 

I do not have access to a unit that only outputs a subset of the biases.

- Trimble Precise GNSS. 
- Firmware 5.60
- RTCM V3  
 




Discussion / hints:

Fix patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6992ef2..901f4d9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [3.7, 3.8, 3.9, "3.10"]
+        python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
 
     steps:
       - uses: actions/checkout@v3
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 27bf4e6..41f53bf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -15,5 +15,5 @@
     "editor.formatOnSave": true,
     "modulename": "${workspaceFolderBasename}",
     "distname": "${workspaceFolderBasename}",
-    "moduleversion": "1.0.4"
+    "moduleversion": "1.0.5"
 }
\ No newline at end of file
diff --git a/README.md b/README.md
index 56e7eb3..b2f845e 100644
--- a/README.md
+++ b/README.md
@@ -106,14 +106,14 @@ Example - File input (using iterator).
 ...
 ```
 
-Example - Socket input (using enhanced iterator):
+Example - Socket input (using iterator):
 ```python
 >>> import socket
 >>> from pyrtcm import RTCMReader
 >>> stream = socket.socket(socket.AF_INET, socket.SOCK_STREAM):
 >>> stream.connect(("localhost", 50007))
 >>> rtr = RTCMReader(stream)
->>> for (raw_data, parsed_data) in rtr.iterate(): print(parsed_data)
+>>> for (raw_data, parsed_data) in rtr: print(parsed_data)
 ```
 
 ---
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index ab97c2b..294827a 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,17 @@
 # pyrtcm Release Notes
 
-### RELEASE 1.0.3
+### RELEASE 1.0.5
+
+FIXES:
+
+1. Fix to 1230 message payload - will now correctly label DF423, DF424, DF425 & DF426 data fields.
+
+CHANGES:
+
+1. `__str__` method enhanced to escape all byte values for clarity e.g. will now return b'\x61\x62\x63' rather than b'abc'
+2. `RTCMReader.iterate()` method deprecated - use the standard iterator instead e.g. `rtr = RTCMReader(**wkargs): for (raw,parse) in ubr: ...`, passing any `quitonerror` or `errorhandler` kwargs to the RTCMReader constructor.
+
+### RELEASE 1.0.4
 
 FIXES:
 
diff --git a/examples/rtcmfile.py b/examples/rtcmfile.py
index ca32198..e217c8b 100644
--- a/examples/rtcmfile.py
+++ b/examples/rtcmfile.py
@@ -29,13 +29,10 @@ def read(stream, errorhandler, quitonerror, validate):
     """
 
     msgcount = 0
-
     ubr = RTCMReader(
         stream, errorhandler=errorhandler, quitonerror=quitonerror, validate=validate
     )
-    for (_, parsed_data) in ubr.iterate(
-        errorhandler=errorhandler, quitonerror=quitonerror
-    ):
+    for _, parsed_data in ubr:
         print(parsed_data)
         msgcount += 1
 
@@ -43,7 +40,6 @@ def read(stream, errorhandler, quitonerror, validate):
 
 
 if __name__ == "__main__":
-
     YES = ("Y", "y", "YES,", "yes", "True")
     NO = ("N", "n", "NO,", "no", "False")
 
diff --git a/examples/rtcmsocket.py b/examples/rtcmsocket.py
index 5a9e60f..0b7cf4b 100644
--- a/examples/rtcmsocket.py
+++ b/examples/rtcmsocket.py
@@ -25,7 +25,7 @@ def read(stream: socket.socket):
         stream,
     )
     try:
-        for (_, parsed_data) in rtr.iterate():
+        for _, parsed_data in rtr:
             print(parsed_data)
             msgcount += 1
     except KeyboardInterrupt:
@@ -39,7 +39,6 @@ def read(stream: socket.socket):
 
 
 if __name__ == "__main__":
-
     SERVER = "192.168.0.72"
     PORT = 50007
 
diff --git a/pyrtcm/_version.py b/pyrtcm/_version.py
index 540c541..765790b 100644
--- a/pyrtcm/_version.py
+++ b/pyrtcm/_version.py
@@ -8,4 +8,4 @@
 :license: BSD 3-Clause
 """
 
-__version__ = "1.0.4"
+__version__ = "1.0.5"
diff --git a/pyrtcm/rtcmhelpers.py b/pyrtcm/rtcmhelpers.py
index adb0964..68efa1a 100644
--- a/pyrtcm/rtcmhelpers.py
+++ b/pyrtcm/rtcmhelpers.py
@@ -423,3 +423,15 @@ def id2prnsigmap(ident: str) -> tuple:
         SIGMAP = None
 
     return (PRNMAP, SIGMAP)
+
+
+def escapeall(val: bytes) -> str:
+    """
+    Escape all byte characters e.g. b'\\\\x73' rather than b`s`
+
+    :param bytes val: bytes
+    :return: string of escaped bytes
+    :rtype: str
+    """
+
+    return "b'{}'".format("".join(f"\\x{b:02x}" for b in val))
diff --git a/pyrtcm/rtcmmessage.py b/pyrtcm/rtcmmessage.py
index 18d5250..6144add 100644
--- a/pyrtcm/rtcmmessage.py
+++ b/pyrtcm/rtcmmessage.py
@@ -10,7 +10,21 @@
 # pylint: disable=invalid-name
 
 import pyrtcm.exceptions as rte
-import pyrtcm.rtcmtypes_core as rtt
+from pyrtcm.rtcmtypes_core import (
+    RTCM_HDR,
+    RTCM_MSGIDS,
+    RTCM_DATA_FIELDS,
+    BOOL_GROUPS,
+    ATT_NSAT,
+    ATT_NCELL,
+    NSAT,
+    NSIG,
+    NCELL,
+    NL1CA,
+    NL1P,
+    NL2CA,
+    NL2P,
+)
 import pyrtcm.rtcmtypes_get as rtg
 from pyrtcm.rtcmhelpers import (
     crc2bytes,
@@ -23,13 +37,9 @@
     cell2prn,
     att2idx,
     att2name,
+    escapeall,
 )
 
-NSAT = "NSat"
-NSIG = "NSig"
-NCELL = "_NCell"
-NBIAS = "_NBias"
-
 
 class RTCMMessage:
     """RTCM Message Class."""
@@ -164,12 +174,12 @@ def _set_attribute_single(
 
         # if attribute is part of a (nested) repeating group, suffix name with index
         keyr = key
-        for i in index:  # one index for each nested level
-            if i > 0:
+        for i in index:  # one index for each nested level (unless it's a boolean group)
+            if i > 0 and keyr not in BOOL_GROUPS:
                 keyr += f"_{i:02d}"
 
         # get value of required number of bits at current payload offset
-        att, scale, _ = rtt.RTCM_DATA_FIELDS[key]
+        att, scale, _ = RTCM_DATA_FIELDS[key]
         if not self._scaling:
             scale = 0
         if key == "DF396":  # this MSM attribute has variable length
@@ -192,8 +202,11 @@ def _set_attribute_single(
         if key == "DF395":  # num of signals in MSM message
             setattr(self, NSIG, num_setbits(bitfield))
             setattr(self, NCELL, getattr(self, NSAT) * getattr(self, NSIG))
-        if key == "DF422":  # num of bias entries in 1230 message
-            setattr(self, NBIAS, num_setbits(bitfield))
+        if key == "DF422":  # bitmask of bias entries in 1230 message
+            setattr(self, NL1CA, bitfield[0])
+            setattr(self, NL1P, bitfield[1])
+            setattr(self, NL2CA, bitfield[2])
+            setattr(self, NL2P, bitfield[3])
 
         return offset
 
@@ -227,7 +240,7 @@ def __str__(self) -> str:
         # if MSM message and labelmsm flag is set,
         # label NSAT and NCELL group attributes with
         # corresponding satellite PRN and signal ID
-        if self._labelmsm and "MSM" in rtt.RTCM_MSGIDS[self.identity]:
+        if self._labelmsm and "MSM" in RTCM_MSGIDS[self.identity]:
             sats = sat2prn(self)
             cells = cell2prn(self)
             is_msm = True
@@ -238,15 +251,17 @@ def __str__(self) -> str:
         for i, att in enumerate(self.__dict__):
             if att[0] != "_":  # only show public attributes
                 val = self.__dict__[att]
-
+                # escape all byte chars
+                if isinstance(val, bytes):
+                    val = escapeall(val)
                 # label MSM NSAT and NCELL group attributes
                 lbl = ""
                 if is_msm:
                     aname = att2name(att)
-                    if aname in rtt.ATT_NSAT:
+                    if aname in ATT_NSAT:
                         prn = sats[att2idx(att)]
                         lbl = f"({prn})"
-                    if aname in rtt.ATT_NCELL:
+                    if aname in ATT_NCELL:
                         prn, sig = cells[att2idx(att)]
                         sig = "n/a" if sig is None else sig
                         lbl = f"({prn},{sig})"
@@ -297,7 +312,7 @@ def serialize(self) -> bytes:
         """
 
         size = len2bytes(self._payload)
-        message = rtt.RTCM_HDR + size + self._payload
+        message = RTCM_HDR + size + self._payload
         crc = crc2bytes(message)
         return message + crc
 
diff --git a/pyrtcm/rtcmreader.py b/pyrtcm/rtcmreader.py
index 304b747..e1f34d9 100644
--- a/pyrtcm/rtcmreader.py
+++ b/pyrtcm/rtcmreader.py
@@ -39,6 +39,7 @@ def __init__(self, datastream, **kwargs):
 
         :param datastream stream: input data stream
         :param int quitonerror: (kwarg) 0 = ignore,  1 = log and continue, 2 = (re)raise (1)
+        :param int errorhandler: (kwarg) error handling object or function (None)
         :param int validate: (kwarg) 0 = ignore invalid checksum, 1 = validate checksum (1)
         :param bool scaling: (kwarg) apply attribute scaling True/False (True)
         :param bool labelmsm: (kwarg) whether to label MSM NSAT and NCELL attributes (True)
@@ -52,6 +53,7 @@ def __init__(self, datastream, **kwargs):
         else:
             self._stream = datastream
         self._quitonerror = int(kwargs.get("quitonerror", rtt.ERR_LOG))
+        self._errorhandler = kwargs.get("errorhandler", None)
         self._validate = int(kwargs.get("validate", rtt.VALCKSUM))
         self._scaling = int(kwargs.get("scaling", True))
         self._labelmsm = int(kwargs.get("labelmsm", True))
@@ -65,15 +67,15 @@ def __next__(self) -> tuple:
         """
         Return next item in iteration.
 
-        :return: tuple of (raw_data as bytes, parsed_data as rtcmMessage)
+        :return: tuple of (raw_data as bytes, parsed_data as RTCMMessage)
         :rtype: tuple
         :raises: StopIteration
         """
 
         (raw_data, parsed_data) = self.read()
-        if raw_data is not None:
-            return (raw_data, parsed_data)
-        raise StopIteration
+        if raw_data is None and parsed_data is None:
+            raise StopIteration
+        return (raw_data, parsed_data)
 
     def read(self) -> tuple:
         """
@@ -114,14 +116,19 @@ def read(self) -> tuple:
                     parsing = False
                 # unrecognised protocol header
                 else:
-                    if self._quitonerror == rtt.ERR_RAISE:
-                        raise rte.RTCMStreamError(f"Unknown protocol {bytehdr}.")
-                    if self._quitonerror == rtt.ERR_LOG:
-                        return (bytehdr, f"<UNKNOWN PROTOCOL(header={bytehdr})>")
                     continue
 
         except EOFError:
             return (None, None)
+        except (
+            rte.RTCMMessageError,
+            rte.RTCMParseError,
+            rte.RTCMStreamError,
+            rte.RTCMTypeError,
+        ) as err:
+            if self._quitonerror:
+                self._do_error(str(err))
+            parsed_data = str(err)
 
         return (raw_data, parsed_data)
 
@@ -157,9 +164,7 @@ def _parse_nmea(self, hdr: bytes) -> tuple:
         """
 
         # read the rest of the NMEA message from the buffer
-        byten = self._stream.readline()  # NMEA protocol is CRLF-terminated
-        if byten[-2:] != b"\x0d\x0a":
-            raise EOFError()
+        byten = self._read_line()  # NMEA protocol is CRLF-terminated
         raw_data = hdr + byten
         parsed_data = None
         return (raw_data, parsed_data)
@@ -202,43 +207,53 @@ def _read_bytes(self, size: int) -> bytes:
             raise EOFError()
         return data
 
+    def _read_line(self) -> bytes:
+        """
+        Read until end of line (CRLF).
+
+        :return: bytes
+        :rtype: bytes
+        :raises: EOFError if stream ends prematurely
+        """
+
+        data = self._stream.readline()
+        if data[-2:] != b"\x0d\x0a":
+            raise EOFError()
+        return data
+
+    def _do_error(self, err: str):
+        """
+        Handle error.
+
+        :param str err: error message
+        :raises: RTCMParseError if quitonerror = 2
+        """
+
+        if self._quitonerror == rtt.ERR_RAISE:
+            raise rte.RTCMParseError(err)
+        if self._quitonerror == rtt.ERR_LOG:
+            # pass to error handler if there is one
+            if self._errorhandler is None:
+                print(err)
+            else:
+                self._errorhandler(err)
+
     def iterate(self, **kwargs) -> tuple:
         """
+        DEPRECATED - WILL BE REMOVED IN VERSION >=1.0.6
+        USE STANDARD ITERATOR INSTEAD
         Invoke the iterator within an exception handling framework.
 
-        :param int quitonerror: (kwarg) 0 = ignore,  1 = log and continue, 2 = (re)raise (1)
-        :param object errorhandler: (kwarg) Optional error handler (None)
         :return: tuple of (raw_data as bytes, parsed_data as RTCMMessage)
         :rtype: tuple
         :raises: RTCM... Error (if quitonerror is set and stream is invalid)
-
         """
 
-        quitonerror = kwargs.get("quitonerror", self._quitonerror)
-        errorhandler = kwargs.get("errorhandler", None)
-
         while True:
             try:
                 yield next(self)  # invoke the iterator
             except StopIteration:
                 break
-            except (
-                rte.RTCMMessageError,
-                rte.RTCMTypeError,
-                rte.RTCMParseError,
-                rte.RTCMStreamError,
-            ) as err:
-                # raise, log or ignore any error depending
-                # on the quitonerror setting
-                if quitonerror == rtt.ERR_RAISE:
-                    raise err
-                if quitonerror == rtt.ERR_LOG:
-                    # pass to error handler if there is one
-                    if errorhandler is None:
-                        print(err)
-                    else:
-                        errorhandler(err)
-                # continue
 
     @property
     def datastream(self) -> object:
diff --git a/pyrtcm/rtcmtypes_core.py b/pyrtcm/rtcmtypes_core.py
index 49a8690..3fd0d13 100644
--- a/pyrtcm/rtcmtypes_core.py
+++ b/pyrtcm/rtcmtypes_core.py
@@ -25,6 +25,20 @@
 ERR_LOG = 1
 ERR_IGNORE = 0
 
+NSAT = "NSat"
+NSIG = "NSig"
+NCELL = "_NCell"
+NL1CA = "_NL1CA"
+NL1P = "_NL1P"
+NL2CA = "_NL2CA"
+NL2P = "_NL2P"
+# Number of Residuals groups in MT1023 and MT1024
+NRES = 16
+# list of 'group' attributes which have
+# an occurrence of 0 or 1
+BOOL_GROUPS = ("DF423", "DF424", "DF425", "DF426")
+
+
 # Power of 2 scaling constants
 P2_P4 = 16  # 2**4
 P2_4 = 0.0625  # 2**-4
diff --git a/pyrtcm/rtcmtypes_get.py b/pyrtcm/rtcmtypes_get.py
index c9412d9..4acb69d 100644
--- a/pyrtcm/rtcmtypes_get.py
+++ b/pyrtcm/rtcmtypes_get.py
@@ -9,14 +9,15 @@
 """
 # pylint: disable=too-many-lines, line-too-long
 
-
-# attribute names holding size of MSM repeating groups
-NSAT = "NSat"
-NSIG = "NSig"
-NCELL = "_NCell"
-NBIAS = "_NBias"
-# Number of Residuals groups in MT1023 and MT1024
-NRES = 16
+from pyrtcm.rtcmtypes_core import (
+    NSAT,
+    NCELL,
+    NL1CA,
+    NL1P,
+    NL2CA,
+    NL2P,
+    NRES,
+)
 
 # *************************************************************
 # MSM MESSAGE SUB-SECTION DEFINITIONS
@@ -1492,16 +1493,29 @@
         "DF003": "Reference Station ID",
         "DF421": "GLONASS Code-Phase bias indicator",
         "DF001_3": "Reserved",
-        "DF422": "GLONASS FDMA signals mask",  # NBIAS = num bits set
-        # TODO verify that each iteration of this group only ever contains
-        # one of either DF423, DF424, DF425 or FD426?
-        "group": (
-            NBIAS,
+        "DF422": "GLONASS FDMA signals mask",
+        "groupL1CA": (
+            NL1CA,  # = 1 if bit 1 (MSB) in DF422 is set, otherwise 0
             {
                 "DF423": "GLONASS L1 C/A Code-Phase Bias",
-                # "DF424": "GLONASS L1 P Code-Phase Bias",
-                # "DF425": "GLONASS L2 C/A Code-Phase Bias",
-                # "DF426": "GLONASS L2 P Code-Phase Bias",
+            },
+        ),
+        "groupL1P": (
+            NL1P,  # = 1 if bit 2 in DF422 is set, otherwise 0
+            {
+                "DF424": "GLONASS L1 P Code-Phase Bias",
+            },
+        ),
+        "groupL2CA": (
+            NL2CA,  # = 1 if bit 3 in DF422 is set, otherwise 0
+            {
+                "DF425": "GLONASS L2 C/A Code-Phase Bias",
+            },
+        ),
+        "groupL2P": (
+            NL2P,  # = 1 if bit 4 (LSB) in DF422 is set, otherwise 0
+            {
+                "DF426": "GLONASS L2 P Code-Phase Bias",
             },
         ),
     },


Test patch:
diff --git a/tests/pygpsdata-1230.bin b/tests/pygpsdata-1230.bin
new file mode 100644
index 0000000..dd5082d
Binary files /dev/null and b/tests/pygpsdata-1230.bin differ
diff --git a/tests/test_socket.py b/tests/test_socket.py
index 90db230..2961a09 100644
--- a/tests/test_socket.py
+++ b/tests/test_socket.py
@@ -21,7 +21,6 @@ class DummySocket(socket):
     """
 
     def __init__(self, *args, **kwargs):
-
         self._timeout = False
         if "timeout" in kwargs:
             self._timeout = kwargs["timeout"]
@@ -44,7 +43,6 @@ def __init__(self, *args, **kwargs):
         self._buffer = self._stream
 
     def recv(self, num: int) -> bytes:
-
         if self._timeout:
             raise TimeoutError
         if len(self._buffer) < num:
@@ -85,7 +83,7 @@ def testSocketStub(self):
         rtr = RTCMReader(stream, bufsize=512)
         buff = rtr._stream.buffer  # test buffer getter method
         i = 0
-        for (raw, parsed) in rtr.iterate():
+        for raw, parsed in rtr:
             if raw is not None:
                 # print(parsed)
                 self.assertEqual(str(parsed), EXPECTED_RESULTS[i])
@@ -95,12 +93,11 @@ def testSocketStub(self):
         self.assertEqual(i, 12)
 
     def testSocketIter(self):  # test for extended stream
-
         raw = None
         stream = DummySocket()
         rtr = RTCMReader(stream)
         i = 0
-        for (raw, parsed) in rtr.iterate():
+        for raw, parsed in rtr:
             if raw is None:
                 raise EOFError
             i += 1
@@ -109,12 +106,11 @@ def testSocketIter(self):  # test for extended stream
         self.assertEqual(i, 123)
 
     def testSocketError(self):  # test for simulated socket timeout
-
         raw = None
         stream = DummySocket(timeout=True)
         rtr = RTCMReader(stream)
         i = 0
-        for (raw, parsed) in rtr.iterate():
+        for raw, parsed in rtr:
             i += 1
             if i >= 12:
                 break
diff --git a/tests/test_static.py b/tests/test_static.py
index 515ed4e..f03414f 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -32,6 +32,7 @@
     num_setbits,
     att2idx,
     att2name,
+    escapeall,
 )
 
 
@@ -191,7 +192,7 @@ def testdatascale(self):  # test datascale
             ds = datascale(dt)
             self.assertEqual(ds, EXPECTED_RESULT[i])
         # double check all the defined res are numbers
-        for (_, res, _) in RTCM_DATA_FIELDS.values():
+        for _, res, _ in RTCM_DATA_FIELDS.values():
             self.assertIsInstance(res, (int, float))
 
     def testdatadesc(self):  # test datadesc
@@ -239,6 +240,13 @@ def testatt2name(self):  # test att2name
             # print(res)
             self.assertEqual(res, EXPECTED_RESULT[i])
 
+    def testescapeall(self):
+        EXPECTED_RESULT = "b'\\x68\\x65\\x72\\x65\\x61\\x72\\x65\\x73\\x6f\\x6d\\x65\\x63\\x68\\x61\\x72\\x73'"
+        val = b"herearesomechars"
+        res = escapeall(val)
+        print(res)
+        self.assertEqual(res, EXPECTED_RESULT)
+
 
 if __name__ == "__main__":
     # import sys;sys.argv = ['', 'Test.testName']
diff --git a/tests/test_stream.py b/tests/test_stream.py
index 6fa0746..3077583 100644
--- a/tests/test_stream.py
+++ b/tests/test_stream.py
@@ -11,6 +11,7 @@
 
 import sys
 import os
+from io import StringIO
 import unittest
 
 from pyrtcm import RTCMReader, RTCMMessage
@@ -37,6 +38,23 @@ def setUp(self):
     def tearDown(self):
         pass
 
+    def catchio(self):
+        """
+        Capture stdout as string.
+        """
+
+        self._saved_stdout = sys.stdout
+        self._strout = StringIO()
+        sys.stdout = self._strout
+
+    def restoreio(self) -> str:
+        """
+        Return captured output and restore stdout.
+        """
+
+        sys.stdout = self._saved_stdout
+        return self._strout.getvalue().strip()
+
     def test1005example(
         self,
     ):  # test sample 1005 given in RTCM standard (with scaling applied)
@@ -54,6 +72,27 @@ def test1023(
         msg = RTCMReader.parse(self._raw1023)
         self.assertEqual(str(msg), EXPECTED_RESULT)
 
+    def test1230(
+        self,
+    ):  # test sample 1230
+        dirname = os.path.dirname(__file__)
+        stream = open(os.path.join(dirname, "pygpsdata-1230.bin"), "rb")
+        EXPECTED_RESULT = [
+            "<RTCM(1230, DF002=1230, DF003=0, DF421=0, DF001_3=0, DF422=15, DF423=0.0, DF424=0.0, DF425=0.0, DF426=0.0)>",
+            "<RTCM(1230, DF002=1230, DF003=0, DF421=0, DF001_3=0, DF422=15, DF423=0.0, DF424=0.0, DF425=0.0, DF426=0.0)>",
+            "<RTCM(1230, DF002=1230, DF003=0, DF421=0, DF001_3=0, DF422=15, DF423=0.0, DF424=0.0, DF425=0.0, DF426=0.0)>",
+            "<RTCM(1230, DF002=1230, DF003=0, DF421=0, DF001_3=0, DF422=15, DF423=0.0, DF424=0.0, DF425=0.0, DF426=0.0)>",
+            "<RTCM(1230, DF002=1230, DF003=0, DF421=0, DF001_3=0, DF422=11, DF423=0.0, DF425=0.0, DF426=0.0)>",
+        ]
+        rtr = RTCMReader(stream, quitonerror=2)
+        i = 0
+        for raw, parsed in rtr:
+            # print(f"{i} - {parsed}\n{raw}")
+            self.assertEqual(str(parsed), EXPECTED_RESULT[i])
+            i += 1
+        self.assertEqual(i, 5)
+        stream.close()
+
     def testMIXEDRTCM_NOSCALE(
         self,
     ):  # test mixed stream of NMEA, UBX & RTCM messages with no scaling applied
@@ -73,9 +112,8 @@ def testMIXEDRTCM_NOSCALE(
         i = 0
         raw = 0
         rtr = RTCMReader(stream, scaling=False, labelmsm=False)
-        for raw, parsed in rtr.iterate():
+        for raw, parsed in rtr:
             if raw is not None:
-                # print(parsed)
                 self.assertEqual(str(parsed), EXPECTED_RESULTS[i])
                 i += 1
         stream.close()
@@ -92,7 +130,7 @@ def testMIXEDRTCM_SCALE(
             "<RTCM(1127, DF002=1127, DF003=0, GNSSEpoch=204123001, DF393=0, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=198178247981137920, NSat=10, DF395=1074003968, NSig=2, DF396=387754, DF397_01=129, DF397_02=132, DF397_03=126, DF397_04=75, DF397_05=81, DF397_06=84, DF397_07=78, DF397_08=74, DF397_09=130, DF397_10=86, GNSSSpecific_01=0, GNSSSpecific_02=0, GNSSSpecific_03=0, GNSSSpecific_04=0, GNSSSpecific_05=0, GNSSSpecific_06=0, GNSSSpecific_07=0, GNSSSpecific_08=0, GNSSSpecific_09=0, GNSSSpecific_10=0, DF398_01=0.1171875, DF398_02=0.48144531, DF398_03=0.30957031, DF398_04=0.72558594, DF398_05=0.41015625, DF398_06=0.5703125, DF398_07=0.55957031, DF398_08=0.32226562, DF398_09=0.578125, DF398_10=0.67382812, DF399_01=-130, DF399_02=-58, DF399_03=-81, DF399_04=32, DF399_05=-398, DF399_06=436, DF399_07=-523, DF399_08=-65, DF399_09=-182, DF399_10=79, DF405_01=-0.00038854, DF405_02=0.0002273, DF405_03=0.00040366, DF405_04=0.00039607, DF405_05=-0.00016684, DF405_06=-4.755e-05, DF405_07=0.00036747, DF405_08=0.0002663, DF405_09=-0.00025026, DF405_10=-0.00011804, DF405_11=-0.00029377, DF405_12=-9.705e-05, DF405_13=-0.00060681, DF405_14=-7.59e-05, DF405_15=0.00076535, DF405_16=-1.958e-05, DF405_17=-0.00066777, DF405_18=-1.212e-05, DF405_19=0.00024992, DF405_20=0.00077083, DF406_01=0.00118631, DF406_02=0.00252339, DF406_03=-0.0017453, DF406_04=0.0027949, DF406_05=0.00260417, DF406_06=0.00260417, DF406_07=0.00260417, DF406_08=0.00260417, DF406_09=0.0019538, DF406_10=0.00220729, DF406_11=-0.00372297, DF406_12=0.00256628, DF406_13=4.2e-05, DF406_14=0.00080749, DF406_15=0.00038083, DF406_16=-0.00031753, DF406_17=-0.00283486, DF406_18=0.00330518, DF406_19=-0.00310773, DF406_20=-0.00117208, DF407_01=0, DF407_02=0, DF407_03=0, DF407_04=0, DF407_05=0, DF407_06=0, DF407_07=0, DF407_08=0, DF407_09=0, DF407_10=0, DF407_11=0, DF407_12=0, DF407_13=0, DF407_14=0, DF407_15=0, DF407_16=0, DF407_17=0, DF407_18=0, DF407_19=0, DF407_20=0, DF420_01=0, DF420_02=0, DF420_03=0, DF420_04=0, DF420_05=0, DF420_06=0, DF420_07=0, DF420_08=0, DF420_09=0, DF420_10=0, DF420_11=0, DF420_12=0, DF420_13=0, DF420_14=0, DF420_15=0, DF420_16=0, DF420_17=0, DF420_18=0, DF420_19=0, DF420_20=0, DF408_01=0, DF408_02=0, DF408_03=0, DF408_04=0, DF408_05=0, DF408_06=0, DF408_07=0, DF408_08=0, DF408_09=0, DF408_10=0, DF408_11=0, DF408_12=0, DF408_13=0, DF408_14=0, DF408_15=0, DF408_16=0, DF408_17=0, DF408_18=0, DF408_19=0, DF408_20=0, DF404_01=0.0, DF404_02=0.0, DF404_03=0.0, DF404_04=0.0, DF404_05=0.0, DF404_06=0.0, DF404_07=0.0, DF404_08=0.0, DF404_09=0.0, DF404_10=0.0, DF404_11=0.0, DF404_12=0.0, DF404_13=0.0, DF404_14=0.0, DF404_15=0.0, DF404_16=0.0, DF404_17=0.0, DF404_18=0.0, DF404_19=0.0, DF404_20=0.0)>",
             "<RTCM(1230, DF002=1230, DF003=0, DF421=1, DF001_3=0, DF422=0, )>",
             "<RTCM(1007, DF002=1007, DF003=1234, DF029=3, DF030_01=A, DF030_02=B, DF030_03=C, DF031=234)>",
-            "<RTCM(1117, )>",
+            "<RTCM(1117, DF002=1117, DF003=0, GNSSEpoch=385820000, DF393=1, DF409=0, DF001_7=0, DF411=1, DF412=0, DF417=0, DF418=0, DF394=7061644215716937728, NSat=3, DF395=1073774849, NSig=4, DF396=4095, DF397_01=140, DF397_02=112, DF397_03=133, GNSSSpecific_01=0, GNSSSpecific_02=0, GNSSSpecific_03=0, DF398_01=0.171875, DF398_02=0.125, DF398_03=0.87207031, DF399_01=481, DF399_02=-28, DF399_03=1, DF405_01=0.00022634, DF405_02=0.00026302, DF405_03=0.00024538, DF405_04=0.00023027, DF405_05=-3.85e-05, DF405_06=-8.33e-06, DF405_07=-2.259e-05, DF405_08=-3.289e-05, DF405_09=-9.806e-05, DF405_10=-7.163e-05, DF405_11=-8.577e-05, DF405_12=-9.572e-05, DF406_01=-0.00021978, DF406_02=0.00050089, DF406_03=-3.95e-06, DF406_04=0.00022199, DF406_05=0.00039012, DF406_06=-0.00017093, DF406_07=-0.00016689, DF406_08=-0.00018814, DF406_09=0.00028417, DF406_10=-0.0001959, DF406_11=0.0002057, DF406_12=0.00017754, DF407_01=516, DF407_02=462, DF407_03=684, DF407_04=365, DF407_05=648, DF407_06=649, DF407_07=649, DF407_08=649, DF407_09=568, DF407_10=704, DF407_11=704, DF407_12=704, DF420_01=0, DF420_02=0, DF420_03=0, DF420_04=0, DF420_05=0, DF420_06=0, DF420_07=0, DF420_08=0, DF420_09=0, DF420_10=0, DF420_11=0, DF420_12=0, DF408_01=7760, DF408_02=9184, DF408_03=8832, DF408_04=8832, DF408_05=12448, DF408_06=12928, DF408_07=13472, DF408_08=13104, DF408_09=9808, DF408_10=11184, DF408_11=11728, DF408_12=10320, DF404_01=-0.1064, DF404_02=-1.6384, DF404_03=-1.6384, DF404_04=-1.6384, DF404_05=-0.5875, DF404_06=-1.6384, DF404_07=-1.6384, DF404_08=-1.6384, DF404_09=-0.413, DF404_10=-1.6384, DF404_11=-1.6384, DF404_12=-1.6384)>",
         )
 
         dirname = os.path.dirname(__file__)
@@ -100,10 +138,9 @@ def testMIXEDRTCM_SCALE(
         i = 0
         raw = 0
         rtr = RTCMReader(stream, scaling=True, labelmsm=False)
-        for raw, parsed in rtr.iterate():
+        for raw, parsed in rtr:
             if raw is not None:
-                print(parsed)
-                # self.assertEqual(str(parsed), EXPECTED_RESULTS[i])
+                self.assertEqual(str(parsed), EXPECTED_RESULTS[i])
                 i += 1
         stream.close()
 
@@ -127,15 +164,38 @@ def testMIXEDRTCM_SCALE_LABELMSM(
         i = 0
         raw = 0
         rtr = RTCMReader(stream, scaling=True, labelmsm=True)
-        # stdout_saved = sys.stdout
-        # sys.stdout = open("output.txt", "w")
+        d = rtr.datastream
+        for raw, parsed in rtr:
+            if raw is not None:
+                self.assertEqual(str(parsed), EXPECTED_RESULTS[i])
+                i += 1
+        stream.close()
+
+    def testMIXEDRTCM_ITERATOR(
+        self,
+    ):  # test mixed stream of NMEA, UBX & RTCM messages with iterator()
+        EXPECTED_RESULTS = (
+            "<RTCM(1005, DF002=1005, DF003=0, DF021=0, DF022=1, DF023=1, DF024=1, DF141=0, DF025=4444030.8028, DF142=1, DF001_1=0, DF026=3085671.2349, DF364=0, DF027=3366658.256)>",
+            "<RTCM(4072, DF002=4072, Not_Yet_Implemented)>",
+            "<RTCM(1077, DF002=1077, DF003=0, GNSSEpoch=204137001, DF393=1, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=760738918298550272, NSat=10, DF395=1073807360, NSig=2, DF396=1044459, DF397_01(005)=75, DF397_02(007)=75, DF397_03(009)=81, DF397_04(013)=72, DF397_05(014)=67, DF397_06(015)=80, DF397_07(017)=75, DF397_08(019)=82, DF397_09(020)=75, DF397_10(030)=71, GNSSSpecific_01(005)=0, GNSSSpecific_02(007)=0, GNSSSpecific_03(009)=0, GNSSSpecific_04(013)=0, GNSSSpecific_05(014)=0, GNSSSpecific_06(015)=0, GNSSSpecific_07(017)=0, GNSSSpecific_08(019)=0, GNSSSpecific_09(020)=0, GNSSSpecific_10(030)=0, DF398_01(005)=0.00585938, DF398_02(007)=0.53417969, DF398_03(009)=0.76269531, DF398_04(013)=0.13867188, DF398_05(014)=0.54980469, DF398_06(015)=0.11328125, DF398_07(017)=0.80371094, DF398_08(019)=0.10253906, DF398_09(020)=0.52148438, DF398_10(030)=0.34570312, DF399_01(005)=-178, DF399_02(007)=-304, DF399_03(009)=-643, DF399_04(013)=477, DF399_05(014)=-52, DF399_06(015)=645, DF399_07(017)=529, DF399_08(019)=643, DF399_09(020)=-428, DF399_10(030)=-181, DF405_01(005,1C)=0.00014309, DF405_02(005,2L)=0.00014183, DF405_03(007,1C)=0.00038833, DF405_04(007,2L)=0.00038741, DF405_05(009,1C)=-0.00048384, DF405_06(009,2L)=-0.00046884, DF405_07(013,1C)=0.00034787, DF405_08(013,n/a)=0.00021969, DF405_09(014,1C)=0.00021521, DF405_10(014,2L)=-0.00018852, DF405_11(015,1C)=-0.00018319, DF405_12(015,2L)=-0.00010088, DF405_13(017,1C)=-9.844e-05, DF405_14(017,2L)=0.00047875, DF405_15(019,1C)=0.00043665, DF405_16(019,n/a)=-0.00031057, DF405_17(020,1C)=-0.00030866, DF405_18(020,n/a)=3.548e-05, DF405_19(030,1C)=0.00012431, DF405_20(030,2L)=-0.00016747, DF406_01(005,1C)=-0.00247183, DF406_02(005,2L)=0.00097835, DF406_03(007,1C)=0.00030125, DF406_04(007,2L)=-0.00390617, DF406_05(009,1C)=0.00095563, DF406_06(009,2L)=0.00061994, DF406_07(013,1C)=0.00181956, DF406_08(013,n/a)=0.00139041, DF406_09(014,1C)=-0.00161936, DF406_10(014,2L)=-0.00306895, DF406_11(015,1C)=0.00010691, DF406_12(015,2L)=0.0034257, DF406_13(017,1C)=0.00296013, DF406_14(017,2L)=-0.00152405, DF406_15(019,1C)=1.399e-05, DF406_16(019,n/a)=0.00260417, DF406_17(020,1C)=0.00260417, DF406_18(020,n/a)=0.00260417, DF406_19(030,1C)=0.00260417, DF406_20(030,2L)=0.00260417, DF407_01(005,1C)=157, DF407_02(005,2L)=341, DF407_03(007,1C)=341, DF407_04(007,2L)=340, DF407_05(009,1C)=0, DF407_06(009,2L)=22, DF407_07(013,1C)=531, DF407_08(013,n/a)=21, DF407_09(014,1C)=531, DF407_10(014,2L)=531, DF407_11(015,1C)=530, DF407_12(015,2L)=534, DF407_13(017,1C)=535, DF407_14(017,2L)=23, DF407_15(019,1C)=19, DF407_16(019,n/a)=529, DF407_17(020,1C)=22, DF407_18(020,n/a)=531, DF407_19(030,1C)=15, DF407_20(030,2L)=534, DF420_01(005,1C)=1, DF420_02(005,2L)=0, DF420_03(007,1C)=0, DF420_04(007,2L)=0, DF420_05(009,1C)=0, DF420_06(009,2L)=1, DF420_07(013,1C)=0, DF420_08(013,n/a)=1, DF420_09(014,1C)=1, DF420_10(014,2L)=1, DF420_11(015,1C)=0, DF420_12(015,2L)=0, DF420_13(017,1C)=0, DF420_14(017,2L)=0, DF420_15(019,1C)=0, DF420_16(019,n/a)=1, DF420_17(020,1C)=0, DF420_18(020,n/a)=1, DF420_19(030,1C)=0, DF420_20(030,2L)=0, DF408_01(005,1C)=8544, DF408_02(005,2L)=16144, DF408_03(007,1C)=11776, DF408_04(007,2L)=11632, DF408_05(009,1C)=14320, DF408_06(009,2L)=12208, DF408_07(013,1C)=13744, DF408_08(013,n/a)=16208, DF408_09(014,1C)=14336, DF408_10(014,2L)=12320, DF408_11(015,1C)=6448, DF408_12(015,2L)=16032, DF408_13(017,1C)=6128, DF408_14(017,2L)=5440, DF408_15(019,1C)=4688, DF408_16(019,n/a)=7312, DF408_17(020,1C)=3216, DF408_18(020,n/a)=1184, DF408_19(030,1C)=8224, DF408_20(030,2L)=5888, DF404_01(005,1C)=0.6636, DF404_02(005,2L)=-0.9556, DF404_03(007,1C)=-0.2148, DF404_04(007,2L)=-0.2174, DF404_05(009,1C)=0.0, DF404_06(009,2L)=0.0, DF404_07(013,1C)=0.0, DF404_08(013,n/a)=0.0, DF404_09(014,1C)=0.0, DF404_10(014,2L)=0.0, DF404_11(015,1C)=0.0, DF404_12(015,2L)=0.0, DF404_13(017,1C)=0.0, DF404_14(017,2L)=0.0, DF404_15(019,1C)=0.0, DF404_16(019,n/a)=0.0, DF404_17(020,1C)=0.0, DF404_18(020,n/a)=0.0, DF404_19(030,1C)=0.0, DF404_20(030,2L)=0.0)>",
+            "<RTCM(1087, DF002=1087, DF003=0, GNSSEpoch=310554457, DF393=1, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=4039168114821169152, NSat=7, DF395=1090519040, NSig=2, DF396=16382, DF397_01(003)=69, DF397_02(004)=64, DF397_03(005)=73, DF397_04(013)=76, DF397_05(014)=66, DF397_06(015)=70, DF397_07(023)=78, GNSSSpecific_01(003)=12, GNSSSpecific_02(004)=13, GNSSSpecific_03(005)=8, GNSSSpecific_04(013)=5, GNSSSpecific_05(014)=0, GNSSSpecific_06(015)=7, GNSSSpecific_07(023)=10, DF398_01(003)=0.63378906, DF398_02(004)=0.34277344, DF398_03(005)=0.25390625, DF398_04(013)=0.31054688, DF398_05(014)=0.51269531, DF398_06(015)=0.82714844, DF398_07(023)=0.88378906, DF399_01(003)=-665, DF399_02(004)=29, DF399_03(005)=672, DF399_04(013)=-573, DF399_05(014)=-211, DF399_06(015)=312, DF399_07(023)=317, DF405_01(003,1C)=0.00024936, DF405_02(003,2C)=0.00025116, DF405_03(004,1C)=-4.679e-05, DF405_04(004,2C)=-5.142e-05, DF405_05(005,1C)=1.114e-05, DF405_06(005,2C)=2.15e-05, DF405_07(013,1C)=0.00047079, DF405_08(013,2C)=0.0004795, DF405_09(014,1C)=-0.00038792, DF405_10(014,2C)=-0.00037604, DF405_11(015,1C)=0.00027718, DF405_12(015,2C)=0.00028714, DF405_13(023,1C)=-0.00023612, DF405_14(023,n/a)=6.234e-05, DF406_01(003,1C)=0.0019688, DF406_02(003,2C)=0.00341495, DF406_03(004,1C)=0.0024382, DF406_04(004,2C)=-0.00097587, DF406_05(005,1C)=0.00341933, DF406_06(005,2C)=0.00295908, DF406_07(013,1C)=-0.0024111, DF406_08(013,2C)=0.00388202, DF406_09(014,1C)=-0.00344146, DF406_10(014,2C)=-0.00291232, DF406_11(015,1C)=-0.00193512, DF406_12(015,2C)=0.00145003, DF406_13(023,1C)=-0.00276693, DF406_14(023,n/a)=0.00259653, DF407_01(003,1C)=341, DF407_02(003,2C)=341, DF407_03(004,1C)=325, DF407_04(004,2C)=341, DF407_05(005,1C)=341, DF407_06(005,2C)=341, DF407_07(013,1C)=341, DF407_08(013,2C)=341, DF407_09(014,1C)=320, DF407_10(014,2C)=1, DF407_11(015,1C)=481, DF407_12(015,2C)=257, DF407_13(023,1C)=481, DF407_14(023,n/a)=321, DF420_01(003,1C)=0, DF420_02(003,2C)=1, DF420_03(004,1C)=1, DF420_04(004,2C)=1, DF420_05(005,1C)=1, DF420_06(005,2C)=0, DF420_07(013,1C)=0, DF420_08(013,2C)=0, DF420_09(014,1C)=0, DF420_10(014,2C)=1, DF420_11(015,1C)=0, DF420_12(015,2C)=0, DF420_13(023,1C)=1, DF420_14(023,n/a)=1, DF408_01(003,1C)=8480, DF408_02(003,2C)=256, DF408_03(004,1C)=8576, DF408_04(004,2C)=336, DF408_05(005,1C)=8576, DF408_06(005,2C)=320, DF408_07(013,1C)=320, DF408_08(013,2C)=8560, DF408_09(014,1C)=16368, DF408_10(014,2C)=12288, DF408_11(015,1C)=9856, DF408_12(015,2C)=5552, DF408_13(023,1C)=4240, DF408_14(023,n/a)=10864, DF404_01(003,1C)=0.5287, DF404_02(003,2C)=0.4605, DF404_03(004,1C)=-1.4211, DF404_04(004,2C)=-1.5811, DF404_05(005,1C)=-0.3491, DF404_06(005,2C)=-0.4157, DF404_07(013,1C)=-0.5501, DF404_08(013,2C)=-0.4762, DF404_09(014,1C)=0.0064, DF404_10(014,2C)=0.0, DF404_11(015,1C)=0.0, DF404_12(015,2C)=0.0, DF404_13(023,1C)=0.0, DF404_14(023,n/a)=0.0)>",
+            "<RTCM(1097, DF002=1097, DF003=0, GNSSEpoch=204137001, DF393=1, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=216181732825628672, NSat=5, DF395=1073872896, NSig=2, DF396=1023, DF397_01(007)=79, DF397_02(008)=84, DF397_03(021)=89, DF397_04(027)=78, DF397_05(030)=83, GNSSSpecific_01(007)=0, GNSSSpecific_02(008)=0, GNSSSpecific_03(021)=0, GNSSSpecific_04(027)=0, GNSSSpecific_05(030)=0, DF398_01(007)=0.15625, DF398_02(008)=0.25097656, DF398_03(021)=0.35449219, DF398_04(027)=0.37109375, DF398_05(030)=0.25976562, DF399_01(007)=-198, DF399_02(008)=-516, DF399_03(021)=423, DF399_04(027)=63, DF399_05(030)=-384, DF405_01(007,1C)=-4.54e-05, DF405_02(007,7Q)=-2.825e-05, DF405_03(008,1C)=-0.00034597, DF405_04(008,7Q)=-0.00032683, DF405_05(021,1C)=0.00048097, DF405_06(021,7Q)=0.00050125, DF405_07(027,1C)=-0.00013696, DF405_08(027,7Q)=-0.00012604, DF405_09(030,1C)=-1.844e-05, DF405_10(030,7Q)=-3.04e-06, DF406_01(007,1C)=-4.445e-05, DF406_02(007,7Q)=-2.835e-05, DF406_03(008,1C)=-0.00035255, DF406_04(008,7Q)=-0.00032637, DF406_05(021,1C)=0.00048204, DF406_06(021,7Q)=0.00050084, DF406_07(027,1C)=-0.00013757, DF406_08(027,7Q)=-0.00012636, DF406_09(030,1C)=-1.804e-05, DF406_10(030,7Q)=-3.29e-06, DF407_01(007,1C)=341, DF407_02(007,7Q)=341, DF407_03(008,1C)=341, DF407_04(008,7Q)=341, DF407_05(021,1C)=341, DF407_06(021,7Q)=341, DF407_07(027,1C)=341, DF407_08(027,7Q)=341, DF407_09(030,1C)=341, DF407_10(030,7Q)=341, DF420_01(007,1C)=0, DF420_02(007,7Q)=0, DF420_03(008,1C)=0, DF420_04(008,7Q)=0, DF420_05(021,1C)=0, DF420_06(021,7Q)=0, DF420_07(027,1C)=0, DF420_08(027,7Q)=0, DF420_09(030,1C)=0, DF420_10(030,7Q)=0, DF408_01(007,1C)=11776, DF408_02(007,7Q)=12544, DF408_03(008,1C)=10496, DF408_04(008,7Q)=11008, DF408_05(021,1C)=11008, DF408_06(021,7Q)=11008, DF408_07(027,1C)=11520, DF408_08(027,7Q)=12544, DF408_09(030,1C)=11008, DF408_10(030,7Q)=12032, DF404_01(007,1C)=-0.5806, DF404_02(007,7Q)=-0.5831, DF404_03(008,1C)=-0.7947, DF404_04(008,7Q)=-0.7943, DF404_05(021,1C)=0.7243, DF404_06(021,7Q)=0.7174, DF404_07(027,1C)=0.5534, DF404_08(027,7Q)=0.5545, DF404_09(030,1C)=-0.7726, DF404_10(030,7Q)=-0.7733)>",
+            "<RTCM(1127, DF002=1127, DF003=0, GNSSEpoch=204123001, DF393=0, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=198178247981137920, NSat=10, DF395=1074003968, NSig=2, DF396=387754, DF397_01(007)=129, DF397_02(009)=132, DF397_03(010)=126, DF397_04(020)=75, DF397_05(023)=81, DF397_06(028)=84, DF397_07(032)=78, DF397_08(037)=74, DF397_09(040)=130, DF397_10(043)=86, GNSSSpecific_01(007)=0, GNSSSpecific_02(009)=0, GNSSSpecific_03(010)=0, GNSSSpecific_04(020)=0, GNSSSpecific_05(023)=0, GNSSSpecific_06(028)=0, GNSSSpecific_07(032)=0, GNSSSpecific_08(037)=0, GNSSSpecific_09(040)=0, GNSSSpecific_10(043)=0, DF398_01(007)=0.1171875, DF398_02(009)=0.48144531, DF398_03(010)=0.30957031, DF398_04(020)=0.72558594, DF398_05(023)=0.41015625, DF398_06(028)=0.5703125, DF398_07(032)=0.55957031, DF398_08(037)=0.32226562, DF398_09(040)=0.578125, DF398_10(043)=0.67382812, DF399_01(007)=-130, DF399_02(009)=-58, DF399_03(010)=-81, DF399_04(020)=32, DF399_05(023)=-398, DF399_06(028)=436, DF399_07(032)=-523, DF399_08(037)=-65, DF399_09(040)=-182, DF399_10(043)=79, DF405_01(007,n/a)=-0.00038854, DF405_02(007,7I)=0.0002273, DF405_03(009,n/a)=0.00040366, DF405_04(009,7I)=0.00039607, DF405_05(010,2I)=-0.00016684, DF405_06(010,7I)=-4.755e-05, DF405_07(020,2I)=0.00036747, DF405_08(020,n/a)=0.0002663, DF405_09(023,2I)=-0.00025026, DF405_10(023,n/a)=-0.00011804, DF405_11(028,2I)=-0.00029377, DF405_12(028,n/a)=-9.705e-05, DF405_13(032,2I)=-0.00060681, DF405_14(032,n/a)=-7.59e-05, DF405_15(037,2I)=0.00076535, DF405_16(037,n/a)=-1.958e-05, DF405_17(040,2I)=-0.00066777, DF405_18(040,n/a)=-1.212e-05, DF405_19(043,2I)=0.00024992, DF405_20(043,n/a)=0.00077083, DF406_01(007,n/a)=0.00118631, DF406_02(007,7I)=0.00252339, DF406_03(009,n/a)=-0.0017453, DF406_04(009,7I)=0.0027949, DF406_05(010,2I)=0.00260417, DF406_06(010,7I)=0.00260417, DF406_07(020,2I)=0.00260417, DF406_08(020,n/a)=0.00260417, DF406_09(023,2I)=0.0019538, DF406_10(023,n/a)=0.00220729, DF406_11(028,2I)=-0.00372297, DF406_12(028,n/a)=0.00256628, DF406_13(032,2I)=4.2e-05, DF406_14(032,n/a)=0.00080749, DF406_15(037,2I)=0.00038083, DF406_16(037,n/a)=-0.00031753, DF406_17(040,2I)=-0.00283486, DF406_18(040,n/a)=0.00330518, DF406_19(043,2I)=-0.00310773, DF406_20(043,n/a)=-0.00117208, DF407_01(007,n/a)=0, DF407_02(007,7I)=0, DF407_03(009,n/a)=0, DF407_04(009,7I)=0, DF407_05(010,2I)=0, DF407_06(010,7I)=0, DF407_07(020,2I)=0, DF407_08(020,n/a)=0, DF407_09(023,2I)=0, DF407_10(023,n/a)=0, DF407_11(028,2I)=0, DF407_12(028,n/a)=0, DF407_13(032,2I)=0, DF407_14(032,n/a)=0, DF407_15(037,2I)=0, DF407_16(037,n/a)=0, DF407_17(040,2I)=0, DF407_18(040,n/a)=0, DF407_19(043,2I)=0, DF407_20(043,n/a)=0, DF420_01(007,n/a)=0, DF420_02(007,7I)=0, DF420_03(009,n/a)=0, DF420_04(009,7I)=0, DF420_05(010,2I)=0, DF420_06(010,7I)=0, DF420_07(020,2I)=0, DF420_08(020,n/a)=0, DF420_09(023,2I)=0, DF420_10(023,n/a)=0, DF420_11(028,2I)=0, DF420_12(028,n/a)=0, DF420_13(032,2I)=0, DF420_14(032,n/a)=0, DF420_15(037,2I)=0, DF420_16(037,n/a)=0, DF420_17(040,2I)=0, DF420_18(040,n/a)=0, DF420_19(043,2I)=0, DF420_20(043,n/a)=0, DF408_01(007,n/a)=0, DF408_02(007,7I)=0, DF408_03(009,n/a)=0, DF408_04(009,7I)=0, DF408_05(010,2I)=0, DF408_06(010,7I)=0, DF408_07(020,2I)=0, DF408_08(020,n/a)=0, DF408_09(023,2I)=0, DF408_10(023,n/a)=0, DF408_11(028,2I)=0, DF408_12(028,n/a)=0, DF408_13(032,2I)=0, DF408_14(032,n/a)=0, DF408_15(037,2I)=0, DF408_16(037,n/a)=0, DF408_17(040,2I)=0, DF408_18(040,n/a)=0, DF408_19(043,2I)=0, DF408_20(043,n/a)=0, DF404_01(007,n/a)=0.0, DF404_02(007,7I)=0.0, DF404_03(009,n/a)=0.0, DF404_04(009,7I)=0.0, DF404_05(010,2I)=0.0, DF404_06(010,7I)=0.0, DF404_07(020,2I)=0.0, DF404_08(020,n/a)=0.0, DF404_09(023,2I)=0.0, DF404_10(023,n/a)=0.0, DF404_11(028,2I)=0.0, DF404_12(028,n/a)=0.0, DF404_13(032,2I)=0.0, DF404_14(032,n/a)=0.0, DF404_15(037,2I)=0.0, DF404_16(037,n/a)=0.0, DF404_17(040,2I)=0.0, DF404_18(040,n/a)=0.0, DF404_19(043,2I)=0.0, DF404_20(043,n/a)=0.0)>",
+            "<RTCM(1230, DF002=1230, DF003=0, DF421=1, DF001_3=0, DF422=0, )>",
+            "<RTCM(1007, DF002=1007, DF003=1234, DF029=3, DF030_01=A, DF030_02=B, DF030_03=C, DF031=234)>",
+            "<RTCM(1117, DF002=1117, DF003=0, GNSSEpoch=385820000, DF393=1, DF409=0, DF001_7=0, DF411=1, DF412=0, DF417=0, DF418=0, DF394=7061644215716937728, NSat=3, DF395=1073774849, NSig=4, DF396=4095, DF397_01(194)=140, DF397_02(195)=112, DF397_03(199)=133, GNSSSpecific_01(194)=0, GNSSSpecific_02(195)=0, GNSSSpecific_03(199)=0, DF398_01(194)=0.171875, DF398_02(195)=0.125, DF398_03(199)=0.87207031, DF399_01(194)=481, DF399_02(195)=-28, DF399_03(199)=1, DF405_01(194,1C)=0.00022634, DF405_02(194,2X)=0.00026302, DF405_03(194,5X)=0.00024538, DF405_04(194,1X)=0.00023027, DF405_05(195,1C)=-3.85e-05, DF405_06(195,2X)=-8.33e-06, DF405_07(195,5X)=-2.259e-05, DF405_08(195,1X)=-3.289e-05, DF405_09(199,1C)=-9.806e-05, DF405_10(199,2X)=-7.163e-05, DF405_11(199,5X)=-8.577e-05, DF405_12(199,1X)=-9.572e-05, DF406_01(194,1C)=-0.00021978, DF406_02(194,2X)=0.00050089, DF406_03(194,5X)=-3.95e-06, DF406_04(194,1X)=0.00022199, DF406_05(195,1C)=0.00039012, DF406_06(195,2X)=-0.00017093, DF406_07(195,5X)=-0.00016689, DF406_08(195,1X)=-0.00018814, DF406_09(199,1C)=0.00028417, DF406_10(199,2X)=-0.0001959, DF406_11(199,5X)=0.0002057, DF406_12(199,1X)=0.00017754, DF407_01(194,1C)=516, DF407_02(194,2X)=462, DF407_03(194,5X)=684, DF407_04(194,1X)=365, DF407_05(195,1C)=648, DF407_06(195,2X)=649, DF407_07(195,5X)=649, DF407_08(195,1X)=649, DF407_09(199,1C)=568, DF407_10(199,2X)=704, DF407_11(199,5X)=704, DF407_12(199,1X)=704, DF420_01(194,1C)=0, DF420_02(194,2X)=0, DF420_03(194,5X)=0, DF420_04(194,1X)=0, DF420_05(195,1C)=0, DF420_06(195,2X)=0, DF420_07(195,5X)=0, DF420_08(195,1X)=0, DF420_09(199,1C)=0, DF420_10(199,2X)=0, DF420_11(199,5X)=0, DF420_12(199,1X)=0, DF408_01(194,1C)=7760, DF408_02(194,2X)=9184, DF408_03(194,5X)=8832, DF408_04(194,1X)=8832, DF408_05(195,1C)=12448, DF408_06(195,2X)=12928, DF408_07(195,5X)=13472, DF408_08(195,1X)=13104, DF408_09(199,1C)=9808, DF408_10(199,2X)=11184, DF408_11(199,5X)=11728, DF408_12(199,1X)=10320, DF404_01(194,1C)=-0.1064, DF404_02(194,2X)=-1.6384, DF404_03(194,5X)=-1.6384, DF404_04(194,1X)=-1.6384, DF404_05(195,1C)=-0.5875, DF404_06(195,2X)=-1.6384, DF404_07(195,5X)=-1.6384, DF404_08(195,1X)=-1.6384, DF404_09(199,1C)=-0.413, DF404_10(199,2X)=-1.6384, DF404_11(199,5X)=-1.6384, DF404_12(199,1X)=-1.6384)>",
+        )
+
+        dirname = os.path.dirname(__file__)
+        stream = open(os.path.join(dirname, "pygpsdata-RTCM3.log"), "rb")
+        i = 0
+        raw = 0
+        rtr = RTCMReader(stream, scaling=True, labelmsm=True)
         for raw, parsed in rtr.iterate():
             if raw is not None:
-                # print(parsed)
                 self.assertEqual(str(parsed), EXPECTED_RESULTS[i])
                 i += 1
         stream.close()
-        # sys.stdout = stdout_saved
 
     def testNTRIP(
         self,
@@ -145,16 +205,12 @@ def testNTRIP(
         stream = open(os.path.join(dirname, "pygpsdata-NTRIP.log"), "rb")
         i = 0
         raw = 0
-        rtr = RTCMReader(stream)
-        # stdout_saved = sys.stdout
-        # sys.stdout = open("output.txt", "w")
-        for raw, parsed in rtr.iterate(quitonerror=2):
+        rtr = RTCMReader(stream, quitonerror=rtt.ERR_RAISE)
+        for raw, parsed in rtr:
             if raw is not None:
-                # print(parsed)
                 i += 1
         self.assertEqual(str(parsed), EXPECTED_RESULTS)
         stream.close()
-        # sys.stdout = stdout_saved
 
     def testNTRIP(
         self,
@@ -239,15 +295,12 @@ def testNTRIP(
         i = 0
         raw = 0
         rtr = RTCMReader(stream, scaling=True, labelmsm=True)
-        # stdout_saved = sys.stdout
-        # sys.stdout = open("output.txt", "w")
-        for raw, parsed in rtr.iterate():
+        for raw, parsed in rtr:
             if raw is not None:
                 # print(parsed)
                 self.assertEqual(f"{parsed.identity}", EXPECTED_RESULTS[i])
                 i += 1
         stream.close()
-        # sys.stdout = stdout_saved
 
     def testSerialize(self):  # test serialize()
         payload = self._raw1005[3:-3]
@@ -303,14 +356,54 @@ def testbadCRC(
         stream = open(os.path.join(dirname, "pygpsdata-MIXED-RTCM3BADCRC.log"), "rb")
         i = 0
         raw = 0
-        rtr = RTCMReader(stream, protfilter=7)
+        rtr = RTCMReader(stream, protfilter=7, quitonerror=rtt.ERR_RAISE)
         with self.assertRaisesRegex(rte.RTCMParseError, EXPECTED_ERROR):
-            for raw, parsed in rtr.iterate(quitonerror=rtt.ERR_RAISE):
+            for raw, parsed in rtr:
                 if raw is not None:
                     print(parsed)
                     i += 1
         stream.close()
 
+    def testbadCRC2(
+        self,
+    ):  # test mixed stream of NMEA, UBX & RTCM messages with invalid RTCM CRC
+        EXPECTED_ERROR = "I ignored the following error: RTCM3 message invalid - failed CRC: b'Z\\xd7\\xf7'"
+        dirname = os.path.dirname(__file__)
+        stream = open(os.path.join(dirname, "pygpsdata-MIXED-RTCM3BADCRC.log"), "rb")
+        self.catchio()
+        rtr = RTCMReader(
+            stream,
+            protfilter=7,
+            quitonerror=rtt.ERR_LOG,
+            errorhandler=lambda e: print(f"I ignored the following error: {e}"),
+        )
+        for raw, parsed in rtr:
+            if raw is not None:
+                pass
+        stream.close()
+        output = self.restoreio()
+        self.assertEqual(output, EXPECTED_ERROR)
+
+    def testbadCRC3(
+        self,
+    ):  # test mixed stream of NMEA, UBX & RTCM messages with invalid RTCM CRC
+        EXPECTED_ERROR = "RTCM3 message invalid - failed CRC: b'Z\\xd7\\xf7'"
+        dirname = os.path.dirname(__file__)
+        stream = open(os.path.join(dirname, "pygpsdata-MIXED-RTCM3BADCRC.log"), "rb")
+        self.catchio()
+        rtr = RTCMReader(
+            stream,
+            protfilter=7,
+            quitonerror=rtt.ERR_LOG,
+            errorhandler=None,
+        )
+        for raw, parsed in rtr:
+            if raw is not None:
+                pass
+        stream.close()
+        output = self.restoreio()
+        self.assertEqual(output, EXPECTED_ERROR)
+
 
 if __name__ == "__main__":
     # import sys;sys.argv = ['', 'Test.testName']