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

278
analyze_rtcm_binary.py Normal file
View File

@@ -0,0 +1,278 @@
#!/usr/bin/env python3
"""
Analyze binary file for RTCM3 messages and ASCII data.
Searches for RTCM3 message headers (0xD3) and displays message info.
Also identifies ASCII text blocks.
"""
import sys
import argparse
from pathlib import Path
def find_rtcm_messages(data: bytes) -> list[dict]:
"""Find all RTCM3 messages in binary data."""
messages = []
i = 0
while i < len(data):
# RTCM3 messages start with 0xD3
if data[i] == 0xD3 and i + 2 < len(data):
# Parse header
length = ((data[i+1] & 0x03) << 8) | data[i+2]
msg_total_len = 3 + length + 3 # header + payload + CRC
if i + msg_total_len <= len(data) and length >= 3:
# Extract message type (first 12 bits of payload)
msg_type = (data[i+3] << 4) | (data[i+4] >> 4)
messages.append({
'offset': i,
'type': msg_type,
'length': length,
'total_length': msg_total_len,
'header': data[i:i+3],
'payload': data[i+3:i+3+length],
'crc': data[i+3+length:i+3+length+3] if i+3+length+3 <= len(data) else None,
})
i += msg_total_len
continue
i += 1
return messages
def find_ascii_blocks(data: bytes, min_length: int = 10) -> list[dict]:
"""Find blocks of ASCII printable text."""
blocks = []
start = None
for i, byte in enumerate(data):
is_printable = 32 <= byte < 127 or byte in [9, 10, 13] # Include tab, LF, CR
if is_printable:
if start is None:
start = i
else:
if start is not None and (i - start) >= min_length:
text = data[start:i].decode('ascii', errors='ignore')
blocks.append({
'offset': start,
'length': i - start,
'text': text,
})
start = None
# Handle final block
if start is not None and (len(data) - start) >= min_length:
text = data[start:].decode('ascii', errors='ignore')
blocks.append({
'offset': start,
'length': len(data) - start,
'text': text,
})
return blocks
def hex_dump(data: bytes, offset: int = 0, max_bytes: int = 64) -> str:
"""Create a hex dump of data."""
lines = []
data = data[:max_bytes]
for i in range(0, len(data), 16):
chunk = data[i:i+16]
hex_part = " ".join(f"{b:02X}" for b in chunk)
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
lines.append(f"{offset + i:08X} {hex_part:<48} {ascii_part}")
return "\n".join(lines)
def get_message_description(msg_type: int) -> str:
"""Get human-readable description for RTCM message type."""
descriptions = {
1005: "Stationary RTK Reference Station ARP",
1006: "Stationary RTK Reference Station ARP + Antenna Height",
1007: "Antenna Descriptor",
1008: "Antenna Descriptor & Serial Number",
1019: "GPS Ephemerides",
1020: "GLONASS Ephemerides",
1033: "Receiver and Antenna Descriptors",
1074: "GPS MSM4",
1075: "GPS MSM5",
1077: "GPS MSM7",
1084: "GLONASS MSM4",
1085: "GLONASS MSM5",
1087: "GLONASS MSM7",
1094: "Galileo MSM4",
1095: "Galileo MSM5",
1097: "Galileo MSM7",
1124: "BeiDou MSM4",
1125: "BeiDou MSM5",
1127: "BeiDou MSM7",
1230: "GLONASS Code-Phase Biases",
}
return descriptions.get(msg_type, f"Type {msg_type}")
def analyze_file(filename: str, show_ascii: bool = True, show_messages: bool = True,
show_hex: bool = False, filter_type: int = None, max_messages: int = None):
"""Analyze binary file for RTCM messages and ASCII data."""
path = Path(filename)
if not path.exists():
print(f"ERROR: File not found: {filename}")
return
print(f"Analyzing: {filename}")
print(f"File size: {path.stat().st_size:,} bytes")
print(f"{'=' * 80}\n")
# Read file
data = path.read_bytes()
# Find ASCII blocks
if show_ascii:
print(f"ASCII TEXT BLOCKS (10+ chars):")
print(f"{'' * 80}")
ascii_blocks = find_ascii_blocks(data, min_length=10)
if ascii_blocks:
for block in ascii_blocks[:20]: # Show first 20 blocks
print(f"Offset: 0x{block['offset']:08X} ({block['offset']}) - Length: {block['length']} bytes")
# Show text with visible whitespace
text = block['text'].replace('\r', '\\r').replace('\n', '\\n').replace('\t', '\\t')
if len(text) > 200:
text = text[:200] + "..."
print(f" Text: {text}")
print()
if len(ascii_blocks) > 20:
print(f"... and {len(ascii_blocks) - 20} more ASCII blocks\n")
else:
print(" (none found)\n")
print()
# Find RTCM messages
if show_messages:
print(f"RTCM3 MESSAGES:")
print(f"{'' * 80}")
messages = find_rtcm_messages(data)
if messages:
# Group by type
type_counts = {}
for msg in messages:
msg_type = msg['type']
type_counts[msg_type] = type_counts.get(msg_type, 0) + 1
print(f"Found {len(messages)} RTCM3 messages\n")
print(f"Message type summary:")
for msg_type in sorted(type_counts.keys()):
desc = get_message_description(msg_type)
print(f" Type {msg_type:4d}: {type_counts[msg_type]:4d} messages - {desc}")
print()
# Show individual messages
print(f"Individual messages:")
print(f"{'' * 80}")
shown_count = 0
for i, msg in enumerate(messages):
msg_type = msg['type']
# Apply filter if specified
if filter_type is not None and msg_type != filter_type:
continue
# Apply max messages limit
if max_messages is not None and shown_count >= max_messages:
remaining = sum(1 for m in messages[i:] if filter_type is None or m['type'] == filter_type)
if remaining > 0:
print(f"\n... and {remaining} more messages (use --max-messages to show more)")
break
shown_count += 1
desc = get_message_description(msg_type)
print(f"\nMessage #{i+1}: Type {msg_type:4d} - {desc}")
print(f" Offset: 0x{msg['offset']:08X} ({msg['offset']})")
print(f" Payload length: {msg['length']} bytes")
print(f" Total length: {msg['total_length']} bytes")
# Show hex dump of payload
if show_hex:
print(f" Payload hex (first 64 bytes):")
hex_lines = hex_dump(msg['payload'], msg['offset'] + 3, max_bytes=64)
for line in hex_lines.split('\n'):
print(f" {line}")
# Check for ASCII in payload
payload = msg['payload']
printable_count = sum(1 for b in payload if 32 <= b < 127)
printable_percent = (printable_count / len(payload) * 100) if len(payload) > 0 else 0
if printable_percent > 50:
print(f" Payload is {printable_percent:.0f}% ASCII printable")
try:
text = payload.decode('ascii', errors='ignore')
text_display = text.replace('\r', '\\r').replace('\n', '\\n').replace('\t', '\\t')
if len(text_display) > 200:
text_display = text_display[:200] + "..."
print(f" ASCII: {text_display}")
except:
pass
# Special handling for specific message types
if msg_type == 208:
print(f" ★ MESSAGE 208 - Proprietary/Custom")
else:
print(" (no RTCM3 messages found)\n")
def main():
parser = argparse.ArgumentParser(
description="Analyze binary file for RTCM3 messages and ASCII data",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Basic analysis
python analyze_rtcm_binary.py ntrip_raw_20250605_120000.bin
# Show hex dumps
python analyze_rtcm_binary.py ntrip_raw_20250605_120000.bin --hex
# Only show message type 208
python analyze_rtcm_binary.py ntrip_raw_20250605_120000.bin --type 208
# Show only first 10 messages
python analyze_rtcm_binary.py ntrip_raw_20250605_120000.bin --max-messages 10
# Skip ASCII blocks, show messages only
python analyze_rtcm_binary.py ntrip_raw_20250605_120000.bin --no-ascii
"""
)
parser.add_argument('filename', help='Binary file to analyze')
parser.add_argument('--no-ascii', action='store_true', help='Do not show ASCII blocks')
parser.add_argument('--no-messages', action='store_true', help='Do not show RTCM messages')
parser.add_argument('--hex', action='store_true', help='Show hex dumps of message payloads')
parser.add_argument('--type', type=int, help='Filter to show only specific message type')
parser.add_argument('--max-messages', type=int, help='Maximum number of messages to display')
args = parser.parse_args()
analyze_file(
args.filename,
show_ascii=not args.no_ascii,
show_messages=not args.no_messages,
show_hex=args.hex,
filter_type=args.type,
max_messages=args.max_messages
)
if __name__ == "__main__":
main()