#!/usr/bin/env python3 """ RTCM message capture and CSV export tool. Connects to NTRIP caster, parses messages with detailed field extraction, and exports specific message types to CSV files. """ import base64 import socket import time from datetime import datetime, timezone import argparse from rtcm_detailed_parser import RTCMDetailedParser # 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 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: RTCM-CSV-Exporter/1.0\r\n" f"Connection: close\r\n" f"Authorization: Basic {auth}\r\n\r\n" ) return req.encode("ascii") def main(): parser_args = argparse.ArgumentParser(description="Capture RTCM messages and export to CSV") parser_args.add_argument("--duration", type=int, default=60, help="Duration in seconds (default: 60)") parser_args.add_argument("--output", type=str, default="rtcm_messages.csv", help="Output CSV filename") parser_args.add_argument("--types", type=str, help="Comma-separated message types to capture (e.g., 1005,1006,1008)") parser_args.add_argument("--verbose", action="store_true", help="Show detailed output") args = parser_args.parse_args() # Parse message types filter message_types_filter = None if args.types: message_types_filter = [int(x.strip()) for x in args.types.split(",")] print(f"Filtering for message types: {message_types_filter}") print(f"RTCM Message Capture to CSV") print(f"=" * 80) print(f"Caster: {CASTER_HOST}:{CASTER_PORT}/{MOUNTPOINT}") print(f"Duration: {args.duration} seconds") print(f"Output: {args.output}") print(f"=" * 80) print() parser = RTCMDetailedParser() last_gga_time = 0 start_time = time.monotonic() try: # Connect print("Connecting...") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(30) 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! Capturing messages...\n") # Capture loop while time.monotonic() - start_time < args.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 captured: {parser.message_count}") last_gga_time = now # Receive and parse data data = sock.recv(4096) if not data: print("Connection closed by caster") break timestamp = datetime.now(timezone.utc).isoformat() messages = parser.parse_messages(data, timestamp) # Display verbose output if args.verbose: for msg in messages: msg_type = msg.get("message_type", "?") msg_name = msg.get("message_name", "Unknown") print(f" [{timestamp}] RTCM {msg_type}: {msg_name}") # Show key fields for specific message types if msg_type in [1005, 1006]: if "latitude_deg" in msg: print(f" Station: {msg.get('station_id')}, Lat: {msg.get('latitude_deg'):.7f}°, Lon: {msg.get('longitude_deg'):.7f}°") elif msg_type in [1007, 1008]: if "antenna_descriptor" in msg: print(f" Station: {msg.get('station_id')}, Antenna: {msg.get('antenna_descriptor')}") if "antenna_serial_number" in msg: print(f" Serial: {msg.get('antenna_serial_number')}") elif msg_type == 1033: if "receiver_descriptor" in msg: print(f" Station: {msg.get('station_id')}") print(f" Receiver: {msg.get('receiver_descriptor')}") print(f" Antenna: {msg.get('antenna_descriptor')}") 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 # Export to CSV elapsed = time.monotonic() - start_time print(f"\n{'=' * 80}") print(f"Capture complete: {parser.message_count} messages in {elapsed:.1f} seconds") print(f"{'=' * 80}") if parser.message_count > 0: print(f"\nExporting to CSV: {args.output}") parser.export_to_csv(args.output, message_types=message_types_filter) print(f"✓ Export complete!") # Show message type summary message_type_counts = {} for msg in parser.messages: msg_type = msg.get("message_type") message_type_counts[msg_type] = message_type_counts.get(msg_type, 0) + 1 print(f"\nMessage types captured:") for msg_type in sorted(message_type_counts.keys()): count = message_type_counts[msg_type] print(f" RTCM {msg_type:4d}: {count:5d} messages") else: print("No messages captured!") if __name__ == "__main__": main()