Initial commit
This commit is contained in:
291
analyze_rtcm_correct.py
Normal file
291
analyze_rtcm_correct.py
Normal file
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Correct RTCM3 message analyzer with proper bit-level parsing.
|
||||
Also detects non-RTCM data (ASCII, NMEA) in the stream.
|
||||
"""
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_rtcm_stream(data: bytes) -> list[dict]:
|
||||
"""Parse RTCM3 stream, identifying both RTCM messages and non-RTCM data."""
|
||||
items = []
|
||||
i = 0
|
||||
|
||||
while i < len(data):
|
||||
# Check for RTCM3 message (0xD3 header)
|
||||
if data[i] == 0xD3 and i + 2 < len(data):
|
||||
# Parse header: 0xD3 + 2 bytes (6 bits reserved + 10 bits length)
|
||||
reserved = (data[i+1] >> 2) & 0x3F
|
||||
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:
|
||||
payload = data[i+3:i+3+length]
|
||||
|
||||
# Extract message type - CORRECTLY from 12 bits
|
||||
# Bits 0-11 of payload contain message type
|
||||
msg_type = (payload[0] << 4) | (payload[1] >> 4)
|
||||
|
||||
# Extract station ID (typically bits 12-23, next 12 bits)
|
||||
station_id = ((payload[1] & 0x0F) << 8) | payload[2]
|
||||
|
||||
# CRC
|
||||
crc = data[i+3+length:i+3+length+3] if i+3+length+3 <= len(data) else None
|
||||
|
||||
items.append({
|
||||
'type': 'rtcm',
|
||||
'offset': i,
|
||||
'message_type': msg_type,
|
||||
'station_id': station_id,
|
||||
'reserved': reserved,
|
||||
'length': length,
|
||||
'total_length': msg_total_len,
|
||||
'payload': payload,
|
||||
'crc': crc,
|
||||
})
|
||||
|
||||
i += msg_total_len
|
||||
continue
|
||||
|
||||
# Check for ASCII data (NMEA, text, etc.)
|
||||
# Look for printable ASCII or common NMEA starters
|
||||
if data[i] == ord('$') or (32 <= data[i] < 127):
|
||||
# Find end of ASCII block
|
||||
start = i
|
||||
while i < len(data) and (32 <= data[i] < 127 or data[i] in [9, 10, 13]):
|
||||
i += 1
|
||||
|
||||
if i > start:
|
||||
text = data[start:i].decode('ascii', errors='ignore')
|
||||
items.append({
|
||||
'type': 'ascii',
|
||||
'offset': start,
|
||||
'length': i - start,
|
||||
'text': text,
|
||||
})
|
||||
continue
|
||||
|
||||
# Unknown/binary data
|
||||
i += 1
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_message_description(msg_type: int) -> str:
|
||||
"""Get human-readable description for RTCM message type."""
|
||||
descriptions = {
|
||||
1001: "GPS L1 RTK Observables",
|
||||
1002: "GPS L1 RTK Observables (Extended)",
|
||||
1003: "GPS L1/L2 RTK Observables",
|
||||
1004: "GPS L1/L2 RTK Observables (Extended)",
|
||||
1005: "Stationary RTK Reference Station ARP",
|
||||
1006: "Stationary RTK Reference Station ARP + Antenna Height",
|
||||
1007: "Antenna Descriptor",
|
||||
1008: "Antenna Descriptor & Serial Number",
|
||||
1009: "GLONASS L1 RTK Observables",
|
||||
1010: "GLONASS L1 RTK Observables (Extended)",
|
||||
1011: "GLONASS L1/L2 RTK Observables",
|
||||
1012: "GLONASS L1/L2 RTK Observables (Extended)",
|
||||
1013: "System Parameters",
|
||||
1019: "GPS Ephemerides",
|
||||
1020: "GLONASS Ephemerides",
|
||||
1033: "Receiver and Antenna Descriptors",
|
||||
1071: "GPS MSM1",
|
||||
1072: "GPS MSM2",
|
||||
1073: "GPS MSM3",
|
||||
1074: "GPS MSM4",
|
||||
1075: "GPS MSM5",
|
||||
1076: "GPS MSM6",
|
||||
1077: "GPS MSM7",
|
||||
1081: "GLONASS MSM1",
|
||||
1082: "GLONASS MSM2",
|
||||
1083: "GLONASS MSM3",
|
||||
1084: "GLONASS MSM4",
|
||||
1085: "GLONASS MSM5",
|
||||
1086: "GLONASS MSM6",
|
||||
1087: "GLONASS MSM7",
|
||||
1091: "Galileo MSM1",
|
||||
1092: "Galileo MSM2",
|
||||
1093: "Galileo MSM3",
|
||||
1094: "Galileo MSM4",
|
||||
1095: "Galileo MSM5",
|
||||
1096: "Galileo MSM6",
|
||||
1097: "Galileo MSM7",
|
||||
1121: "BeiDou MSM1",
|
||||
1122: "BeiDou MSM2",
|
||||
1123: "BeiDou MSM3",
|
||||
1124: "BeiDou MSM4",
|
||||
1125: "BeiDou MSM5",
|
||||
1126: "BeiDou MSM6",
|
||||
1127: "BeiDou MSM7",
|
||||
1230: "GLONASS Code-Phase Biases",
|
||||
}
|
||||
return descriptions.get(msg_type, f"Unknown/Proprietary Type {msg_type}")
|
||||
|
||||
|
||||
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 analyze_file(filename: str, show_hex: bool = False, filter_type: int = None,
|
||||
max_items: int = 50, show_ascii: bool = True):
|
||||
"""Analyze binary file with correct RTCM parsing."""
|
||||
|
||||
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 and parse
|
||||
data = path.read_bytes()
|
||||
items = parse_rtcm_stream(data)
|
||||
|
||||
# Statistics
|
||||
rtcm_count = sum(1 for item in items if item['type'] == 'rtcm')
|
||||
ascii_count = sum(1 for item in items if item['type'] == 'ascii')
|
||||
|
||||
print(f"STREAM SUMMARY:")
|
||||
print(f"{'─' * 80}")
|
||||
print(f"Total RTCM messages: {rtcm_count}")
|
||||
print(f"Total ASCII blocks: {ascii_count}")
|
||||
print()
|
||||
|
||||
# Message type distribution
|
||||
if rtcm_count > 0:
|
||||
type_counts = {}
|
||||
station_ids = set()
|
||||
|
||||
for item in items:
|
||||
if item['type'] == 'rtcm':
|
||||
msg_type = item['message_type']
|
||||
type_counts[msg_type] = type_counts.get(msg_type, 0) + 1
|
||||
station_ids.add(item['station_id'])
|
||||
|
||||
print(f"RTCM MESSAGE TYPES:")
|
||||
print(f"{'─' * 80}")
|
||||
for msg_type in sorted(type_counts.keys()):
|
||||
desc = get_message_description(msg_type)
|
||||
count = type_counts[msg_type]
|
||||
print(f" Type {msg_type:4d}: {count:5d} messages - {desc}")
|
||||
|
||||
print()
|
||||
print(f"Station IDs found: {sorted(station_ids)}")
|
||||
print()
|
||||
|
||||
# Show individual items
|
||||
print(f"STREAM CONTENT (first {max_items} items):")
|
||||
print(f"{'=' * 80}\n")
|
||||
|
||||
shown = 0
|
||||
for i, item in enumerate(items):
|
||||
if shown >= max_items:
|
||||
remaining = len(items) - i
|
||||
print(f"\n... and {remaining} more items (use --max-items to show more)")
|
||||
break
|
||||
|
||||
# Filter if requested
|
||||
if filter_type is not None and (item['type'] != 'rtcm' or item.get('message_type') != filter_type):
|
||||
continue
|
||||
|
||||
shown += 1
|
||||
|
||||
if item['type'] == 'rtcm':
|
||||
msg_type = item['message_type']
|
||||
desc = get_message_description(msg_type)
|
||||
|
||||
print(f"[{i+1}] RTCM Message {msg_type:4d} - {desc}")
|
||||
print(f" Offset: 0x{item['offset']:08X} ({item['offset']})")
|
||||
print(f" Station ID: {item['station_id']}")
|
||||
print(f" Length: {item['length']} bytes payload, {item['total_length']} bytes total")
|
||||
|
||||
if show_hex:
|
||||
print(f" Payload (first 64 bytes):")
|
||||
hex_lines = hex_dump(item['payload'], item['offset'] + 3, max_bytes=64)
|
||||
for line in hex_lines.split('\n'):
|
||||
print(f" {line}")
|
||||
|
||||
# Check for ASCII in payload
|
||||
payload = item['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 > 30:
|
||||
print(f" Contains {printable_percent:.0f}% ASCII text:")
|
||||
try:
|
||||
text = payload.decode('ascii', errors='ignore')
|
||||
text_display = text.replace('\r', '\\r').replace('\n', '\\n').replace('\t', '\\t')
|
||||
if len(text_display) > 150:
|
||||
text_display = text_display[:150] + "..."
|
||||
print(f" {text_display}")
|
||||
except:
|
||||
pass
|
||||
|
||||
print()
|
||||
|
||||
elif item['type'] == 'ascii' and show_ascii:
|
||||
text = item['text'].replace('\r', '\\r').replace('\n', '\\n').replace('\t', '\\t')
|
||||
if len(text) > 150:
|
||||
text = text[:150] + "..."
|
||||
|
||||
print(f"[{i+1}] ASCII DATA")
|
||||
print(f" Offset: 0x{item['offset']:08X} ({item['offset']})")
|
||||
print(f" Length: {item['length']} bytes")
|
||||
print(f" Text: {text}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze RTCM3 binary file with correct message type parsing",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Basic analysis
|
||||
python analyze_rtcm_correct.py ntrip_raw_20250605_120000.bin
|
||||
|
||||
# Show hex dumps
|
||||
python analyze_rtcm_correct.py ntrip_raw_20250605_120000.bin --hex
|
||||
|
||||
# Only show specific message type
|
||||
python analyze_rtcm_correct.py ntrip_raw_20250605_120000.bin --type 1005
|
||||
|
||||
# Show more items
|
||||
python analyze_rtcm_correct.py ntrip_raw_20250605_120000.bin --max-items 100
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('filename', help='Binary file to analyze')
|
||||
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-items', type=int, default=50, help='Maximum number of items to display')
|
||||
parser.add_argument('--no-ascii', action='store_true', help='Hide ASCII blocks in output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
analyze_file(
|
||||
args.filename,
|
||||
show_hex=args.hex,
|
||||
filter_type=args.type,
|
||||
max_items=args.max_items,
|
||||
show_ascii=not args.no_ascii
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user