#!/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()