Initial commit
This commit is contained in:
207
ntrip_message_survey.py
Normal file
207
ntrip_message_survey.py
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple script to survey what RTCM message types are sent by the caster over 60 seconds.
|
||||
Useful to determine if the caster sends base station position messages (1005/1006).
|
||||
"""
|
||||
import base64
|
||||
import socket
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
# NTRIP configuration
|
||||
CASTER_HOST = "truertk.pointonenav.com"
|
||||
CASTER_PORT = 2101
|
||||
MOUNTPOINT = "AUTO"
|
||||
USERNAME = "9t7fwfbm57"
|
||||
PASSWORD = "96m7bec9g8"
|
||||
|
||||
# Position for GGA
|
||||
LAT = 36.1140884
|
||||
LON = -97.0880663
|
||||
ALT = 390.0
|
||||
|
||||
# Survey duration (seconds)
|
||||
SURVEY_DURATION = 60
|
||||
|
||||
|
||||
def build_gga(lat: float, lon: float, alt: float) -> bytes:
|
||||
"""Build NMEA GGA sentence."""
|
||||
now_utc = datetime.now(timezone.utc).strftime("%H%M%S")
|
||||
lat_hemi = "N" if lat >= 0 else "S"
|
||||
lat_abs = abs(lat)
|
||||
lat_deg = int(lat_abs)
|
||||
lat_min = (lat_abs - lat_deg) * 60.0
|
||||
lat_str = f"{lat_deg:02d}{lat_min:07.4f}"
|
||||
|
||||
lon_hemi = "E" if lon >= 0 else "W"
|
||||
lon_abs = abs(lon)
|
||||
lon_deg = int(lon_abs)
|
||||
lon_min = (lon_abs - lon_deg) * 60.0
|
||||
lon_str = f"{lon_deg:03d}{lon_min:07.4f}"
|
||||
|
||||
fields = ["GPGGA", now_utc, lat_str, lat_hemi, lon_str, lon_hemi, "1", "12", "1.0", f"{alt:.1f}", "M", "", "M", "", ""]
|
||||
core = ",".join(fields)
|
||||
checksum = 0
|
||||
for char in core:
|
||||
checksum ^= ord(char)
|
||||
return f"${core}*{checksum:02X}\r\n".encode("ascii")
|
||||
|
||||
|
||||
def make_ntrip_request(host: str, port: int, mount: str, user: str, password: str) -> bytes:
|
||||
"""Create NTRIP v2 HTTP request."""
|
||||
auth = base64.b64encode(f"{user}:{password}".encode("utf-8")).decode("ascii")
|
||||
req = (
|
||||
f"GET /{mount} HTTP/1.1\r\n"
|
||||
f"Host: {host}:{port}\r\n"
|
||||
f"Ntrip-Version: Ntrip/2.0\r\n"
|
||||
f"User-Agent: NTRIP-Survey/1.0\r\n"
|
||||
f"Connection: close\r\n"
|
||||
f"Authorization: Basic {auth}\r\n\r\n"
|
||||
)
|
||||
return req.encode("ascii")
|
||||
|
||||
|
||||
def main():
|
||||
print(f"NTRIP Message Type Survey")
|
||||
print(f"=" * 80)
|
||||
print(f"Caster: {CASTER_HOST}:{CASTER_PORT}/{MOUNTPOINT}")
|
||||
print(f"Duration: {SURVEY_DURATION} seconds")
|
||||
print(f"=" * 80)
|
||||
print()
|
||||
|
||||
message_types = {}
|
||||
last_gga_time = 0
|
||||
start_time = time.monotonic()
|
||||
total_bytes = 0
|
||||
message_count = 0
|
||||
|
||||
try:
|
||||
# Connect
|
||||
print("Connecting...")
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
sock.connect((CASTER_HOST, CASTER_PORT))
|
||||
sock.sendall(make_ntrip_request(CASTER_HOST, CASTER_PORT, MOUNTPOINT, USERNAME, PASSWORD))
|
||||
|
||||
# Read headers
|
||||
header = b""
|
||||
while b"\r\n\r\n" not in header:
|
||||
chunk = sock.recv(1)
|
||||
if not chunk:
|
||||
print("ERROR: Connection closed")
|
||||
return
|
||||
header += chunk
|
||||
|
||||
if "200 OK" not in header.decode("iso-8859-1", errors="replace"):
|
||||
print("ERROR: Connection failed")
|
||||
return
|
||||
|
||||
print(f"Connected! Surveying for {SURVEY_DURATION} seconds...\n")
|
||||
|
||||
# Survey loop
|
||||
while time.monotonic() - start_time < SURVEY_DURATION:
|
||||
now = time.monotonic()
|
||||
|
||||
# Send GGA every 10 seconds
|
||||
if now - last_gga_time >= 10:
|
||||
sock.sendall(build_gga(LAT, LON, ALT))
|
||||
elapsed = int(now - start_time)
|
||||
print(f"[{elapsed:3d}s] Sent GGA | Messages so far: {message_count}")
|
||||
last_gga_time = now
|
||||
|
||||
# Receive data
|
||||
data = sock.recv(4096)
|
||||
if not data:
|
||||
print("Connection closed by caster")
|
||||
break
|
||||
|
||||
# Parse RTCM message types (simple extraction)
|
||||
i = 0
|
||||
while i < len(data):
|
||||
if data[i] == 0xD3 and i + 2 < len(data):
|
||||
length = ((data[i+1] & 0x03) << 8) | data[i+2]
|
||||
msg_total_len = 3 + length + 3
|
||||
|
||||
if i + msg_total_len <= len(data) and length >= 3:
|
||||
msg_type = (data[i+3] << 4) | (data[i+4] >> 4)
|
||||
message_types[msg_type] = message_types.get(msg_type, 0) + 1
|
||||
total_bytes += msg_total_len
|
||||
message_count += 1
|
||||
i += msg_total_len
|
||||
continue
|
||||
i += 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nInterrupted by user")
|
||||
except Exception as e:
|
||||
print(f"\nERROR: {e}")
|
||||
finally:
|
||||
if 'sock' in locals():
|
||||
try:
|
||||
sock.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Print results
|
||||
elapsed = time.monotonic() - start_time
|
||||
print(f"\n{'=' * 80}")
|
||||
print("SURVEY RESULTS")
|
||||
print(f"{'=' * 80}")
|
||||
print(f"Duration: {elapsed:.1f} seconds")
|
||||
print(f"Total messages: {message_count}")
|
||||
print(f"Total bytes: {total_bytes:,}")
|
||||
if elapsed > 0:
|
||||
print(f"Rate: {int(total_bytes / elapsed * 3600):,} bytes/hour")
|
||||
print(f"\nMessage types received:")
|
||||
print(f"{'─' * 80}")
|
||||
|
||||
# Sort by message type
|
||||
for msg_type in sorted(message_types.keys()):
|
||||
count = message_types[msg_type]
|
||||
freq = count / elapsed if elapsed > 0 else 0
|
||||
|
||||
# Add description
|
||||
descriptions = {
|
||||
1005: "Stationary RTK Reference Station ARP",
|
||||
1006: "Stationary RTK Reference Station ARP + Antenna Height",
|
||||
1019: "GPS Ephemerides",
|
||||
1020: "GLONASS Ephemerides",
|
||||
1033: "Receiver and Antenna Descriptors",
|
||||
1074: "GPS MSM4",
|
||||
1075: "GPS MSM5",
|
||||
1077: "GPS MSM7",
|
||||
1084: "GLONASS MSM4",
|
||||
1085: "GLONASS MSM5",
|
||||
1087: "GLONASS MSM7",
|
||||
1094: "Galileo MSM4",
|
||||
1095: "Galileo MSM5",
|
||||
1097: "Galileo MSM7",
|
||||
1124: "BeiDou MSM4",
|
||||
1125: "BeiDou MSM5",
|
||||
1127: "BeiDou MSM7",
|
||||
1230: "GLONASS Code-Phase Biases",
|
||||
}
|
||||
desc = descriptions.get(msg_type, "")
|
||||
|
||||
# Highlight position messages
|
||||
marker = " ← BASE POSITION" if msg_type in [1005, 1006] else ""
|
||||
print(f" RTCM {msg_type:4d}: {count:5d} msgs ({freq:6.2f}/sec) {desc}{marker}")
|
||||
|
||||
print(f"{'─' * 80}")
|
||||
|
||||
# Check for position messages
|
||||
has_position = any(msg_type in [1005, 1006] for msg_type in message_types)
|
||||
if has_position:
|
||||
print("\n✓ Caster DOES send base station position messages (1005/1006)")
|
||||
else:
|
||||
print("\n✗ Caster does NOT send base station position messages (1005/1006)")
|
||||
print(" This caster may only provide observation data without station coordinates.")
|
||||
print(" You may need to use a different mountpoint or obtain station coordinates")
|
||||
print(" from the caster operator/documentation.")
|
||||
|
||||
print(f"{'=' * 80}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user