Repository: semuconsulting/pyrtcm (language: python)

Issue:
Ntrip client + pyrtcm
RTCM data can also be obtained from RTCM sources using RTCM clients such as https://github.com/nunojpg/ntripclient or https://github.com/liukai-tech/NtripClient-Tools.
Is it possible to submit a stream from an RTCM client to pyrtcm?


Discussion / hints:
Hi @foxittt Sorry, no. `pyrtcm` can parse RTCM3 data from any Serial (UART) data stream or binary datafile, but not (for example) an HTTP/TCP socket, so it would be necessary to convert any NTRIP data stream into one or other of these accepted formats before pyrtcm could parse it.

There are no current plans to add such functionality, though I'll try to look into what might be involved when I get some time and if there appears to be sufficient interest.
Re-opened as I think this merits further investigation,  but no guarantee on timescale at this point.
Just found this library with a similar interest in mind. I'm writing a little robot navigation program, that's connecting to an NTRIP client, and then I want to feed the output of the NTRIP session into pyrtcm, and then yield complete RTCM messages that can be forwarded to a ublox GPS module. 

It would be nice to be able to feed data into the RTCMReader, and then async await any complete messages. Will be taking a closer look shortly.
Hi @jakepoz  (and @foxittt)

**FYI** I'm working on a simple command line NTRIP client and hope to upload a preliminary version into the `\examples` folder in the next week or so. It will connect to a designated NTRIP server (caster), parse the header response and RTCM3 output from that server, and handle the uploading of NMEA GGA position records (using the companion `pynmeagps` library).

Watch this space.

Here's a sneak preview of the output:

```
Connected to: 3.23.52.207:2101
GET /AEAUK HTTP/1.1
User-Agent: pyrtcm NTRIP client/0.1
Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Ntrip-Version: Ntrip/2.0


*** Start Of Header ***
HTTP/1.1 200 OK
Date: Mon, 28 March 2022 22:22:13 UTC
Server: SubCarrier Systems Corp SNIP simpleNTRIP_Caster_[3.04.34]RwPRO
Ntrip-Version: Ntrip/2.0
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
Connection: close
Content-Type: gnss/data
*** End Of Header ***

Data bytes received: 304
<RTCM(1074, DF002=1074, DF003=0, GNSSEpoch=166958000, DF393=1, DF409=4, DF001_7=0, DF411=1, DF412=0, DF417=0, DF418=0, DF394=13011088448063275008, NSat=11, DF395=1346437120, NSig=4, DF396=17592186040047, DF400_01=0.00054365, DF400_02=-0.00070077, DF400_03=-0.00082338, DF400_04=0.00080037, DF400_05=0.00089228, DF400_06=-0.00095767, DF400_07=0.00072014, DF400_08=-0.00062102, DF400_09=0.00079465, DF400_10=-0.00061792, DF400_11=0.00063735, DF400_12=0.0006839, DF400_13=-0.00078541, DF400_14=-0.00020713, DF400_15=-0.00020695, DF400_16=3.862e-05, DF400_17=-0.00093812, DF400_18=0.00096422, DF400_19=-0.00074476, DF400_20=0.00047678, DF400_21=0.00023258, DF400_22=0.00022769, DF400_23=-0.00074893, DF400_24=0.00022799, DF400_25=-1.621e-05, DF400_26=0.00024801, DF400_27=-0.00024045, DF400_28=5.19e-06, DF400_29=0.00049347, DF400_30=0.00068736, DF400_31=0.00019866, DF400_32=-0.00077677, DF400_33=-0.00028884, DF400_34=-0.00026464, DF400_35=-0.0007531, DF400_36=0.00022393, DF400_37=0.00095648, DF400_38=0.00072396, DF400_39=0.00023574, DF400_40=-0.00025272, DF400_41=-0.00025266, DF400_42=0.00067472, DF400_43=0.00091863, DF400_44=0.00091839, DF401_01=-0.00188884, DF401_02=3.839e-05, DF401_03=-0.00300141, DF401_04=-0.00141553, DF401_05=0.00263151, DF401_06=0.00089338, DF401_07=0.00229209, DF401_08=0.0006876, DF401_09=0.0006876, DF401_10=0.00074765, DF401_11=0.00073787, DF401_12=-0.00010199, DF401_13=-0.00010198, DF401_14=-4.435e-05, DF401_15=-2.805e-05, DF401_16=-0.0002424, DF401_17=-0.0002424, DF401_18=-0.000231, DF401_19=-0.00022614, DF401_20=8.828e-05, DF401_21=8.831e-05, DF401_22=0.00012761, DF401_23=0.00015692, DF401_24=-0.00070773, DF401_25=-0.00070772, DF401_26=-0.00069049, DF401_27=-0.00070195, DF401_28=-0.00032431, DF401_29=-0.00032431, DF401_30=-0.00032484, DF401_31=-0.00031344, DF401_32=-6.229e-05, DF401_33=-6.229e-05, DF401_34=-1.779e-05, DF401_35=-2.755e-05, DF401_36=-0.00084885, DF401_37=-0.00084886, DF401_38=-0.00086509, DF401_39=0.00042122, DF401_40=0.00042121, DF401_41=0.00036823, DF401_42=-0.00050283, DF401_43=-0.00050284, DF401_44=-0.00046498, DF402_01=14, DF402_02=4, DF402_03=5, DF402_04=12, DF402_05=2, DF402_06=15, DF402_07=9, DF402_08=1, DF402_09=7, DF402_10=1, DF402_11=5, DF402_12=14, DF402_13=4, DF402_14=9, DF402_15=8, DF402_16=4, DF402_17=15, DF402_18=9, DF402_19=4, DF402_20=1, DF402_21=7, DF402_22=3, DF402_23=15, DF402_24=15, DF402_25=15, DF402_26=15, DF402_27=15, DF402_28=15, DF402_29=15, DF402_30=15, DF402_31=15, DF402_32=15, DF402_33=15, DF402_34=15, DF402_35=15, DF402_36=15, DF402_37=15, DF402_38=15, DF402_39=15, DF402_40=15, DF402_41=15, DF402_42=15, DF402_43=15, DF402_44=15, DF420_01=1, DF420_02=1, DF420_03=1, DF420_04=1, DF420_05=1, DF420_06=1, DF420_07=1, DF420_08=1, DF420_09=1, DF420_10=1, DF420_11=1, DF420_12=1, DF420_13=1, DF420_14=1, DF420_15=1, DF420_16=1, DF420_17=1, DF420_18=1, DF420_19=1, DF420_20=1, DF420_21=1, DF420_22=1, DF420_23=1, DF420_24=1, DF420_25=1, DF420_26=1, DF420_27=1, DF420_28=1, DF420_29=1, DF420_30=1, DF420_31=1, DF420_32=1, DF420_33=1, DF420_34=1, DF420_35=1, DF420_36=1, DF420_37=1, DF420_38=1, DF420_39=1, DF420_40=1, DF420_41=1, DF420_42=1, DF420_43=1, DF420_44=1, DF403_01=63, DF403_02=63, DF403_03=63, DF403_04=63, DF403_05=63, DF403_06=32, DF403_07=0, DF403_08=0, DF403_09=0, DF403_10=0, DF403_11=0, DF403_12=0, DF403_13=45, DF403_14=47, DF403_15=46, DF403_16=46, DF403_17=50, DF403_18=51, DF403_19=51, DF403_20=47, DF403_21=46, DF403_22=34, DF403_23=35, DF403_24=45, DF403_25=41, DF403_26=27, DF403_27=27, DF403_28=39, DF403_29=41, DF403_30=36, DF403_31=36, DF403_32=37, DF403_33=36, DF403_34=17, DF403_35=17, DF403_36=33, DF403_37=47, DF403_38=39, DF403_39=39, DF403_40=44, DF403_41=45, DF403_42=36, DF403_43=36, DF403_44=41)>

Data bytes received: 460
<RTCM(1084, DF002=1084, DF003=0, GNSSEpoch=273375456, DF393=1, DF409=4, DF001_7=0, DF411=1, DF412=0, DF417=0, DF418=0, DF394=16195156465768464384, NSat=8, DF395=1090519040, NSig=2, DF396=65471, DF400_01=0.00054353, DF400_02=-0.00069696, DF400_03=-0.00033909, DF400_04=-0.0001781, DF400_05=-0.00083333, DF400_06=0.00084567, DF400_07=-0.00051057, DF400_08=0.00050557, DF400_09=-0.00069439, DF400_10=0.00074321, DF400_11=0.00064403, DF400_12=-0.00041932, DF400_13=-0.00073594, DF400_14=-2.259e-05, DF400_15=0.00085479, DF400_16=-0.00037801, DF401_01=-0.00210683, DF401_02=-0.00359596, DF401_03=-0.00378544, DF401_04=0.00306693, DF401_05=0.00286818, DF401_06=0.00384931, DF401_07=-0.00241077, DF401_08=-0.00225978, DF401_09=0.00161068, DF401_10=0.00178191, DF401_11=0.00154348, DF401_12=0.00159468, DF401_13=-0.00128505, DF401_14=-0.00125057, DF401_15=0.00247344, DF401_16=-0.00237724, DF402_01=11, DF402_02=5, DF402_03=14, DF402_04=8, DF402_05=8, DF402_06=1, DF402_07=4, DF402_08=8, DF402_09=3, DF402_10=7, DF402_11=0, DF402_12=5, DF402_13=5, DF402_14=1, DF402_15=7, DF402_16=11, DF420_01=1, DF420_02=1, DF420_03=1, DF420_04=1, DF420_05=0, DF420_06=0, DF420_07=1, DF420_08=0, DF420_09=1, DF420_10=1, DF420_11=1, DF420_12=0, DF420_13=0, DF420_14=1, DF420_15=1, DF420_16=0, DF403_01=23, DF403_02=60, DF403_03=57, DF403_04=45, DF403_05=63, DF403_06=63, DF403_07=63, DF403_08=63, DF403_09=63, DF403_10=63, DF403_11=47, DF403_12=63, DF403_13=63, DF403_14=63, DF403_15=56, DF403_16=0)>

Data bytes received: 442
<RTCM(1074, DF002=1074, DF003=0, GNSSEpoch=166960000, DF393=1, DF409=4, DF001_7=0, DF411=1, DF412=0, DF417=0, DF418=0, DF394=13011088448063275008, NSat=11, DF395=1346437120, NSig=4, DF396=17592186040047, DF400_01=0.00054365, DF400_02=-0.00070077, DF400_03=-0.00082338, DF400_04=0.00080037, DF400_05=0.00089228, DF400_06=-0.00095767, DF400_07=0.00075066, DF400_08=-0.00037783, DF400_09=0.00076413, DF400_10=0.00011426, DF400_11=0.00063735, DF400_12=0.00019658, DF400_13=-0.00076252, DF400_14=-0.00027585, DF400_15=0.00045657, DF400_16=0.0007022, DF400_17=-0.00076276, DF400_18=0.00019014, DF400_19=-5.4e-05, DF400_20=-0.00078559, DF400_21=-0.00078559, DF400_22=-0.0001964, DF400_23=0.00029194, DF400_24=4.828e-05, DF400_25=0.00053644, DF400_26=-0.00051343, DF400_27=0.00021923, DF400_28=-0.00075638, DF400_29=0.00046462, DF400_30=-0.00077266, DF400_31=-0.00052881, DF400_32=0.00020462, DF400_33=-0.00077206, DF400_34=0.00094515, DF400_35=0.00094497, DF400_36=0.00045735, DF400_37=-0.00027502, DF400_38=-0.00069577, DF400_39=0.00076902, DF400_40=0.00052482, DF400_41=-0.00045168, DF400_42=-0.00097322, DF400_43=-0.00072944, DF400_44=0.00049108, DF401_01=-5.081e-05, DF401_02=0.00103738, DF401_03=-0.00036669, DF401_04=-0.00012541, DF401_05=-0.00151841, DF401_06=0.00093153, DF401_07=0.00082747, DF401_08=-0.0004138, DF401_09=-0.0004138, DF401_10=-0.00035376, DF401_11=-0.00036354, DF401_12=-0.0007684, DF401_13=-0.0007684, DF401_14=-0.00071076, DF401_15=-0.00069447, DF401_16=0.00078646, DF401_17=0.00078648, DF401_18=0.00079787, DF401_19=0.00080274, DF401_20=-0.00037438, DF401_21=-0.00037436, DF401_22=-0.00033505, DF401_23=-0.00030573, DF401_24=-0.00062741, DF401_25=-0.00062742, DF401_26=-0.00061017, DF401_27=-0.00062165, DF401_28=-0.00050234, DF401_29=-0.00050236, DF401_30=-0.00050285, DF401_31=-0.00049147, DF401_32=0.00065958, DF401_33=0.00065958, DF401_34=0.00070409, DF401_35=0.00069432, DF401_36=0.0001262, DF401_37=0.0001262, DF401_38=0.00010996, DF401_39=-3.938e-05, DF401_40=-3.934e-05, DF401_41=-9.232e-05, DF401_42=0.0010432, DF401_43=0.0010432, DF401_44=0.00108102, DF402_01=14, DF402_02=4, DF402_03=9, DF402_04=8, DF402_05=3, DF402_06=15, DF402_07=9, DF402_08=2, DF402_09=6, DF402_10=1, DF402_11=9, DF402_12=14, DF402_13=4, DF402_14=13, DF402_15=4, DF402_16=6, DF402_17=7, DF402_18=9, DF402_19=5, DF402_20=0, DF402_21=6, DF402_22=15, DF402_23=15, DF402_24=15, DF402_25=15, DF402_26=15, DF402_27=15, DF402_28=15, DF402_29=15, DF402_30=15, DF402_31=15, DF402_32=15, DF402_33=15, DF402_34=15, DF402_35=15, DF402_36=15, DF402_37=15, DF402_38=15, DF402_39=15, DF402_40=15, DF402_41=15, DF402_42=15, DF402_43=15, DF402_44=15, DF420_01=1, DF420_02=1, DF420_03=1, DF420_04=1, DF420_05=1, DF420_06=1, DF420_07=1, DF420_08=1, DF420_09=1, DF420_10=1, DF420_11=1, DF420_12=1, DF420_13=1, DF420_14=1, DF420_15=1, DF420_16=1, DF420_17=1, DF420_18=1, DF420_19=1, DF420_20=1, DF420_21=1, DF420_22=1, DF420_23=1, DF420_24=1, DF420_25=1, DF420_26=1, DF420_27=1, DF420_28=1, DF420_29=1, DF420_30=1, DF420_31=1, DF420_32=1, DF420_33=1, DF420_34=1, DF420_35=1, DF420_36=1, DF420_37=1, DF420_38=1, DF420_39=1, DF420_40=1, DF420_41=1, DF420_42=1, DF420_43=1, DF420_44=1, DF403_01=63, DF403_02=63, DF403_03=63, DF403_04=63, DF403_05=63, DF403_06=32, DF403_07=0, DF403_08=0, DF403_09=0, DF403_10=0, DF403_11=0, DF403_12=0, DF403_13=46, DF403_14=47, DF403_15=47, DF403_16=46, DF403_17=50, DF403_18=51, DF403_19=51, DF403_20=47, DF403_21=45, DF403_22=35, DF403_23=35, DF403_24=45, DF403_25=41, DF403_26=27, DF403_27=27, DF403_28=39, DF403_29=41, DF403_30=36, DF403_31=36, DF403_32=37, DF403_33=36, DF403_34=17, DF403_35=17, DF403_36=33, DF403_37=47, DF403_38=38, DF403_39=39, DF403_40=45, DF403_41=46, DF403_42=36, DF403_43=36, DF403_44=41)>

Data bytes received: 471
<RTCM(1094, DF002=1094, DF003=0, GNSSEpoch=166960000, DF393=1, DF409=4, DF001_7=0, DF411=1, DF412=0, DF417=0, DF418=0, DF394=148654798856519680, NSat=6, DF395=134287616, NSig=4, DF396=16777215, DF400_01=0.00063562, DF400_02=0.00034088, DF400_03=-0.0008139, DF400_04=-0.00027132, DF400_05=-0.00079119, DF400_06=-0.00085056, DF400_07=-0.00070149, DF400_08=0.00054318, DF400_09=-0.00067657, DF400_10=0.00054401, DF400_11=-0.00067681, DF400_12=-0.0007062, DF400_13=-0.00095075, DF400_14=-0.00095087, DF400_15=0.00026971, DF400_16=-0.00071502, DF400_17=-0.00071478, DF400_18=-0.00095904, DF400_19=-0.000471, DF400_20=-0.00043929, DF400_21=-0.00043774, DF400_22=5.037e-05, DF400_23=0.00053853, DF400_24=0.00021714, DF401_01=-0.00303585, DF401_02=0.00199467, DF401_03=0.00248398, DF401_04=-0.00232298, DF401_05=-0.00364087, DF401_06=0.00192578, DF401_07=0.00203523, DF401_08=0.00204951, DF401_09=0.00204801, DF401_10=0.00093637, DF401_11=0.00100198, DF401_12=0.00100524, DF401_13=0.00100604, DF401_14=0.00057825, DF401_15=0.0006052, DF401_16=0.00060336, DF401_17=0.00059891, DF401_18=0.00155793, DF401_19=0.00161519, DF401_20=0.00160277, DF401_21=0.00159427, DF401_22=-0.00068408, DF401_23=-0.00052607, DF401_24=-0.0005276, DF402_01=14, DF402_02=14, DF402_03=11, DF402_04=0, DF402_05=2, DF402_06=0, DF402_07=7, DF402_08=2, DF402_09=12, DF402_10=9, DF402_11=0, DF402_12=2, DF402_13=0, DF402_14=8, DF402_15=9, DF402_16=6, DF402_17=0, DF402_18=8, DF402_19=4, DF402_20=1, DF402_21=14, DF402_22=4, DF402_23=2, DF402_24=1, DF420_01=0, DF420_02=0, DF420_03=0, DF420_04=0, DF420_05=0, DF420_06=0, DF420_07=0, DF420_08=1, DF420_09=1, DF420_10=0, DF420_11=0, DF420_12=1, DF420_13=1, DF420_14=1, DF420_15=1, DF420_16=1, DF420_17=1, DF420_18=1, DF420_19=1, DF420_20=1, DF420_21=1, DF420_22=1, DF420_23=1, DF420_24=1, DF403_01=63, DF403_02=63, DF403_03=63, DF403_04=63, DF403_05=63, DF403_06=63, DF403_07=63, DF403_08=63, DF403_09=63, DF403_10=63, DF403_11=63, DF403_12=63, DF403_13=63, DF403_14=63, DF403_15=0, DF403_16=0, DF403_17=0, DF403_18=0, DF403_19=44, DF403_20=50, DF403_21=49, DF403_22=49, DF403_23=41, DF403_24=45)>

^CConnection Terminated by User
```
Nice! I am also experimenting a bit on my side, just got myself a ublox F9P breakout board, and am connecting to a UNAVCO NTRIP server. You can see some of the code I wrote here: https://github.com/jakepoz/deweeder/blob/main/src/ntrip.py

My goal is to write everything using python async generators. So, with  my code you just `await` the `ntrip_client` and it will continuously yield messages that you can pass along to your serial port to the GPS. (In my case, I am also mixing a regular flow of message traffic to the GPS on the same serial port)

In my case, it turned out I didn't need to fully parse RTCM messages, just extract complete packets one at a time from the byte-stream that the NTRIP server is sending, so that you send complete messages one-at-a-time to the serial port.
Hi @jakepoz Sounds interesting. 

I've just posted a somewhat rudimentary NTRIP client to the `\examples` folder. It's clearly not a production-strength implementation but it hopefully demonstrates the basic concepts and provides a basis for further development.

I've just had a quick look at your code and the async HTTP methodology is clearly more advanced than mine; if you fancy collaborating on a more sophisticated implementation, I'd certainly welcome your contributions. Otherwise, I'd be more than happy to add a reference link to your project. 

As you say, if the intention is to receive RTCM3 messages from a designated NTRIP server and simply pass these on to a local GNSS device via UART (_or even a remote device via Zigbee radio or UDP socket_), you don't actually need to decode the payloads - you can simply forward the individual binary messages.

Out of interest, is it possible to implement an `aiohttp` bitstream as a stream class and use this directly in RTCMReader?

FYI I've only recently started to explore RTK / NTRIP as part of my SEMU GNSS library suite, as I wasn't sure if there was any significant appetite for Python implementations given the ubiquity of utilities like RTKLIB, gpsd etc. But it appears there is, so I'm hoping to commit more resources to this in future releases (including acquiring the definitive NTRIP protocol specifications rather than relying on public domain resources!). As I say, contributions very welcome.

Fix patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8cc99ed..6218b7a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -14,5 +14,5 @@
     "editor.formatOnSave": true,
     "modulename": "${workspaceFolderBasename}",
     "distname": "${workspaceFolderBasename}",
-    "moduleversion": "0.1.8"
+    "moduleversion": "0.2.0"
 }
\ No newline at end of file
diff --git a/README.md b/README.md
index 6540f52..aebab61 100644
--- a/README.md
+++ b/README.md
@@ -101,7 +101,6 @@ Example - File input (using iterator).
 >>> for (raw_data, parsed_data) in rtr: print(parsed_data)
 ...
 ```
-
 ---
 ## <a name="parsing">Parsing</a>
 
@@ -128,6 +127,24 @@ The `RTCMMessage` object exposes different public attributes depending on its me
 1
 ```
 
+It is also possible to parse raw RTCM3 messages from a buffer using the `RTCMReader.parse_buffer(bytearray)` function, which takes a bytearray containing one or more whole or partial RTCM3 messages and returns the first complete raw RTCM3 message and any remaining buffer.
+
+Example - Reading successive RTCM3 messages from an NTRIP server socket.
+```python
+  buf = bytearray()
+  data = True
+  while data:
+    data = sock.recv(1024) # NTRIP server socket
+    buf += data
+    while True:
+      raw_data, buf = RTCMReader.parse_buffer(buf)
+      if raw_data is not None:
+        parsed_data = RTCMReader.parse(raw_data)
+        print(parsed_data)
+      else:
+        break
+```
+
 Helper methods are available to interpret the individual datafields:
 
 ```python
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 78a7441..9eba490 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,14 @@
 # pyrtcm Release Notes
 
+### RELEASE 0.2-beta
+
+CHANGES:
+
+1. Added `parse_buffer()` static method to `RTCMReader` to parse an individual RTCM3 message from a buffer
+containing whole or partial RTCM3 messages, and return any remaining buffer. Can be used to parse the output from an NTRIP server HTTP GET response, for example. Thanks to @jakepoz and @foxittt for suggestion & inspiration.
+2. Add simple NTRIP client example `ntripclient.py` which uses the new method above.
+3. Development status updated to beta
+
 ### RELEASE 0.1.8-alpha
 
 CHANGES:
diff --git a/build.xml b/build.xml
index feb6e5d..ca2a945 100644
--- a/build.xml
+++ b/build.xml
@@ -1,7 +1,7 @@
 <?xml version = "1.0"?>
 <project name="pyrtcm" default="build">
 	<property name="module.name" value="pyrtcm" />
-	<property name="version" value="0.1.8" />
+	<property name="version" value="0.2.0" />
 	<condition property="python" value="python">
 		<os family="windows" />
 	</condition>
diff --git a/examples/ntripclient.py b/examples/ntripclient.py
index 9ae52e0..aca1c0a 100644
--- a/examples/ntripclient.py
+++ b/examples/ntripclient.py
@@ -10,7 +10,7 @@
 
 Usage:
 
-python3 ntripclient.py server=127.0.0.1 port=2101 mountpoint=RTMC32 user=myuser@domain.com password=mypassword
+python3 ntripclient.py server=127.0.0.1 port=2101 mountpoint=RTMC32 user=myuser password=mypassword
 
 NB: Please respect the terms and conditions of any public NTRIP service you use with this utility.
 
@@ -27,7 +27,6 @@
 import sys
 from datetime import datetime, timedelta
 from base64 import b64encode
-from typing import ByteString, Tuple
 from pynmeagps import NMEAMessage, GET, ddd2dmm
 from pyrtcm import RTCMReader, RTCMParseError, RTCMMessageError, ParameterError
 
@@ -65,9 +64,10 @@
     + "  V2 - use NTRIP version 2 (True)\n"
     + "  user - user name (anon)\n"
     + "  password - user password (password)\n"
-    + "  lat - base latitude (53.0)\n"
-    + "  lon - base longitute (-2.0)\n"
-    + "  alt - base altitude (0.0)\n"
+    + "  sendGGA - send GGA sentence to server (False)\n"
+    + "  lat - base latitude for GGA (53.0)\n"
+    + "  lon - base longitute for GGA (-2.0)\n"
+    + "  alt - base altitude for GGA (0.0)\n"
     + "  verbose - verbose log messages (True)\n\n"
     + f"{GREEN}Type Ctrl-C to terminate.{NORMAL}\n\n"
     + f"{YELLOW}© 2022 SEMU Consulting BSD 3-Clause license\n"
@@ -87,40 +87,41 @@ def __init__(self, **kwargs):
 
         user = kwargs.get("user", "anon")
         password = kwargs.get("password", "password")
-        self.caster = kwargs.get("server", "3.23.52.207")  # 3.23.52.207 = RTK2Go
-        self.port = int(kwargs.get("port", 2101))
-        self.mountpoint = kwargs.get("mountpoint", None)
-        self.V2 = bool(kwargs.get("V2", True))
-        self.lat = float(kwargs.get("lat", 53.0))
-        self.lon = float(kwargs.get("lon", -2.0))
-        self.alt = kwargs.get("alt", 0)
-        self.verbose = bool(kwargs.get("verbose", True))
-        self.lastGGATime = datetime(1990, 1, 1, 1, 0, 0)
+        self._caster = kwargs.get("server", "3.23.52.207")  # 3.23.52.207 = RTK2Go
+        self._port = int(kwargs.get("port", 2101))
+        self._mountpoint = kwargs.get("mountpoint", None)
+        self._V2 = bool(kwargs.get("V2", True))
+        self._lat = float(kwargs.get("lat", 53.0))
+        self._lon = float(kwargs.get("lon", -2.0))
+        self._alt = float(kwargs.get("alt", 0))
+        self._sendGGA = int(kwargs.get("sendGGA", False))
+        self._verbose = bool(kwargs.get("verbose", True))
+        self._lastGGAtime = datetime(1990, 1, 1, 1, 0, 0)
 
         user = user + ":" + password
-        self.user = b64encode(user.encode(encoding="utf-8"))
+        self._user = b64encode(user.encode(encoding="utf-8"))
 
-        self.socket = None
+        self._socket = None
 
-    def formatGET(self):
+    def _formatGET(self):
         """
         Format HTTP GET Request.
         """
 
-        if self.mountpoint[0:1] != "/":
-            self.mountpoint = "/" + self.mountpoint
+        if self._mountpoint[0:1] != "/":
+            self._mountpoint = "/" + self._mountpoint
         req = (
-            f"GET {self.mountpoint} HTTP/1.1\r\n"
+            f"GET {self._mountpoint} HTTP/1.1\r\n"
             + f"User-Agent: {USERAGENT}\r\n"
-            + f"Authorization: Basic {self.user.decode(encoding='utf-8')}\r\n"
+            + f"Authorization: Basic {self._user.decode(encoding='utf-8')}\r\n"
         )
-        if self.V2:
+        if self._V2:
             req += "Ntrip-Version: Ntrip/2.0\r\n"
         req += "\r\n"
         self.doOutput(req)
         return req.encode(encoding="utf-8")
 
-    def formatGGA(self):
+    def _formatGGA(self):
         """
         Format NMEA GGA sentence using pynmeagps.
         """
@@ -129,59 +130,34 @@ def formatGGA(self):
             "GP",
             "GGA",
             GET,
-            lat=float(ddd2dmm(self.lat, "LA")),
-            NS="N" if self.lat > 0 else "S",
-            lon=float(ddd2dmm(self.lon, "LN")),
-            EW="E" if self.lon > 0 else "W",
+            lat=float(ddd2dmm(self._lat, "LA")),
+            NS="N" if self._lat > 0 else "S",
+            lon=float(ddd2dmm(self._lon, "LN")),
+            EW="E" if self._lon > 0 else "W",
             quality=1,
             numSV=15,
             HDOP=0.19,
-            alt=float(self.alt),
+            alt=self._alt,
             altUnit="M",
             sep=-8.992,
             sepUnit="M",
         )
 
-    def parse_rtcm(self, buf: ByteString) -> Tuple[ByteString, ByteString]:
-        """
-        Parses a bytearray and decodes RTCM messages one at a time.
-        Based on, and thanks to:
-        https://github.com/jakepoz/deweeder/blob/main/src/ntrip.py
-        """
-        start = 0
-
-        while start < len(buf):
-            if buf[start] != 0xD3:
-                start += 1
-                continue
-
-            msglen = (buf[start + 1] & 0b00000011 > 8) | buf[start + 2]
-
-            if len(buf) < start + 3 + msglen + 3:
-                return None, buf
-
-            return (
-                buf[start : start + 3 + msglen + 3],
-                buf[start + 3 + msglen + 3 :],
-            )
-
-        return None, bytearray()
-
     def doOutput(self, msg):
         """
         Output debug message according to verbosity setting.
         """
 
-        if self.verbose:
+        if self._verbose:
             print(msg)
 
-    def doHeader(self, sock):
+    def _doHeader(self, sock):
         """
         Parse response header lines.
         """
 
         header = "Initial header"
-        print("*** Start Of Header ***")
+        self.doOutput("*** Start Of Header ***")
         while header:
             try:
 
@@ -207,12 +183,12 @@ def doHeader(self, sock):
                         self.doOutput("Mountpoint does not exist\n")
                         sys.exit(2)
                     elif line.find("200 OK") >= 0:  # Request was valid
-                        self.doGGA(sock)
+                        self._doGGA(sock)
 
             except UnicodeDecodeError as err:
                 self.doOutput(f"Header decode error {err}")
 
-    def doData(self, sock):
+    def _doData(self, sock):
         """
         Parse RTCM3 data.
         """
@@ -228,36 +204,36 @@ def doData(self, sock):
 
                 # Parse the data into individual RTCM3 messages
                 while True:
-                    rtcm_msg, buf = self.parse_rtcm(buf)
-
-                    if rtcm_msg:
-                        msg = RTCMReader.parse(rtcm_msg)
-                        print(f"{msg}\n")
-                        # if you wanted to forward this RTCM3 message to a GNSS device
-                        # via its serial port at this point, you could use something
-                        # like this:
+                    raw_data, buf = RTCMReader.parse_buffer(buf)
+
+                    if raw_data is not None:
+                        parsed_data = RTCMReader.parse(raw_data)
+                        print(f"{parsed_data}\n")
+                        # if you wanted to forward this RTCM3 message to
+                        # a GNSS device via its serial port at this point,
+                        # you could use something like this:
                         #
                         # with Serial(port, baudrate, timeout=timeout) as serial:
-                        #    serial.write(msg.serialize())
+                        #    serial.write(parsed_data.serialize())
                         #
                     else:
                         break
 
-                self.doGGA(sock)
+                self._doGGA(sock)
 
             except (RTCMParseError, RTCMMessageError) as err:
                 self.doOutput(f"RTCM Parse Error {err}\n")
                 data = False
 
-    def doGGA(self, sock):
+    def _doGGA(self, sock):
         """
         Send GGA sentence periodically.
         """
 
-        if datetime.now() > self.lastGGATime + GGAINTERVAL:
-            gga = self.formatGGA()
+        if self._sendGGA and (datetime.now() > self._lastGGAtime + GGAINTERVAL):
+            gga = self._formatGGA()
             sock.sendall(gga.serialize())
-            self.lastGGATime = datetime.now()
+            self._lastGGAtime = datetime.now()
             self.doOutput(gga)
 
     def run(self):
@@ -268,13 +244,13 @@ def run(self):
         try:
 
             with socket.socket() as sock:
-                sock.connect((self.caster, self.port))
-                self.doOutput(f"Connected to: {self.caster}:{self.port}")
+                sock.connect((self._caster, self._port))
+                self.doOutput(f"Connected to: {self._caster}:{self._port}")
                 sock.settimeout(TIMEOUT)
-                sock.sendall(self.formatGET())
+                sock.sendall(self._formatGET())
                 while True:
-                    self.doHeader(sock)
-                    self.doData(sock)
+                    self._doHeader(sock)
+                    self._doData(sock)
 
         except socket.timeout as err:
             self.doOutput(f"Connection Timed Out {err}")
diff --git a/pyrtcm/_version.py b/pyrtcm/_version.py
index b8fac3a..995172d 100644
--- a/pyrtcm/_version.py
+++ b/pyrtcm/_version.py
@@ -8,4 +8,4 @@
 :license: BSD 3-Clause
 """
 
-__version__ = "0.1.8"
+__version__ = "0.2.0"
diff --git a/pyrtcm/rtcmreader.py b/pyrtcm/rtcmreader.py
index 876c156..052c938 100644
--- a/pyrtcm/rtcmreader.py
+++ b/pyrtcm/rtcmreader.py
@@ -256,3 +256,36 @@ def parse(message: bytes, **kwargs) -> object:
         scaling = int(kwargs.get("scaling", True))
         payload = message[3:-3]
         return RTCMMessage(payload=payload, scaling=scaling)
+
+    @staticmethod
+    def parse_buffer(buf: bytearray) -> tuple:
+        """
+        Parses a bytearray buffer containing whole or partial RTCM3 messages
+        (e.g. an NTRIP server HTTP GET response) and returns the first raw
+        RTCM3 message and any remaining buffer. Based on, and thanks to:
+        https://github.com/jakepoz/deweeder/blob/main/src/ntrip.py
+
+        :param bytearray buf: buffer containing whole or partial RTCM3 messages
+        :return: tuple of (raw_data as bytes, buf_remain as bytes)
+        :rtype: tuple
+        """
+
+        start = 0
+
+        while start < len(buf):
+            if buf[start] != 0xD3:  # RTCM3 header
+                start += 1
+                continue
+
+            msglen = (buf[start + 1] & 0b00000011 > 8) | buf[start + 2]
+
+            if len(buf) < start + 3 + msglen + 3:
+                return (None, buf)
+
+            raw_data = buf[start : start + 3 + msglen + 3]
+            buf_remain = buf[start + 3 + msglen + 3 :]
+            if calc_crc24q(raw_data) == 0:  # valid checksum
+                return (raw_data, buf_remain)
+            return (None, buf_remain)
+
+        return (None, bytearray())
diff --git a/setup.py b/setup.py
index cd096dd..6708102 100644
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@
         "pyserial>=3.4",
     ],
     license="BSD 3-Clause 'Modified' License",
-    keywords="PYRTCM GNSS GPS RTCM RTCM3 GIS",
+    keywords="PYRTCM GNSS GPS RTCM RTCM3 GIS NTRIP",
     platforms="Windows, MacOS, Linux",
     project_urls={
         "Bug Tracker": "https://github.com/semuconsulting/pyrtcm",
@@ -36,7 +36,7 @@
     },
     classifiers=[
         "Operating System :: OS Independent",
-        "Development Status :: 3 - Alpha",
+        "Development Status ::4 - Beta",
         "Environment :: MacOS X",
         "Environment :: Win32 (MS Windows)",
         "Environment :: X11 Applications",


Test patch:
diff --git a/tests/test_stream.py b/tests/test_stream.py
index 58d9ffc..ab8065a 100644
--- a/tests/test_stream.py
+++ b/tests/test_stream.py
@@ -136,6 +136,35 @@ def testnestedgroups(self):  # test message with nested repeating group (1059, 1
         msg = RTCMReader.parse(self._raw1065, scaling=True)
         self.assertEqual(str(msg), EXPECTED_RESULT)
 
+    def testparsebuffer(self):  # test parse_buffer method
+        DATA = b"\xd3\x00\x8aC \x004\x85\x01\xe6\x00\x00\x08\x00\xa2e\x00\x00\x00\x00 \x00\x80\x00w\x7f\xa8\xa7\xa5)%#\"\xa2\x84\x94\x87V\t&\x18\x19\x1b\x7fz\xe6\xf5\x02P\xbc'\x10C\xffO=\xd1\xfbQ\xfd\x9f\xf9\xfb\x91\xef\x1a\x0f?\xfeS\xff\xcd\xbd~\x84x\x14\x8fp\x1f\x07\xa0Q\x0c\xfe\xdf\x89\xf6^\x0f\xdd\x14\x7f\xfb\x8d\x00\x1d\xa5\xe8\xce\xff\x99\xb9\x7fL\xd5\xfc\xd2\xbd\xff\xff\xf9\xff\xff\xff\xfe\x00\x053QeNU%\xf5aN\x15\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0]J\xd3\x00{C\xc0\x00M\xe46\xa6\x00\x00\x18\x0c\x0e\x00\x00\x00\x00\x00 \x80\x00\x00\x7f>\x9a\x9e\x84\x9e\x98\x80\x89\xda\x08O\x8bL\xeb\xe4\xd1wB~e\xbf\xcc?6z\xaa\xf3\xd3\x8dx\xc8g7\xee?&{\x8c\xa2}`\xfb\xf2,\xcf\xff\xc8\x7f\xc6k\xff;u\xfd*\xef\x8c{\x81\x85\x8e}\xaa\x05\xf8\xfa\x80u|\xa1\xa0F\x7f\xff\x7f\xbd\xff\xfe\x80\x063Q\x0eu\xb8\xee4N\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xad\xccJ\xd3\x00\x8aD`\x004\x85\x01\xe6\x00\x00!\x98\x00\xc0\x08\x00\x00\x00 \x01\x00\x00\x7f\xff\xa8.\xad\xa6)-'+g`\x84\x14,\xd4;\xae-G\x1c\xb38\xc7\xa8\xafU0\x86A\x13\x9c\x87\xb8v\x96\x90,\x8d\xcf\xfb\xa1yb\x92\xbed\xc4I\xca\x87\x9d\x1a\x1f\x00\xbf\xa5~\x1e\xad\xed\x01\xdbZ\x08>/\xd0\xba\x9fI\xb0\x05{\x06\x18\x99w\xcd&\x1e\xf1a\x06^\xa6\x17\xb6\xd0Nn\x01b\x0b\x7f\xff\xff\xff\xff\xff\xff\xff\x80\x00]\x84\xf2G=S\xd1C\xf1\xdfm\x95\x00\x10g\x90\xd3\x00mF@\x004\x84'$\x00\x00\x00\x84\x00\xa4\x10\x00\x00\x00 \x02\x00\x00}T\x0c\x02\xa2BJP\xe2[\xff\xd7+ \xbf\x89y\x0eF,\xa4QW\xfb\xd3\xce\xa0\xef8\x97\x82\xb9.\x08\xad\x807\x92 \xbe\xe0\x80*\x1c q\x90\x17\x86?4\xa3\x7f\xff\xff\xff\x80Se2Y\x867\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x86m-\xd3\x00\x13>\xd0\x00\x03\x89g\x8f\x8bL\xbfo!\x19\xbf\x0bp\xcc\xad\xe0<`\xe6"
+        EXPECTED_RESULT = "<RTCM(1074, DF002=1074, DF003=0, GNSSEpoch=220283001, DF393=1, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=1153278613957640192, NSat=8, DF395=1073807360, NSig=2, DF396=61183, DF400_01=0.00062031, DF400_02=-0.00069308, DF400_03=-0.00083494, DF400_04=-0.00042224, DF400_05=0.0006147, DF400_06=0.00055277, DF400_07=-0.00060821, DF400_08=-0.0007906, DF400_09=0.00038308, DF400_10=0.00097257, DF400_11=0.00088102, DF400_12=0.00049269, DF400_13=0.00017941, DF400_14=0.00086224, DF400_15=0.00024396, DF400_16=0.0004651, DF401_01=-0.00280873, DF401_02=-3.624e-05, DF401_03=0.00193623, DF401_04=0.00368696, DF401_05=0.00390585, DF401_06=-3e-06, DF401_07=-0.00244706, DF401_08=-0.00193352, DF401_09=-0.00096917, DF401_10=-0.00146001, DF401_11=-0.00293399, DF401_12=0.00243222, DF401_13=0.00096824, DF401_14=0.00195286, DF401_15=-0.00292925, DF401_16=0.00144272, DF402_01=15, DF402_02=15, DF402_03=3, DF402_04=3, DF402_05=7, DF402_06=2, DF402_07=15, DF402_08=14, DF402_09=9, DF402_10=9, DF402_11=10, DF402_12=11, DF402_13=15, DF402_14=9, DF402_15=10, DF402_16=5, DF420_01=0, DF420_02=1, DF420_03=1, DF420_04=1, DF420_05=1, DF420_06=0, DF420_07=1, DF420_08=1, DF420_09=1, DF420_10=1, DF420_11=1, DF420_12=1, DF420_13=1, DF420_14=1, DF420_15=1, DF420_16=1, DF403_01=63, DF403_02=63, DF403_03=15, DF403_04=63, DF403_05=63, DF403_06=63, DF403_07=63, DF403_08=60, DF403_09=0, DF403_10=0, DF403_11=41, DF403_12=38, DF403_13=40, DF403_14=44, DF403_15=42, DF403_16=28)>"
+        buffer = bytearray(DATA)
+        raw_data, _ = RTCMReader.parse_buffer(buffer)
+        msg = RTCMReader.parse(raw_data)
+        self.assertEqual(str(msg), EXPECTED_RESULT)
+
+    def testparsebuffer2(self):  # test parse_buffer method with junk at start
+        DATA = b"%w\xd3\x00\x8aC \x004\x85\x01\xe6\x00\x00\x08\x00\xa2e\x00\x00\x00\x00 \x00\x80\x00w\x7f\xa8\xa7\xa5)%#\"\xa2\x84\x94\x87V\t&\x18\x19\x1b\x7fz\xe6\xf5\x02P\xbc'\x10C\xffO=\xd1\xfbQ\xfd\x9f\xf9\xfb\x91\xef\x1a\x0f?\xfeS\xff\xcd\xbd~\x84x\x14\x8fp\x1f\x07\xa0Q\x0c\xfe\xdf\x89\xf6^\x0f\xdd\x14\x7f\xfb\x8d\x00\x1d\xa5\xe8\xce\xff\x99\xb9\x7fL\xd5\xfc\xd2\xbd\xff\xff\xf9\xff\xff\xff\xfe\x00\x053QeNU%\xf5aN\x15\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0]J\xd3\x00{C\xc0\x00M\xe46\xa6\x00\x00\x18\x0c\x0e\x00\x00\x00\x00\x00 \x80\x00\x00\x7f>\x9a\x9e\x84\x9e\x98\x80\x89\xda\x08O\x8bL\xeb\xe4\xd1wB~e\xbf\xcc?6z\xaa\xf3\xd3\x8dx\xc8g7\xee?&{\x8c\xa2}`\xfb\xf2,\xcf\xff\xc8\x7f\xc6k\xff;u\xfd*\xef\x8c{\x81\x85\x8e}\xaa\x05\xf8\xfa\x80u|\xa1\xa0F\x7f\xff\x7f\xbd\xff\xfe\x80\x063Q\x0eu\xb8\xee4N\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xad\xccJ\xd3\x00\x8aD`\x004\x85\x01\xe6\x00\x00!\x98\x00\xc0\x08\x00\x00\x00 \x01\x00\x00\x7f\xff\xa8.\xad\xa6)-'+g`\x84\x14,\xd4;\xae-G\x1c\xb38\xc7\xa8\xafU0\x86A\x13\x9c\x87\xb8v\x96\x90,\x8d\xcf\xfb\xa1yb\x92\xbed\xc4I\xca\x87\x9d\x1a\x1f\x00\xbf\xa5~\x1e\xad\xed\x01\xdbZ\x08>/\xd0\xba\x9fI\xb0\x05{\x06\x18\x99w\xcd&\x1e\xf1a\x06^\xa6\x17\xb6\xd0Nn\x01b\x0b\x7f\xff\xff\xff\xff\xff\xff\xff\x80\x00]\x84\xf2G=S\xd1C\xf1\xdfm\x95\x00\x10g\x90\xd3\x00mF@\x004\x84'$\x00\x00\x00\x84\x00\xa4\x10\x00\x00\x00 \x02\x00\x00}T\x0c\x02\xa2BJP\xe2[\xff\xd7+ \xbf\x89y\x0eF,\xa4QW\xfb\xd3\xce\xa0\xef8\x97\x82\xb9.\x08\xad\x807\x92 \xbe\xe0\x80*\x1c q\x90\x17\x86?4\xa3\x7f\xff\xff\xff\x80Se2Y\x867\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x86m-\xd3\x00\x13>\xd0\x00\x03\x89g\x8f\x8bL\xbfo!\x19\xbf\x0bp\xcc\xad\xe0<`\xe6"
+        EXPECTED_RESULT = "<RTCM(1074, DF002=1074, DF003=0, GNSSEpoch=220283001, DF393=1, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=1153278613957640192, NSat=8, DF395=1073807360, NSig=2, DF396=61183, DF400_01=0.00062031, DF400_02=-0.00069308, DF400_03=-0.00083494, DF400_04=-0.00042224, DF400_05=0.0006147, DF400_06=0.00055277, DF400_07=-0.00060821, DF400_08=-0.0007906, DF400_09=0.00038308, DF400_10=0.00097257, DF400_11=0.00088102, DF400_12=0.00049269, DF400_13=0.00017941, DF400_14=0.00086224, DF400_15=0.00024396, DF400_16=0.0004651, DF401_01=-0.00280873, DF401_02=-3.624e-05, DF401_03=0.00193623, DF401_04=0.00368696, DF401_05=0.00390585, DF401_06=-3e-06, DF401_07=-0.00244706, DF401_08=-0.00193352, DF401_09=-0.00096917, DF401_10=-0.00146001, DF401_11=-0.00293399, DF401_12=0.00243222, DF401_13=0.00096824, DF401_14=0.00195286, DF401_15=-0.00292925, DF401_16=0.00144272, DF402_01=15, DF402_02=15, DF402_03=3, DF402_04=3, DF402_05=7, DF402_06=2, DF402_07=15, DF402_08=14, DF402_09=9, DF402_10=9, DF402_11=10, DF402_12=11, DF402_13=15, DF402_14=9, DF402_15=10, DF402_16=5, DF420_01=0, DF420_02=1, DF420_03=1, DF420_04=1, DF420_05=1, DF420_06=0, DF420_07=1, DF420_08=1, DF420_09=1, DF420_10=1, DF420_11=1, DF420_12=1, DF420_13=1, DF420_14=1, DF420_15=1, DF420_16=1, DF403_01=63, DF403_02=63, DF403_03=15, DF403_04=63, DF403_05=63, DF403_06=63, DF403_07=63, DF403_08=60, DF403_09=0, DF403_10=0, DF403_11=41, DF403_12=38, DF403_13=40, DF403_14=44, DF403_15=42, DF403_16=28)>"
+        buffer = bytearray(DATA)
+        raw_data, _ = RTCMReader.parse_buffer(buffer)
+        msg = RTCMReader.parse(raw_data)
+        self.assertEqual(str(msg), EXPECTED_RESULT)
+
+    def testparsebuffer3(self):  # test parse_buffer method with invalid CRC
+        DATA = b"\xd3\x00\x8aC \x004\x85\x01\xe6\x00\x00\x08\x00\xa2e\x00\x00\x00\x00 \x00\x80\x00w\x7f\xa8\xa7\xa5)%#\"\xa2\x84\x94\x87V\t&\x18\x19\x1b\x7fz\xe6\xf5\x02P\xbc'\x10C\xffO=\xd1\xfbQ\xfd\x9f\xf9\xfb\x91\xef\x1a\x0f?\xfeS\xff\xcd\xbd~\x84x\x14\x8fp\x1f\x07\xa0Q\x0c\xfe\xdf\x89\xf6^\x0f\xdd\x14\x7f\xfb\x8d\x00\x1d\xa5\xe8\xce\xff\x99\xb9\x7fL\xd5\xfc\xd2\xbd\xff\xff\xf9\xff\xff\xff\xfe\x00\x053QeNU%\xf5aN\x15\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc0]K\xd3\x00{C\xc0\x00M\xe46\xa6\x00\x00\x18\x0c\x0e\x00\x00\x00\x00\x00 \x80\x00\x00\x7f>\x9a\x9e\x84\x9e\x98\x80\x89\xda\x08O\x8bL\xeb\xe4\xd1wB~e\xbf\xcc?6z\xaa\xf3\xd3\x8dx\xc8g7\xee?&{\x8c\xa2}`\xfb\xf2,\xcf\xff\xc8\x7f\xc6k\xff;u\xfd*\xef\x8c{\x81\x85\x8e}\xaa\x05\xf8\xfa\x80u|\xa1\xa0F\x7f\xff\x7f\xbd\xff\xfe\x80\x063Q\x0eu\xb8\xee4N\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xad\xccJ\xd3\x00\x8aD`\x004\x85\x01\xe6\x00\x00!\x98\x00\xc0\x08\x00\x00\x00 \x01\x00\x00\x7f\xff\xa8.\xad\xa6)-'+g`\x84\x14,\xd4;\xae-G\x1c\xb38\xc7\xa8\xafU0\x86A\x13\x9c\x87\xb8v\x96\x90,\x8d\xcf\xfb\xa1yb\x92\xbed\xc4I\xca\x87\x9d\x1a\x1f\x00\xbf\xa5~\x1e\xad\xed\x01\xdbZ\x08>/\xd0\xba\x9fI\xb0\x05{\x06\x18\x99w\xcd&\x1e\xf1a\x06^\xa6\x17\xb6\xd0Nn\x01b\x0b\x7f\xff\xff\xff\xff\xff\xff\xff\x80\x00]\x84\xf2G=S\xd1C\xf1\xdfm\x95\x00\x10g\x90\xd3\x00mF@\x004\x84'$\x00\x00\x00\x84\x00\xa4\x10\x00\x00\x00 \x02\x00\x00}T\x0c\x02\xa2BJP\xe2[\xff\xd7+ \xbf\x89y\x0eF,\xa4QW\xfb\xd3\xce\xa0\xef8\x97\x82\xb9.\x08\xad\x807\x92 \xbe\xe0\x80*\x1c q\x90\x17\x86?4\xa3\x7f\xff\xff\xff\x80Se2Y\x867\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x86m-\xd3\x00\x13>\xd0\x00\x03\x89g\x8f\x8bL\xbfo!\x19\xbf\x0bp\xcc\xad\xe0<`\xe6"
+        EXPECTED_RESULT = "<RTCM(1074, DF002=1074, DF003=0, GNSSEpoch=220283001, DF393=1, DF409=0, DF001_7=0, DF411=0, DF412=0, DF417=0, DF418=0, DF394=1153278613957640192, NSat=8, DF395=1073807360, NSig=2, DF396=61183, DF400_01=0.00062031, DF400_02=-0.00069308, DF400_03=-0.00083494, DF400_04=-0.00042224, DF400_05=0.0006147, DF400_06=0.00055277, DF400_07=-0.00060821, DF400_08=-0.0007906, DF400_09=0.00038308, DF400_10=0.00097257, DF400_11=0.00088102, DF400_12=0.00049269, DF400_13=0.00017941, DF400_14=0.00086224, DF400_15=0.00024396, DF400_16=0.0004651, DF401_01=-0.00280873, DF401_02=-3.624e-05, DF401_03=0.00193623, DF401_04=0.00368696, DF401_05=0.00390585, DF401_06=-3e-06, DF401_07=-0.00244706, DF401_08=-0.00193352, DF401_09=-0.00096917, DF401_10=-0.00146001, DF401_11=-0.00293399, DF401_12=0.00243222, DF401_13=0.00096824, DF401_14=0.00195286, DF401_15=-0.00292925, DF401_16=0.00144272, DF402_01=15, DF402_02=15, DF402_03=3, DF402_04=3, DF402_05=7, DF402_06=2, DF402_07=15, DF402_08=14, DF402_09=9, DF402_10=9, DF402_11=10, DF402_12=11, DF402_13=15, DF402_14=9, DF402_15=10, DF402_16=5, DF420_01=0, DF420_02=1, DF420_03=1, DF420_04=1, DF420_05=1, DF420_06=0, DF420_07=1, DF420_08=1, DF420_09=1, DF420_10=1, DF420_11=1, DF420_12=1, DF420_13=1, DF420_14=1, DF420_15=1, DF420_16=1, DF403_01=63, DF403_02=63, DF403_03=15, DF403_04=63, DF403_05=63, DF403_06=63, DF403_07=63, DF403_08=60, DF403_09=0, DF403_10=0, DF403_11=41, DF403_12=38, DF403_13=40, DF403_14=44, DF403_15=42, DF403_16=28)>"
+        buffer = bytearray(DATA)
+        raw_data, _ = RTCMReader.parse_buffer(buffer)
+        self.assertIsNone(raw_data)
+
+    def testparsebuffer4(self):  # test parse_buffer method with short buffer
+        DATA = b"\xd3\x00\x8aC \x004"
+        buffer = bytearray(DATA)
+        raw_data, _ = RTCMReader.parse_buffer(buffer)
+        self.assertIsNone(raw_data)
+
 
 if __name__ == "__main__":
     # import sys;sys.argv = ['', 'Test.testName']