195 lines
6.9 KiB
Python
195 lines
6.9 KiB
Python
#!/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()
|