Initial commit

This commit is contained in:
brentperteet
2026-06-24 11:12:44 -05:00
commit 5703c05c1d
30 changed files with 8149 additions and 0 deletions

207
ntrip_message_survey.py Normal file
View 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()