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

194
rtcm_to_csv.py Normal file
View File

@@ -0,0 +1,194 @@
#!/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()