Files
maglink-console/rtcm_208_capture.py
brentperteet 5703c05c1d Initial commit
2026-06-24 11:12:44 -05:00

191 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""
Capture raw RTCM message 208 to binary file for manual analysis.
"""
import base64
import socket
import time
from datetime import datetime
# NTRIP configuration
CASTER_HOST = "truertk.pointonenav.com"
CASTER_PORT = 2101
MOUNTPOINT = "AUTO"
USERNAME = "9t7fwfbm57"
PASSWORD = "96m7bec9g8"
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.utcnow().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-Capture/1.0\r\n"
f"Connection: close\r\n"
f"Authorization: Basic {auth}\r\n\r\n"
)
return req.encode("ascii")
def main():
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = f"rtcm_208_raw_{timestamp}.bin"
index_file = f"rtcm_208_index_{timestamp}.txt"
print(f"RTCM Message 208 Raw Capture")
print(f"=" * 80)
print(f"Caster: {CASTER_HOST}:{CASTER_PORT}/{MOUNTPOINT}")
print(f"Output: {output_file}")
print(f"Index: {index_file}")
print(f"Capturing for 60 seconds or 20 messages, whichever comes first...")
print(f"=" * 80)
print()
last_gga_time = 0
start_time = time.monotonic()
msg_208_count = 0
total_messages = 0
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("Connected!\n")
# Open output files
with open(output_file, 'wb') as binfile, open(index_file, 'w') as idxfile:
idxfile.write(f"# RTCM 208 Message Index\n")
idxfile.write(f"# Captured: {datetime.now().isoformat()}\n")
idxfile.write(f"# Format: message_number, offset, length, timestamp\n")
idxfile.write(f"#\n")
file_offset = 0
# Receive loop
while time.monotonic() - start_time < 60 and msg_208_count < 20:
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] Total: {total_messages}, Msg 208: {msg_208_count}/20")
last_gga_time = now
# Receive data
data = sock.recv(4096)
if not data:
print("Connection closed")
break
# Parse messages
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)
total_messages += 1
if msg_type == 208:
msg_208_count += 1
msg_data = data[i+3:i+3+length] # Just the payload
# Write to binary file
binfile.write(msg_data)
binfile.flush()
# Write to index
ts = datetime.now().isoformat()
idxfile.write(f"{msg_208_count},{file_offset},{length},{ts}\n")
idxfile.flush()
print(f" ✓ Captured message 208 #{msg_208_count} - {length} bytes at offset {file_offset}")
file_offset += length
i += msg_total_len
continue
i += 1
except KeyboardInterrupt:
print("\n\nInterrupted by user")
except Exception as e:
print(f"\nERROR: {e}")
import traceback
traceback.print_exc()
finally:
if 'sock' in locals():
try:
sock.close()
except:
pass
print(f"\n{'=' * 80}")
print(f"CAPTURE COMPLETE")
print(f"{'=' * 80}")
print(f"Messages captured: {msg_208_count}")
print(f"Output file: {output_file} ({file_offset} bytes)")
print(f"Index file: {index_file}")
print()
print("To view the binary file:")
print(f" hexdump -C {output_file}")
print(f" xxd {output_file}")
print()
print("To extract a specific message (e.g., message #1):")
print(" 1. Look up offset and length in index file")
print(" 2. Use: dd if={output_file} bs=1 skip=<offset> count=<length> | hexdump -C")
if __name__ == "__main__":
main()