parent
4b50fe2a2a
commit
a55cd76ead
3 changed files with 0 additions and 417 deletions
@ -1,308 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
""" |
||||
Test suite for the Python ubloxd implementation. |
||||
|
||||
Tests UBLOX message parsing, processing, and cereal message generation. |
||||
""" |
||||
|
||||
import struct |
||||
|
||||
from openpilot.system.ubloxd.ubloxd import ( |
||||
UBXMessageParser, UBXMessageProcessor, UBXMessage, |
||||
UBLOX_PREAMBLE1, UBLOX_PREAMBLE2, UBXClass, UBXMessageID |
||||
) |
||||
|
||||
|
||||
class TestUBXMessageParser: |
||||
"""Test UBLOX message parser functionality""" |
||||
|
||||
def setup_method(self): |
||||
self.parser = UBXMessageParser() |
||||
|
||||
def test_calculate_checksum(self): |
||||
"""Test UBLOX checksum calculation""" |
||||
# Test message: UBX header + simple payload |
||||
msg = bytearray([0xb5, 0x62, 0x01, 0x07, 0x04, 0x00, 0x01, 0x02, 0x03, 0x04]) |
||||
|
||||
ck_a, ck_b = self.parser._calculate_checksum(msg, 2, 10) |
||||
assert isinstance(ck_a, int) |
||||
assert isinstance(ck_b, int) |
||||
assert 0 <= ck_a <= 255 |
||||
assert 0 <= ck_b <= 255 |
||||
|
||||
def test_simple_message_parsing(self): |
||||
"""Test parsing a complete simple message""" |
||||
# Create a simple test message: UBX-NAV-PVT stub |
||||
payload = b'\x00' * 92 # Minimum NAV-PVT payload size |
||||
msg_data = struct.pack('<BBBBH', UBLOX_PREAMBLE1, UBLOX_PREAMBLE2, |
||||
UBXClass.NAV, UBXMessageID.NAV.PVT, len(payload)) |
||||
msg_data += payload |
||||
|
||||
# Add checksum |
||||
ck_a = ck_b = 0 |
||||
for byte in msg_data[2:]: |
||||
ck_a = (ck_a + byte) & 0xFF |
||||
ck_b = (ck_b + ck_a) & 0xFF |
||||
msg_data += bytes([ck_a, ck_b]) |
||||
|
||||
# Parse the message |
||||
complete, consumed = self.parser.add_data(123.456, msg_data) |
||||
assert complete |
||||
assert consumed == len(msg_data) |
||||
|
||||
# Extract the parsed message |
||||
parsed_msg = self.parser.parse_message() |
||||
assert parsed_msg is not None |
||||
assert parsed_msg.msg_class == UBXClass.NAV |
||||
assert parsed_msg.msg_id == UBXMessageID.NAV.PVT |
||||
assert len(parsed_msg.payload) == 92 |
||||
assert parsed_msg.checksum_valid |
||||
assert parsed_msg.log_time == 123.456 |
||||
|
||||
def test_incremental_parsing(self): |
||||
"""Test parsing message received in multiple chunks""" |
||||
# Create test message |
||||
payload = b'\x11\x22\x33\x44' |
||||
msg_data = struct.pack('<BBBBH', UBLOX_PREAMBLE1, UBLOX_PREAMBLE2, |
||||
0x01, 0x02, len(payload)) |
||||
msg_data += payload |
||||
|
||||
# Add checksum |
||||
ck_a = ck_b = 0 |
||||
for byte in msg_data[2:]: |
||||
ck_a = (ck_a + byte) & 0xFF |
||||
ck_b = (ck_b + ck_a) & 0xFF |
||||
msg_data += bytes([ck_a, ck_b]) |
||||
|
||||
# Send message in chunks |
||||
chunk1 = msg_data[:4] # Header partial |
||||
chunk2 = msg_data[4:8] # Header complete + payload start |
||||
chunk3 = msg_data[8:] # Rest of payload + checksum |
||||
|
||||
# Process chunks |
||||
complete1, consumed1 = self.parser.add_data(100.0, chunk1) |
||||
assert not complete1 |
||||
assert consumed1 == len(chunk1) |
||||
|
||||
complete2, consumed2 = self.parser.add_data(100.0, chunk2) |
||||
assert not complete2 |
||||
assert consumed2 == len(chunk2) |
||||
|
||||
complete3, consumed3 = self.parser.add_data(100.0, chunk3) |
||||
assert complete3 |
||||
assert consumed3 == len(chunk3) |
||||
|
||||
# Verify parsed message |
||||
parsed_msg = self.parser.parse_message() |
||||
assert parsed_msg is not None |
||||
assert parsed_msg.payload == payload |
||||
|
||||
def test_corrupted_data_recovery(self): |
||||
"""Test recovery from corrupted message data""" |
||||
# Send some garbage data followed by valid message |
||||
garbage = b'\x00\x11\x22\x33\x44\x55' |
||||
|
||||
# Valid message |
||||
payload = b'\xaa\xbb' |
||||
msg_data = struct.pack('<BBBBH', UBLOX_PREAMBLE1, UBLOX_PREAMBLE2, |
||||
0x01, 0x02, len(payload)) |
||||
msg_data += payload |
||||
|
||||
# Add checksum |
||||
ck_a = ck_b = 0 |
||||
for byte in msg_data[2:]: |
||||
ck_a = (ck_a + byte) & 0xFF |
||||
ck_b = (ck_b + ck_a) & 0xFF |
||||
msg_data += bytes([ck_a, ck_b]) |
||||
|
||||
combined_data = garbage + msg_data |
||||
|
||||
# Parser should skip garbage and find valid message |
||||
complete, consumed = self.parser.add_data(100.0, combined_data) |
||||
assert complete |
||||
|
||||
parsed_msg = self.parser.parse_message() |
||||
assert parsed_msg is not None |
||||
assert parsed_msg.payload == payload |
||||
|
||||
def test_reset(self): |
||||
"""Test parser reset functionality""" |
||||
# Add some data |
||||
test_data = b'\xb5\x62\x01\x02' |
||||
self.parser.add_data(100.0, test_data) |
||||
assert len(self.parser.parse_buffer) > 0 |
||||
|
||||
# Reset should clear buffer |
||||
self.parser.reset() |
||||
assert len(self.parser.parse_buffer) == 0 |
||||
|
||||
|
||||
class TestUBXMessageProcessor: |
||||
"""Test UBLOX message processing and cereal event generation""" |
||||
|
||||
def setup_method(self): |
||||
self.processor = UBXMessageProcessor() |
||||
|
||||
def test_process_nav_pvt(self, mocker): |
||||
"""Test processing of NAV-PVT messages""" |
||||
# Mock cereal message |
||||
mock_event = mocker.MagicMock() |
||||
mock_location = mocker.MagicMock() |
||||
mock_event.gpsLocationExternal = mock_location |
||||
mock_new_message = mocker.patch('openpilot.system.ubloxd.ubloxd.messaging.new_message', |
||||
return_value=mock_event) |
||||
|
||||
# Create NAV-PVT payload with test data |
||||
fixType = 3 # 3D fix |
||||
numSV = 12 # 12 satellites |
||||
|
||||
lon = int(-122.419416 * 1e7) # San Francisco longitude |
||||
lat = int(37.774929 * 1e7) # San Francisco latitude |
||||
height = 100000 # 100m above ellipsoid (in mm) |
||||
hAcc = 5000 # 5m horizontal accuracy (in mm) |
||||
vAcc = 10000 # 10m vertical accuracy (in mm) |
||||
|
||||
gSpeed = 2236 # ground speed ~2.24 m/s (in mm/s) |
||||
headMot = 4500000 # 45 degrees heading (in 1e-5 deg) |
||||
sAcc = 500 # 0.5 m/s speed accuracy (in mm/s) |
||||
headAcc = 1800000 # 18 degree heading accuracy (in 1e-5 deg) |
||||
|
||||
# Pack the payload (first 92 bytes of NAV-PVT) |
||||
payload = struct.pack('<IHBBBBBBIiBBBBiiiiIIiiiiiIIHHHHHHBBBB', |
||||
123000, # iTOW |
||||
2024, 1, 15, 12, 30, 45, # year, month, day, hour, min, sec |
||||
0x07, # valid flags |
||||
1000, # tAcc |
||||
0, # nano |
||||
fixType, # fixType (3D) |
||||
0x01, # flags |
||||
0x00, # flags2 |
||||
numSV, # numSV |
||||
lon, lat, height, 5000, hAcc, vAcc, |
||||
0, 0, 0, gSpeed, headMot, sAcc, headAcc, 100, |
||||
0, 0, 0, 0, 0, 0, 0, 0, 0) # Reserved fields |
||||
|
||||
# Create UBX message |
||||
msg = UBXMessage( |
||||
msg_class=UBXClass.NAV, |
||||
msg_id=UBXMessageID.NAV.PVT, |
||||
payload=payload, |
||||
checksum_valid=True, |
||||
log_time=100.0 |
||||
) |
||||
|
||||
# Process message |
||||
events = self.processor.process_message(msg) |
||||
|
||||
# Verify results |
||||
assert len(events) == 1 |
||||
mock_new_message.assert_called_once_with('gpsLocationExternal', valid=True) |
||||
|
||||
# Check that location fields were set correctly |
||||
assert mock_location.unixTimestampMillis == 100000 |
||||
assert abs(mock_location.latitude - 37.774929) < 1e-6 |
||||
assert abs(mock_location.longitude - (-122.419416)) < 1e-6 |
||||
assert abs(mock_location.altitude - 100.0) < 0.01 |
||||
assert abs(mock_location.speed - 2.236) < 0.001 |
||||
assert abs(mock_location.bearingDeg - 45.0) < 0.1 |
||||
assert abs(mock_location.horizontalAccuracy - 5.0) < 0.01 |
||||
assert mock_location.hasFix |
||||
assert mock_location.satelliteCount == 12 |
||||
|
||||
def test_process_invalid_message(self): |
||||
"""Test handling of invalid/corrupted messages""" |
||||
# Create message with invalid payload (too short for NAV-PVT) |
||||
msg = UBXMessage( |
||||
msg_class=UBXClass.NAV, |
||||
msg_id=UBXMessageID.NAV.PVT, |
||||
payload=b'\x00\x01\x02', # Too short |
||||
checksum_valid=True, |
||||
log_time=100.0 |
||||
) |
||||
|
||||
# Should handle gracefully without crashing |
||||
events = self.processor.process_message(msg) |
||||
assert len(events) == 0 |
||||
|
||||
def test_process_unknown_message(self): |
||||
"""Test handling of unknown message types""" |
||||
msg = UBXMessage( |
||||
msg_class=0xFF, # Unknown class |
||||
msg_id=0xFF, # Unknown ID |
||||
payload=b'\x00\x01\x02\x03', |
||||
checksum_valid=True, |
||||
log_time=100.0 |
||||
) |
||||
|
||||
# Should handle gracefully |
||||
events = self.processor.process_message(msg) |
||||
assert len(events) == 0 |
||||
|
||||
|
||||
class TestIntegration: |
||||
"""Integration tests for complete parsing pipeline""" |
||||
|
||||
def test_end_to_end_nav_pvt(self, mocker): |
||||
"""Test complete NAV-PVT message parsing and processing""" |
||||
parser = UBXMessageParser() |
||||
processor = UBXMessageProcessor() |
||||
|
||||
# Create realistic NAV-PVT message |
||||
payload = struct.pack('<IHBBBBBBIiBBBBiiiiIIiiiiiIIHHHHHHBBBB', |
||||
123000, # iTOW |
||||
2024, 1, 15, 12, 30, 45, # year, month, day, hour, min, sec |
||||
0x07, # valid flags |
||||
1000, # tAcc |
||||
0, # nano |
||||
3, # fixType (3D) |
||||
0x01, # flags |
||||
0x00, # flags2 |
||||
10, # numSV |
||||
int(-122.0 * 1e7), # lon |
||||
int(37.0 * 1e7), # lat |
||||
10000, # height (10m) |
||||
5000, # hMSL (5m) |
||||
2000, # hAcc (2m) |
||||
3000, # vAcc (3m) |
||||
0, 0, 0, # velN, velE, velD |
||||
0, # gSpeed |
||||
0, # headMot |
||||
0, # sAcc |
||||
0, # headAcc |
||||
100, # pDOP |
||||
0, 0, 0, 0, 0, 0, 0, 0, 0) # reserved |
||||
|
||||
# Build complete message with header and checksum |
||||
msg_data = struct.pack('<BBBBH', UBLOX_PREAMBLE1, UBLOX_PREAMBLE2, |
||||
UBXClass.NAV, UBXMessageID.NAV.PVT, len(payload)) |
||||
msg_data += payload |
||||
|
||||
# Calculate and add checksum |
||||
ck_a = ck_b = 0 |
||||
for byte in msg_data[2:]: |
||||
ck_a = (ck_a + byte) & 0xFF |
||||
ck_b = (ck_b + ck_a) & 0xFF |
||||
msg_data += bytes([ck_a, ck_b]) |
||||
|
||||
# Parse message |
||||
mock_event = mocker.MagicMock() |
||||
mock_location = mocker.MagicMock() |
||||
mock_event.gpsLocationExternal = mock_location |
||||
mock_new_message = mocker.patch('openpilot.system.ubloxd.ubloxd.messaging.new_message', |
||||
return_value=mock_event) |
||||
|
||||
complete, consumed = parser.add_data(200.0, msg_data) |
||||
assert complete |
||||
|
||||
parsed_msg = parser.parse_message() |
||||
assert parsed_msg is not None |
||||
assert parsed_msg.checksum_valid |
||||
|
||||
events = processor.process_message(parsed_msg) |
||||
assert len(events) == 1 |
||||
|
||||
# Verify location was processed correctly |
||||
mock_new_message.assert_called_once_with('gpsLocationExternal', valid=True) |
||||
assert mock_location.unixTimestampMillis == 200000 |
||||
assert abs(mock_location.latitude - 37.0) < 0.1 |
||||
assert abs(mock_location.longitude - (-122.0)) < 0.1 |
@ -1,20 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
import time |
||||
import cereal.messaging as messaging |
||||
|
||||
if __name__ == "__main__": |
||||
sm = messaging.SubMaster(['ubloxGnss', 'gpsLocationExternal']) |
||||
|
||||
while 1: |
||||
ug = sm['ubloxGnss'] |
||||
gle = sm['gpsLocationExternal'] |
||||
|
||||
try: |
||||
cnos = [] |
||||
for m in ug.measurementReport.measurements: |
||||
cnos.append(m.cno) |
||||
print(f"Sats: {ug.measurementReport.numMeas} Accuracy: {gle.horizontalAccuracy:.2f} m cnos", sorted(cnos)) |
||||
except Exception: |
||||
pass |
||||
sm.update() |
||||
time.sleep(0.1) |
@ -1,89 +0,0 @@ |
||||
#!/usr/bin/env python3 |
||||
# type: ignore |
||||
|
||||
from openpilot.selfdrive.locationd.test import ublox |
||||
import struct |
||||
|
||||
baudrate = 460800 |
||||
rate = 100 # send new data every 100ms |
||||
|
||||
|
||||
def configure_ublox(dev): |
||||
# configure ports and solution parameters and rate |
||||
dev.configure_port(port=ublox.PORT_USB, inMask=1, outMask=1) # enable only UBX on USB |
||||
dev.configure_port(port=0, inMask=0, outMask=0) # disable DDC |
||||
|
||||
payload = struct.pack('<BBHIIHHHBB', 1, 0, 0, 2240, baudrate, 1, 1, 0, 0, 0) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_PRT, payload) # enable UART |
||||
|
||||
dev.configure_port(port=4, inMask=0, outMask=0) # disable SPI |
||||
dev.configure_poll_port() |
||||
dev.configure_poll_port(ublox.PORT_SERIAL1) |
||||
dev.configure_poll_port(ublox.PORT_USB) |
||||
dev.configure_solution_rate(rate_ms=rate) |
||||
|
||||
# Configure solution |
||||
payload = struct.pack('<HBBIIBB4H6BH6B', 5, 4, 3, 0, 0, |
||||
0, 0, 0, 0, 0, |
||||
0, 0, 0, 0, 0, |
||||
0, 0, 0, 0, 0, |
||||
0, 0, 0, 0) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAV5, payload) |
||||
payload = struct.pack('<B3BBB6BBB2BBB2B', 0, 0, 0, 0, 1, |
||||
3, 0, 0, 0, 0, |
||||
0, 0, 0, 0, 0, |
||||
0, 0, 0, 0, 0) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_ODO, payload) |
||||
#bits_ITMF_config1 = '10101101011000101010110111111111' |
||||
#bits_ITMF_config2 = '00000000000000000110001100011110' |
||||
ITMF_config1 = 2908925439 |
||||
ITMF_config2 = 25374 |
||||
payload = struct.pack('<II', ITMF_config1, ITMF_config2) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_ITMF, payload) |
||||
payload = struct.pack('<HHIBBBBBBBBBBH6BBB2BH4B3BB', 0, (1 << 10), 0, 0, 0, |
||||
0, 0, 0, 0, 0, 0, |
||||
0, 1, 0, 0, 0, 0, |
||||
0, 0, 0, 0, 0, 0, |
||||
0, 0, 0, 0, 0, 0, |
||||
0, 0, 0, 0) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAVX5, payload) |
||||
|
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAV5) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_NAVX5) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_ODO) |
||||
dev.configure_poll(ublox.CLASS_CFG, ublox.MSG_CFG_ITMF) |
||||
|
||||
# Configure RAW, PVT and HW messages to be sent every solution cycle |
||||
dev.configure_message_rate(ublox.CLASS_NAV, ublox.MSG_NAV_PVT, 1) |
||||
dev.configure_message_rate(ublox.CLASS_RXM, ublox.MSG_RXM_RAW, 1) |
||||
dev.configure_message_rate(ublox.CLASS_RXM, ublox.MSG_RXM_SFRBX, 1) |
||||
dev.configure_message_rate(ublox.CLASS_MON, ublox.MSG_MON_HW, 1) |
||||
dev.configure_message_rate(ublox.CLASS_MON, ublox.MSG_MON_HW2, 1) |
||||
dev.configure_message_rate(ublox.CLASS_NAV, ublox.MSG_NAV_SAT, 1) |
||||
|
||||
# Query the backup restore status |
||||
print("backup restore polling message (implement custom response handler!):") |
||||
dev.configure_poll(0x09, 0x14) |
||||
|
||||
print("if successful, send this to clear the flash:") |
||||
dev.send_message(0x09, 0x14, b"\x01\x00\x00\x00") |
||||
|
||||
print("send on stop:") |
||||
|
||||
# Save on shutdown |
||||
# Controlled GNSS stop and hot start |
||||
payload = struct.pack('<HBB', 0x0000, 0x08, 0x00) |
||||
dev.send_message(ublox.CLASS_CFG, ublox.MSG_CFG_RST, payload) |
||||
|
||||
# UBX-UPD-SOS backup |
||||
dev.send_message(0x09, 0x14, b"\x00\x00\x00\x00") |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
class Device: |
||||
def write(self, s): |
||||
d = '"{}"s'.format(''.join(f'\\x{b:02X}' for b in s)) |
||||
print(f" if (!send_with_ack({d})) continue;") |
||||
|
||||
dev = ublox.UBlox(Device(), baudrate=baudrate) |
||||
configure_ublox(dev) |
Loading…
Reference in new issue