#!/usr/bin/env python3 """ Decoder for RTCM message 208 - appears to be ASCII/text based. Captures and displays message 208 content in multiple formats. """ import base64 import socket import time import csv from datetime import datetime, timezone # 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.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-208-Decoder/1.0\r\n" f"Connection: close\r\n" f"Authorization: Basic {auth}\r\n\r\n" ) return req.encode("ascii") def decode_message_208(data: bytes, msg_num: int) -> dict: """Decode message 208 and extract fields.""" result = { "message_number": msg_num, "timestamp": datetime.now(timezone.utc).isoformat(), "length_bytes": len(data), "raw_hex": data.hex(), } # Try decoding as ASCII text try: text = data.decode('ascii') result["text_ascii"] = text result["text_repr"] = repr(text) except: result["text_ascii"] = None # Try decoding as UTF-8 try: text_utf8 = data.decode('utf-8') result["text_utf8"] = text_utf8 except: result["text_utf8"] = None # Check if it looks like comma-separated values if result.get("text_ascii"): text = result["text_ascii"].strip() if "," in text or ";" in text: result["appears_csv"] = True # Try splitting by common delimiters if "," in text: parts = text.split(",") result["csv_fields"] = parts result["csv_field_count"] = len(parts) elif ";" in text: parts = text.split(";") result["csv_fields"] = parts result["csv_field_count"] = len(parts) # Check if it starts with specific markers if len(data) >= 2: if data[0:2] == b'\r\n': result["starts_with_crlf"] = True # Look for printable characters percentage printable_count = sum(1 for b in data if 32 <= b < 127 or b in [9, 10, 13]) result["printable_percent"] = (printable_count / len(data) * 100) if len(data) > 0 else 0 return result def main(): print(f"RTCM Message 208 Decoder") print(f"=" * 80) print(f"Caster: {CASTER_HOST}:{CASTER_PORT}/{MOUNTPOINT}") print(f"Capturing message 208 for 60 seconds...") print(f"=" * 80) print() messages_208 = [] last_gga_time = 0 start_time = time.monotonic() total_messages = 0 msg_208_count = 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! Capturing messages...\n") # Receive loop while time.monotonic() - start_time < 60: 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 | Total: {total_messages}, Msg 208: {msg_208_count}") 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) msg_data = data[i+3:i+3+length] total_messages += 1 if msg_type == 208: msg_208_count += 1 decoded = decode_message_208(msg_data, msg_208_count) messages_208.append(decoded) # Show first few messages if msg_208_count <= 5: print(f"\n{'─' * 80}") print(f"Message 208 #{msg_208_count} - {length} bytes - {decoded['printable_percent']:.0f}% printable") print(f"{'─' * 80}") if decoded.get("text_ascii"): print(f"ASCII: {decoded['text_repr']}") if decoded.get("appears_csv"): print(f"CSV Fields ({decoded['csv_field_count']}): {decoded['csv_fields']}") else: print(f"Hex: {decoded['raw_hex'][:100]}...") 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 # Analysis print(f"\n{'=' * 80}") print(f"ANALYSIS") print(f"{'=' * 80}") print(f"Total messages: {total_messages}") print(f"Message 208 captured: {msg_208_count}") if messages_208: # Check if they're all ASCII all_ascii = all(m.get("text_ascii") is not None for m in messages_208) print(f"\nAll message 208 are ASCII text: {all_ascii}") # Check for CSV pattern csv_count = sum(1 for m in messages_208 if m.get("appears_csv")) print(f"Messages that appear to be CSV: {csv_count}/{msg_208_count}") # Show field count distribution if csv_count > 0: field_counts = [m.get("csv_field_count", 0) for m in messages_208 if m.get("appears_csv")] print(f"CSV field counts: min={min(field_counts)}, max={max(field_counts)}, mode={max(set(field_counts), key=field_counts.count)}") # Export to CSV csv_filename = "rtcm_208_messages.csv" print(f"\nExporting to CSV: {csv_filename}") with open(csv_filename, 'w', newline='', encoding='utf-8') as csvfile: fieldnames = ["message_number", "timestamp", "length_bytes", "printable_percent", "text_ascii", "text_repr", "csv_field_count", "raw_hex"] writer = csv.DictWriter(csvfile, fieldnames=fieldnames, extrasaction='ignore') writer.writeheader() for msg in messages_208: writer.writerow(msg) print(f"✓ Exported {len(messages_208)} messages to {csv_filename}") # If CSV-like, create a parsed CSV if csv_count > 0: csv_parsed_filename = "rtcm_208_parsed.csv" print(f"\nExporting parsed CSV fields: {csv_parsed_filename}") # Find max field count max_fields = max(m.get("csv_field_count", 0) for m in messages_208 if m.get("appears_csv")) with open(csv_parsed_filename, 'w', newline='', encoding='utf-8') as csvfile: fieldnames = ["message_number", "timestamp"] + [f"field_{i}" for i in range(max_fields)] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for msg in messages_208: if msg.get("appears_csv") and msg.get("csv_fields"): row = { "message_number": msg["message_number"], "timestamp": msg["timestamp"] } for i, field in enumerate(msg["csv_fields"]): row[f"field_{i}"] = field.strip() writer.writerow(row) print(f"✓ Exported parsed fields to {csv_parsed_filename}") # Show sample text from a few messages print(f"\n{'=' * 80}") print("SAMPLE MESSAGE CONTENT:") print(f"{'=' * 80}") for i, msg in enumerate(messages_208[:3]): print(f"\nMessage #{msg['message_number']}:") if msg.get("text_ascii"): # Show with visible whitespace text = msg["text_ascii"].replace('\r', '\\r').replace('\n', '\\n').replace('\t', '\\t') print(f" {text}") else: print(f" (binary data)") if __name__ == "__main__": main()