IQ.Pilot Prebuilt Release @ 27f668a

This commit is contained in:
IQ.Lvbs CI [bot]
2026-09-03 18:23:24 -05:00
commit b073c5182b
2554 changed files with 679696 additions and 0 deletions

View File

@@ -0,0 +1,280 @@
"""
Binary struct parsing DSL.
Defines a declarative schema for binary messages using dataclasses
and type annotations.
"""
import struct
from enum import Enum
from dataclasses import dataclass, is_dataclass
from typing import Annotated, Any, TypeVar, get_args, get_origin
class FieldType:
"""Base class for field type descriptors."""
@dataclass(frozen=True)
class IntType(FieldType):
bits: int
signed: bool
big_endian: bool = False
@dataclass(frozen=True)
class FloatType(FieldType):
bits: int
@dataclass(frozen=True)
class BitsType(FieldType):
bits: int
@dataclass(frozen=True)
class BytesType(FieldType):
size: int
@dataclass(frozen=True)
class ArrayType(FieldType):
element_type: Any
count_field: str
@dataclass(frozen=True)
class SwitchType(FieldType):
selector: str
cases: dict[Any, Any]
default: Any = None
@dataclass(frozen=True)
class EnumType(FieldType):
base_type: FieldType
enum_cls: type[Enum]
@dataclass(frozen=True)
class ConstType(FieldType):
base_type: FieldType
expected: Any
@dataclass(frozen=True)
class SubstreamType(FieldType):
length_field: str
element_type: Any
# Common types - little endian
u8 = IntType(8, False)
u16 = IntType(16, False)
u32 = IntType(32, False)
s8 = IntType(8, True)
s16 = IntType(16, True)
s32 = IntType(32, True)
f32 = FloatType(32)
f64 = FloatType(64)
# Big endian variants
u16be = IntType(16, False, big_endian=True)
u32be = IntType(32, False, big_endian=True)
s16be = IntType(16, True, big_endian=True)
s32be = IntType(32, True, big_endian=True)
def bits(n: int) -> BitsType:
"""Create a bit-level field type."""
return BitsType(n)
def bytes_field(size: int) -> BytesType:
"""Create a fixed-size bytes field."""
return BytesType(size)
def array(element_type: Any, count_field: str) -> ArrayType:
"""Create an array/repeated field."""
return ArrayType(element_type, count_field)
def switch(selector: str, cases: dict[Any, Any], default: Any = None) -> SwitchType:
"""Create a switch-on field."""
return SwitchType(selector, cases, default)
def enum(base_type: Any, enum_cls: type[Enum]) -> EnumType:
"""Create an enum-wrapped field."""
field_type = _field_type_from_spec(base_type)
if field_type is None:
raise TypeError(f"Unsupported field type: {base_type!r}")
return EnumType(field_type, enum_cls)
def const(base_type: Any, expected: Any) -> ConstType:
"""Create a constant-value field."""
field_type = _field_type_from_spec(base_type)
if field_type is None:
raise TypeError(f"Unsupported field type: {base_type!r}")
return ConstType(field_type, expected)
def substream(length_field: str, element_type: Any) -> SubstreamType:
"""Parse a fixed-length substream using an inner schema."""
return SubstreamType(length_field, element_type)
class BinaryReader:
def __init__(self, data: bytes):
self.data = data
self.pos = 0
self.bit_pos = 0 # 0-7, position within current byte
def _require(self, n: int) -> None:
if self.pos + n > len(self.data):
raise EOFError("Unexpected end of data")
def _read_struct(self, fmt: str):
self._align_to_byte()
size = struct.calcsize(fmt)
self._require(size)
value = struct.unpack_from(fmt, self.data, self.pos)[0]
self.pos += size
return value
def read_bytes(self, n: int) -> bytes:
self._align_to_byte()
self._require(n)
result = self.data[self.pos : self.pos + n]
self.pos += n
return result
def read_bits_int_be(self, n: int) -> int:
result = 0
bits_remaining = n
while bits_remaining > 0:
if self.pos >= len(self.data):
raise EOFError("Unexpected end of data while reading bits")
bits_in_byte = 8 - self.bit_pos
bits_to_read = min(bits_remaining, bits_in_byte)
byte_val = self.data[self.pos]
shift = bits_in_byte - bits_to_read
mask = (1 << bits_to_read) - 1
extracted = (byte_val >> shift) & mask
result = (result << bits_to_read) | extracted
self.bit_pos += bits_to_read
bits_remaining -= bits_to_read
if self.bit_pos >= 8:
self.bit_pos = 0
self.pos += 1
return result
def _align_to_byte(self) -> None:
if self.bit_pos > 0:
self.bit_pos = 0
self.pos += 1
T = TypeVar('T', bound='BinaryStruct')
class BinaryStruct:
"""Base class for binary struct definitions."""
def __init_subclass__(cls, **kwargs) -> None:
super().__init_subclass__(**kwargs)
if cls is BinaryStruct:
return
if not is_dataclass(cls):
dataclass(init=False)(cls)
fields = list(getattr(cls, '__annotations__', {}).items())
cls.__binary_fields__ = fields # type: ignore[attr-defined]
@classmethod
def _read(inner_cls, reader: BinaryReader):
obj = inner_cls.__new__(inner_cls)
for name, spec in inner_cls.__binary_fields__:
value = _parse_field(spec, reader, obj)
setattr(obj, name, value)
return obj
cls._read = _read # type: ignore[attr-defined]
@classmethod
def from_bytes(cls: type[T], data: bytes) -> T:
"""Parse struct from bytes."""
reader = BinaryReader(data)
return cls._read(reader)
@classmethod
def _read(cls: type[T], reader: BinaryReader) -> T:
"""Override in subclasses to implement parsing."""
raise NotImplementedError
def _resolve_path(obj: Any, path: str) -> Any:
cur = obj
for part in path.split('.'):
cur = getattr(cur, part)
return cur
def _unwrap_annotated(spec: Any) -> tuple[Any, ...]:
if get_origin(spec) is Annotated:
return get_args(spec)[1:]
return ()
def _field_type_from_spec(spec: Any) -> FieldType | None:
if isinstance(spec, FieldType):
return spec
for item in _unwrap_annotated(spec):
if isinstance(item, FieldType):
return item
return None
def _int_format(field_type: IntType) -> str:
if field_type.bits == 8:
return 'b' if field_type.signed else 'B'
endian = '>' if field_type.big_endian else '<'
if field_type.bits == 16:
code = 'h' if field_type.signed else 'H'
elif field_type.bits == 32:
code = 'i' if field_type.signed else 'I'
else:
raise ValueError(f"Unsupported integer size: {field_type.bits}")
return f"{endian}{code}"
def _float_format(field_type: FloatType) -> str:
if field_type.bits == 32:
return '<f'
if field_type.bits == 64:
return '<d'
raise ValueError(f"Unsupported float size: {field_type.bits}")
def _parse_field(spec: Any, reader: BinaryReader, obj: Any) -> Any:
field_type = _field_type_from_spec(spec)
if field_type is not None:
spec = field_type
if isinstance(spec, ConstType):
value = _parse_field(spec.base_type, reader, obj)
if value != spec.expected:
raise ValueError(f"Invalid constant: expected {spec.expected!r}, got {value!r}")
return value
if isinstance(spec, EnumType):
raw = _parse_field(spec.base_type, reader, obj)
try:
return spec.enum_cls(raw)
except ValueError:
return raw
if isinstance(spec, SwitchType):
key = _resolve_path(obj, spec.selector)
target = spec.cases.get(key, spec.default)
if target is None:
return None
return _parse_field(target, reader, obj)
if isinstance(spec, ArrayType):
count = _resolve_path(obj, spec.count_field)
return [_parse_field(spec.element_type, reader, obj) for _ in range(int(count))]
if isinstance(spec, SubstreamType):
length = _resolve_path(obj, spec.length_field)
data = reader.read_bytes(int(length))
sub_reader = BinaryReader(data)
return _parse_field(spec.element_type, sub_reader, obj)
if isinstance(spec, IntType):
return reader._read_struct(_int_format(spec))
if isinstance(spec, FloatType):
return reader._read_struct(_float_format(spec))
if isinstance(spec, BitsType):
value = reader.read_bits_int_be(spec.bits)
return bool(value) if spec.bits == 1 else value
if isinstance(spec, BytesType):
return reader.read_bytes(spec.size)
if isinstance(spec, type) and issubclass(spec, BinaryStruct):
return spec._read(reader)
raise TypeError(f"Unsupported field spec: {spec!r}")

View File

@@ -0,0 +1,156 @@
"""
Parses GLONASS navigation strings per GLONASS ICD specification.
http://gauss.gge.unb.ca/GLONASS.ICD.pdf
https://www.unavco.org/help/glossary/docs/ICD_GLONASS_4.0_(1998)_en.pdf
"""
from typing import Annotated
from iqpilot.system.ubloxd import binary_struct as bs
class Glonass(bs.BinaryStruct):
class String1(bs.BinaryStruct):
not_used: Annotated[int, bs.bits(2)]
p1: Annotated[int, bs.bits(2)]
t_k: Annotated[int, bs.bits(12)]
x_vel_sign: Annotated[bool, bs.bits(1)]
x_vel_value: Annotated[int, bs.bits(23)]
x_accel_sign: Annotated[bool, bs.bits(1)]
x_accel_value: Annotated[int, bs.bits(4)]
x_sign: Annotated[bool, bs.bits(1)]
x_value: Annotated[int, bs.bits(26)]
@property
def x_vel(self) -> int:
"""Computed x_vel from sign-magnitude representation."""
return (self.x_vel_value * -1) if self.x_vel_sign else self.x_vel_value
@property
def x_accel(self) -> int:
"""Computed x_accel from sign-magnitude representation."""
return (self.x_accel_value * -1) if self.x_accel_sign else self.x_accel_value
@property
def x(self) -> int:
"""Computed x from sign-magnitude representation."""
return (self.x_value * -1) if self.x_sign else self.x_value
class String2(bs.BinaryStruct):
b_n: Annotated[int, bs.bits(3)]
p2: Annotated[bool, bs.bits(1)]
t_b: Annotated[int, bs.bits(7)]
not_used: Annotated[int, bs.bits(5)]
y_vel_sign: Annotated[bool, bs.bits(1)]
y_vel_value: Annotated[int, bs.bits(23)]
y_accel_sign: Annotated[bool, bs.bits(1)]
y_accel_value: Annotated[int, bs.bits(4)]
y_sign: Annotated[bool, bs.bits(1)]
y_value: Annotated[int, bs.bits(26)]
@property
def y_vel(self) -> int:
"""Computed y_vel from sign-magnitude representation."""
return (self.y_vel_value * -1) if self.y_vel_sign else self.y_vel_value
@property
def y_accel(self) -> int:
"""Computed y_accel from sign-magnitude representation."""
return (self.y_accel_value * -1) if self.y_accel_sign else self.y_accel_value
@property
def y(self) -> int:
"""Computed y from sign-magnitude representation."""
return (self.y_value * -1) if self.y_sign else self.y_value
class String3(bs.BinaryStruct):
p3: Annotated[bool, bs.bits(1)]
gamma_n_sign: Annotated[bool, bs.bits(1)]
gamma_n_value: Annotated[int, bs.bits(10)]
not_used: Annotated[bool, bs.bits(1)]
p: Annotated[int, bs.bits(2)]
l_n: Annotated[bool, bs.bits(1)]
z_vel_sign: Annotated[bool, bs.bits(1)]
z_vel_value: Annotated[int, bs.bits(23)]
z_accel_sign: Annotated[bool, bs.bits(1)]
z_accel_value: Annotated[int, bs.bits(4)]
z_sign: Annotated[bool, bs.bits(1)]
z_value: Annotated[int, bs.bits(26)]
@property
def gamma_n(self) -> int:
"""Computed gamma_n from sign-magnitude representation."""
return (self.gamma_n_value * -1) if self.gamma_n_sign else self.gamma_n_value
@property
def z_vel(self) -> int:
"""Computed z_vel from sign-magnitude representation."""
return (self.z_vel_value * -1) if self.z_vel_sign else self.z_vel_value
@property
def z_accel(self) -> int:
"""Computed z_accel from sign-magnitude representation."""
return (self.z_accel_value * -1) if self.z_accel_sign else self.z_accel_value
@property
def z(self) -> int:
"""Computed z from sign-magnitude representation."""
return (self.z_value * -1) if self.z_sign else self.z_value
class String4(bs.BinaryStruct):
tau_n_sign: Annotated[bool, bs.bits(1)]
tau_n_value: Annotated[int, bs.bits(21)]
delta_tau_n_sign: Annotated[bool, bs.bits(1)]
delta_tau_n_value: Annotated[int, bs.bits(4)]
e_n: Annotated[int, bs.bits(5)]
not_used_1: Annotated[int, bs.bits(14)]
p4: Annotated[bool, bs.bits(1)]
f_t: Annotated[int, bs.bits(4)]
not_used_2: Annotated[int, bs.bits(3)]
n_t: Annotated[int, bs.bits(11)]
n: Annotated[int, bs.bits(5)]
m: Annotated[int, bs.bits(2)]
@property
def tau_n(self) -> int:
"""Computed tau_n from sign-magnitude representation."""
return (self.tau_n_value * -1) if self.tau_n_sign else self.tau_n_value
@property
def delta_tau_n(self) -> int:
"""Computed delta_tau_n from sign-magnitude representation."""
return (self.delta_tau_n_value * -1) if self.delta_tau_n_sign else self.delta_tau_n_value
class String5(bs.BinaryStruct):
n_a: Annotated[int, bs.bits(11)]
tau_c: Annotated[int, bs.bits(32)]
not_used: Annotated[bool, bs.bits(1)]
n_4: Annotated[int, bs.bits(5)]
tau_gps: Annotated[int, bs.bits(22)]
l_n: Annotated[bool, bs.bits(1)]
class StringNonImmediate(bs.BinaryStruct):
data_1: Annotated[int, bs.bits(64)]
data_2: Annotated[int, bs.bits(8)]
idle_chip: Annotated[bool, bs.bits(1)]
string_number: Annotated[int, bs.bits(4)]
data: Annotated[
object,
bs.switch(
'string_number',
{
1: String1,
2: String2,
3: String3,
4: String4,
5: String5,
},
default=StringNonImmediate,
),
]
hamming_code: Annotated[int, bs.bits(8)]
pad_1: Annotated[int, bs.bits(11)]
superframe_number: Annotated[int, bs.bits(16)]
pad_2: Annotated[int, bs.bits(8)]
frame_number: Annotated[int, bs.bits(8)]

View File

@@ -0,0 +1,116 @@
"""
Parses GPS navigation subframes per IS-GPS-200E specification.
https://www.gps.gov/technical/icwg/IS-GPS-200E.pdf
"""
from typing import Annotated
from iqpilot.system.ubloxd import binary_struct as bs
class Gps(bs.BinaryStruct):
class Tlm(bs.BinaryStruct):
preamble: Annotated[bytes, bs.const(bs.bytes_field(1), b"\x8b")]
tlm: Annotated[int, bs.bits(14)]
integrity_status: Annotated[bool, bs.bits(1)]
reserved: Annotated[bool, bs.bits(1)]
class How(bs.BinaryStruct):
tow_count: Annotated[int, bs.bits(17)]
alert: Annotated[bool, bs.bits(1)]
anti_spoof: Annotated[bool, bs.bits(1)]
subframe_id: Annotated[int, bs.bits(3)]
reserved: Annotated[int, bs.bits(2)]
class Subframe1(bs.BinaryStruct):
week_no: Annotated[int, bs.bits(10)]
code: Annotated[int, bs.bits(2)]
sv_accuracy: Annotated[int, bs.bits(4)]
sv_health: Annotated[int, bs.bits(6)]
iodc_msb: Annotated[int, bs.bits(2)]
l2_p_data_flag: Annotated[bool, bs.bits(1)]
reserved1: Annotated[int, bs.bits(23)]
reserved2: Annotated[int, bs.bits(24)]
reserved3: Annotated[int, bs.bits(24)]
reserved4: Annotated[int, bs.bits(16)]
t_gd: Annotated[int, bs.s8]
iodc_lsb: Annotated[int, bs.u8]
t_oc: Annotated[int, bs.u16be]
af_2: Annotated[int, bs.s8]
af_1: Annotated[int, bs.s16be]
af_0_sign: Annotated[bool, bs.bits(1)]
af_0_value: Annotated[int, bs.bits(21)]
reserved5: Annotated[int, bs.bits(2)]
@property
def af_0(self) -> int:
"""Computed af_0 from sign-magnitude representation."""
return (self.af_0_value - (1 << 21)) if self.af_0_sign else self.af_0_value
class Subframe2(bs.BinaryStruct):
iode: Annotated[int, bs.u8]
c_rs: Annotated[int, bs.s16be]
delta_n: Annotated[int, bs.s16be]
m_0: Annotated[int, bs.s32be]
c_uc: Annotated[int, bs.s16be]
e: Annotated[int, bs.s32be]
c_us: Annotated[int, bs.s16be]
sqrt_a: Annotated[int, bs.u32be]
t_oe: Annotated[int, bs.u16be]
fit_interval_flag: Annotated[bool, bs.bits(1)]
aoda: Annotated[int, bs.bits(5)]
reserved: Annotated[int, bs.bits(2)]
class Subframe3(bs.BinaryStruct):
c_ic: Annotated[int, bs.s16be]
omega_0: Annotated[int, bs.s32be]
c_is: Annotated[int, bs.s16be]
i_0: Annotated[int, bs.s32be]
c_rc: Annotated[int, bs.s16be]
omega: Annotated[int, bs.s32be]
omega_dot_sign: Annotated[bool, bs.bits(1)]
omega_dot_value: Annotated[int, bs.bits(23)]
iode: Annotated[int, bs.u8]
idot_sign: Annotated[bool, bs.bits(1)]
idot_value: Annotated[int, bs.bits(13)]
reserved: Annotated[int, bs.bits(2)]
@property
def omega_dot(self) -> int:
"""Computed omega_dot from sign-magnitude representation."""
return (self.omega_dot_value - (1 << 23)) if self.omega_dot_sign else self.omega_dot_value
@property
def idot(self) -> int:
"""Computed idot from sign-magnitude representation."""
return (self.idot_value - (1 << 13)) if self.idot_sign else self.idot_value
class Subframe4(bs.BinaryStruct):
class IonosphereData(bs.BinaryStruct):
a0: Annotated[int, bs.s8]
a1: Annotated[int, bs.s8]
a2: Annotated[int, bs.s8]
a3: Annotated[int, bs.s8]
b0: Annotated[int, bs.s8]
b1: Annotated[int, bs.s8]
b2: Annotated[int, bs.s8]
b3: Annotated[int, bs.s8]
data_id: Annotated[int, bs.bits(2)]
page_id: Annotated[int, bs.bits(6)]
body: Annotated[object, bs.switch('page_id', {56: IonosphereData})]
tlm: Tlm
how: How
body: Annotated[
object,
bs.switch(
'how.subframe_id',
{
1: Subframe1,
2: Subframe2,
3: Subframe3,
4: Subframe4,
},
),
]

328
iqpilot/system/ubloxd/pigeond.py Executable file
View File

@@ -0,0 +1,328 @@
#!/usr/bin/env python3
import os
import sys
import time
import signal
import serial
import struct
import requests
import urllib.parse
from datetime import datetime, UTC
from iqpilot.cereal import messaging
from iqpilot.common.time_helpers import system_time_valid
from iqpilot.common.params import Params
from iqpilot.common.swaglog import cloudlog
from iqpilot.system.hardware import TICI
from iqpilot.common.gpio import gpio_init, gpio_set
from iqpilot.system.hardware.tici.pins import GPIO
UBLOX_TTY = "/dev/ttyHS0"
UBLOX_ACK = b"\xb5\x62\x05\x01\x02\x00"
UBLOX_NACK = b"\xb5\x62\x05\x00\x02\x00"
UBLOX_SOS_ACK = b"\xb5\x62\x09\x14\x08\x00\x02\x00\x00\x00\x01\x00\x00\x00"
UBLOX_SOS_NACK = b"\xb5\x62\x09\x14\x08\x00\x02\x00\x00\x00\x00\x00\x00\x00"
UBLOX_BACKUP_RESTORE_MSG = b"\xb5\x62\x09\x14\x08\x00\x03"
UBLOX_ASSIST_ACK = b"\xb5\x62\x13\x60\x08\x00"
# written by hephaestusd from konn3kt's /v1/{dongle_id}/assist proxy
ASSISTNOW_CACHE_PATH = "/data/agps/assistnow.ubx"
ASSISTNOW_CACHE_MAX_AGE = 2 * 60 * 60 # ephemeris expires after ~4h; stale data is worse than none
def set_power(enabled: bool) -> None:
gpio_init(GPIO.UBLOX_SAFEBOOT_N, True)
gpio_init(GPIO.GNSS_PWR_EN, True)
gpio_init(GPIO.UBLOX_RST_N, True)
gpio_set(GPIO.UBLOX_SAFEBOOT_N, True)
gpio_set(GPIO.GNSS_PWR_EN, enabled)
gpio_set(GPIO.UBLOX_RST_N, enabled)
def add_ubx_checksum(msg: bytes) -> bytes:
A = B = 0
for b in msg[2:]:
A = (A + b) % 256
B = (B + A) % 256
return msg + bytes([A, B])
def split_ubx_messages(dat: bytes) -> list[bytes]:
msgs = []
while len(dat) > 0:
assert dat[:2] == b"\xB5\x62"
msg_len = 6 + (dat[5] << 8 | dat[4]) + 2
msgs.append(dat[:msg_len])
dat = dat[msg_len:]
return msgs
def get_assistnow_messages(token: str) -> list[bytes]:
# make request
# TODO: implement adding the last known location
r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({
'token': token,
'gnss': 'gps,glo',
'datatype': 'eph,alm,aux',
}, safe=':,'), timeout=5)
assert r.status_code == 200, "Got invalid status code"
return split_ubx_messages(r.content)
def get_cached_assistnow_messages() -> list[bytes]:
try:
if time.time() - os.path.getmtime(ASSISTNOW_CACHE_PATH) > ASSISTNOW_CACHE_MAX_AGE:
return []
with open(ASSISTNOW_CACHE_PATH, "rb") as f:
dat = f.read()
except OSError:
return []
return split_ubx_messages(dat)
class TTYPigeon:
def __init__(self):
self.tty = serial.VTIMESerial(UBLOX_TTY, baudrate=9600, timeout=0)
def send(self, dat: bytes) -> None:
self.tty.write(dat)
def receive(self) -> bytes:
dat = b''
while len(dat) < 0x1000:
d = self.tty.read(0x40)
dat += d
if len(d) == 0:
break
return dat
def set_baud(self, baud: int) -> None:
self.tty.baudrate = baud
def wait_for_ack(self, ack: bytes = UBLOX_ACK, nack: bytes = UBLOX_NACK, timeout: float = 0.5) -> bool:
dat = b''
st = time.monotonic()
while True:
dat += self.receive()
if ack in dat:
cloudlog.debug("Received ACK from ublox")
return True
elif nack in dat:
cloudlog.error("Received NACK from ublox")
return False
elif time.monotonic() - st > timeout:
cloudlog.error("No response from ublox")
raise TimeoutError('No response from ublox')
time.sleep(0.001)
def send_with_ack(self, dat: bytes, ack: bytes = UBLOX_ACK, nack: bytes = UBLOX_NACK) -> None:
self.send(dat)
self.wait_for_ack(ack, nack)
def wait_for_backup_restore_status(self, timeout: float = 1.) -> int:
dat = b''
st = time.monotonic()
while True:
dat += self.receive()
position = dat.find(UBLOX_BACKUP_RESTORE_MSG)
if position >= 0 and len(dat) >= position + 11:
return dat[position + 10]
elif time.monotonic() - st > timeout:
cloudlog.error("No backup restore response from ublox")
raise TimeoutError('No response from ublox')
time.sleep(0.001)
def reset_device(self) -> bool:
# deleting the backup does not always work on first try (mostly on second try)
for _ in range(5):
# device cold start
self.send(b"\xb5\x62\x06\x04\x04\x00\xff\xff\x00\x00\x0c\x5d")
time.sleep(1) # wait for cold start
init_baudrate(self)
# clear configuration
self.send_with_ack(b"\xb5\x62\x06\x09\x0d\x00\x1f\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x71\xd7")
# clear flash memory (almanac backup)
self.send_with_ack(b"\xB5\x62\x09\x14\x04\x00\x01\x00\x00\x00\x22\xf0")
# try restoring backup to verify it got deleted
self.send(b"\xB5\x62\x09\x14\x00\x00\x1D\x60")
# 1: failed to restore, 2: could restore, 3: no backup
status = self.wait_for_backup_restore_status()
if status == 1 or status == 3:
return True
return False
def save_almanac(pigeon: TTYPigeon) -> None:
# store almanac in flash
pigeon.send(b"\xB5\x62\x09\x14\x04\x00\x00\x00\x00\x00\x21\xEC")
try:
if pigeon.wait_for_ack(ack=UBLOX_SOS_ACK, nack=UBLOX_SOS_NACK):
cloudlog.info("Done storing almanac")
else:
cloudlog.error("Error storing almanac")
except TimeoutError:
pass
def init_baudrate(pigeon: TTYPigeon):
# ublox default setting on startup is 9600 baudrate
pigeon.set_baud(9600)
# $PUBX,41,1,0007,0003,460800,0*15\r\n
pigeon.send(b"\x24\x50\x55\x42\x58\x2C\x34\x31\x2C\x31\x2C\x30\x30\x30\x37\x2C\x30\x30\x30\x33\x2C\x34\x36\x30\x38\x30\x30\x2C\x30\x2A\x31\x35\x0D\x0A")
time.sleep(0.1)
pigeon.set_baud(460800)
def init_pigeon(pigeon: TTYPigeon) -> bool:
# try initializing a few times
for _ in range(10):
try:
# setup port config
pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x03\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x1E\x7F")
pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x35")
pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x01\x00\x00\x00\xC0\x08\x00\x00\x00\x08\x07\x00\x01\x00\x01\x00\x00\x00\x00\x00\xF4\x80")
pigeon.send_with_ack(b"\xb5\x62\x06\x00\x14\x00\x04\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1D\x85")
pigeon.send_with_ack(b"\xb5\x62\x06\x00\x00\x00\x06\x18")
pigeon.send_with_ack(b"\xb5\x62\x06\x00\x01\x00\x01\x08\x22")
pigeon.send_with_ack(b"\xb5\x62\x06\x00\x01\x00\x03\x0A\x24")
# UBX-CFG-RATE (0x06 0x08)
pigeon.send_with_ack(b"\xB5\x62\x06\x08\x06\x00\x64\x00\x01\x00\x00\x00\x79\x10")
# UBX-CFG-NAV5 (0x06 0x24)
pigeon.send_with_ack(b"\xB5\x62\x06\x24\x24\x00\x05\x00\x04\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x5A\x63")
# UBX-CFG-ODO (0x06 0x1E)
pigeon.send_with_ack(b"\xB5\x62\x06\x1E\x14\x00\x00\x00\x00\x00\x01\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3C\x37")
pigeon.send_with_ack(b"\xB5\x62\x06\x39\x08\x00\xFF\xAD\x62\xAD\x1E\x63\x00\x00\x83\x0C")
pigeon.send_with_ack(b"\xB5\x62\x06\x23\x28\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x24")
# UBX-CFG-NAV5 (0x06 0x24)
pigeon.send_with_ack(b"\xB5\x62\x06\x24\x00\x00\x2A\x84")
pigeon.send_with_ack(b"\xB5\x62\x06\x23\x00\x00\x29\x81")
pigeon.send_with_ack(b"\xB5\x62\x06\x1E\x00\x00\x24\x72")
pigeon.send_with_ack(b"\xB5\x62\x06\x39\x00\x00\x3F\xC3")
# UBX-CFG-MSG (set message rate)
pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x01\x07\x01\x13\x51")
pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x02\x15\x01\x22\x70")
pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x02\x13\x01\x20\x6C")
pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x0A\x09\x01\x1E\x70")
pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x0A\x0B\x01\x20\x74")
pigeon.send_with_ack(b"\xB5\x62\x06\x01\x03\x00\x01\x35\x01\x41\xAD")
cloudlog.debug("pigeon configured")
# try restoring almanac backup
pigeon.send(b"\xB5\x62\x09\x14\x00\x00\x1D\x60")
restore_status = pigeon.wait_for_backup_restore_status()
if restore_status == 2:
cloudlog.warning("almanac backup restored")
elif restore_status == 3:
cloudlog.warning("no almanac backup found")
else:
cloudlog.error(f"failed to restore almanac backup, status: {restore_status}")
# sending time to ublox
if system_time_valid():
t_now = datetime.now(UTC).replace(tzinfo=None)
cloudlog.warning("Sending current time to ublox")
# UBX-MGA-INI-TIME_UTC
msg = add_ubx_checksum(b"\xB5\x62\x13\x40\x18\x00" + struct.pack("<BBBBHBBBBBxIHxxI",
0x10,
0x00,
0x00,
0x80,
t_now.year,
t_now.month,
t_now.day,
t_now.hour,
t_now.minute,
t_now.second,
0,
30,
0
))
pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK)
# a locally configured u-blox token takes precedence over the konn3kt-prefetched cache
try:
token = Params().get('AssistNowToken')
if token is not None:
msgs = get_assistnow_messages(token)
else:
msgs = get_cached_assistnow_messages()
if msgs:
for msg in msgs:
pigeon.send_with_ack(msg, ack=UBLOX_ASSIST_ACK)
cloudlog.warning("AssistNow messages sent")
except Exception:
cloudlog.warning("failed to get AssistNow messages")
cloudlog.warning("Pigeon GPS on!")
break
except TimeoutError:
cloudlog.warning("Initialization failed, trying again!")
else:
cloudlog.warning("Failed to initialize pigeon")
return False
return True
def deinitialize_and_exit(pigeon: TTYPigeon | None):
if pigeon is not None:
# controlled GNSS stop
pigeon.send(b"\xB5\x62\x06\x04\x04\x00\x00\x00\x08\x00\x16\x74")
# turn off power and exit cleanly
set_power(False)
sys.exit(0)
def init(pigeon: TTYPigeon) -> None:
# register exit handler
signal.signal(signal.SIGINT, lambda sig, frame: deinitialize_and_exit(pigeon))
# power cycle ublox
set_power(False)
time.sleep(0.1)
set_power(True)
time.sleep(0.5)
init_baudrate(pigeon)
init_pigeon(pigeon)
def run_receiving(duration: int = 0):
pm = messaging.PubMaster(['ubloxRaw'])
pigeon = TTYPigeon()
init(pigeon)
start_time = time.monotonic()
last_almanac_save = time.monotonic()
while (duration == 0) or (time.monotonic() - start_time < duration):
dat = pigeon.receive()
if len(dat) > 0:
if dat[0] == 0x00:
cloudlog.warning("received invalid data from ublox, re-initing!")
init(pigeon)
continue
# send out to socket
msg = messaging.new_message('ubloxRaw', len(dat), valid=True)
msg.ubloxRaw = dat[:]
pm.send('ubloxRaw', msg)
# save almanac every 5 minutes
if (time.monotonic() - last_almanac_save) > 60*5:
save_almanac(pigeon)
last_almanac_save = time.monotonic()
else:
# prevent locking up a CPU core if ublox disconnects
time.sleep(0.001)
def main():
assert TICI, "unsupported hardware for pigeond"
run_receiving()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
import pytest
import time
import iqpilot.cereal.messaging as messaging
from iqpilot.cereal.services import SERVICE_LIST
from iqpilot.common.gpio import gpio_read
from iqpilot.selfdrive.test.helpers import with_processes
from iqpilot.system.manager.process_config import managed_processes
from iqpilot.system.hardware.tici.pins import GPIO
# TODO: test TTFF when we have good A-GNSS
@pytest.mark.tici
class TestPigeond:
def teardown_method(self):
managed_processes['pigeond'].stop()
@with_processes(['pigeond'])
def test_frequency(self):
sm = messaging.SubMaster(['ubloxRaw'])
# setup time
for _ in range(int(5 * SERVICE_LIST['ubloxRaw'].frequency)):
sm.update()
for _ in range(int(10 * SERVICE_LIST['ubloxRaw'].frequency)):
sm.update()
assert sm.all_checks()
def test_startup_time(self):
for _ in range(5):
sm = messaging.SubMaster(['ubloxRaw'])
managed_processes['pigeond'].start()
start_time = time.monotonic()
for __ in range(10):
sm.update(1 * 1000)
if sm.updated['ubloxRaw']:
break
assert sm.recv_frame['ubloxRaw'] > 0, "pigeond didn't start outputting messages in time"
et = time.monotonic() - start_time
assert et < 5, f"pigeond took {et:.1f}s to start"
managed_processes['pigeond'].stop()
def test_turns_off_ublox(self):
for s in (0.1, 0.5, 1, 5):
managed_processes['pigeond'].start()
time.sleep(s)
managed_processes['pigeond'].stop()
assert gpio_read(GPIO.UBLOX_RST_N) == 0
assert gpio_read(GPIO.GNSS_PWR_EN) == 0

534
iqpilot/system/ubloxd/ubloxd.py Executable file
View File

@@ -0,0 +1,534 @@
#!/usr/bin/env python3
import math
import capnp
import calendar
import numpy as np
from collections import defaultdict
from dataclasses import dataclass
from iqpilot.cereal import log
from iqpilot.cereal import messaging
from iqpilot.system.ubloxd.ubx import Ubx
from iqpilot.system.ubloxd.gps import Gps
from iqpilot.system.ubloxd.glonass import Glonass
SECS_IN_MIN = 60
SECS_IN_HR = 60 * SECS_IN_MIN
SECS_IN_DAY = 24 * SECS_IN_HR
SECS_IN_WEEK = 7 * SECS_IN_DAY
class UbxFramer:
PREAMBLE1 = 0xB5
PREAMBLE2 = 0x62
HEADER_SIZE = 6
CHECKSUM_SIZE = 2
def __init__(self) -> None:
self.buf = bytearray()
self.last_log_time = 0.0
def reset(self) -> None:
self.buf.clear()
@staticmethod
def _checksum_ok(frame: bytes) -> bool:
ck_a = 0
ck_b = 0
for b in frame[2:-2]:
ck_a = (ck_a + b) & 0xFF
ck_b = (ck_b + ck_a) & 0xFF
return ck_a == frame[-2] and ck_b == frame[-1]
def add_data(self, log_time: float, incoming: bytes) -> list[bytes]:
self.last_log_time = log_time
out: list[bytes] = []
if not incoming:
return out
self.buf += incoming
while True:
# find preamble
if len(self.buf) < 2:
break
start = self.buf.find(b"\xb5\x62")
if start < 0:
# no preamble in buffer
self.buf.clear()
break
if start > 0:
# drop garbage before preamble
self.buf = self.buf[start:]
if len(self.buf) < self.HEADER_SIZE:
break
length_le = int.from_bytes(self.buf[4:6], 'little', signed=False)
total_len = self.HEADER_SIZE + length_le + self.CHECKSUM_SIZE
if len(self.buf) < total_len:
break
candidate = bytes(self.buf[:total_len])
if self._checksum_ok(candidate):
out.append(candidate)
# consume this frame
self.buf = self.buf[total_len:]
else:
# drop first byte and retry
self.buf = self.buf[1:]
return out
def _bit(b: int, shift: int) -> bool:
return (b & (1 << shift)) != 0
@dataclass
class EphemerisCaches:
gps_subframes: defaultdict[int, dict[int, bytes]]
glonass_strings: defaultdict[int, dict[int, bytes]]
glonass_string_times: defaultdict[int, dict[int, float]]
glonass_string_superframes: defaultdict[int, dict[int, int]]
class UbloxMsgParser:
gpsPi = 3.1415926535898
# user range accuracy in meters
glonass_URA_lookup: dict[int, float] = {
0: 1,
1: 2,
2: 2.5,
3: 4,
4: 5,
5: 7,
6: 10,
7: 12,
8: 14,
9: 16,
10: 32,
11: 64,
12: 128,
13: 256,
14: 512,
15: 1024,
}
def __init__(self) -> None:
self.framer = UbxFramer()
self.caches = EphemerisCaches(
gps_subframes=defaultdict(dict),
glonass_strings=defaultdict(dict),
glonass_string_times=defaultdict(dict),
glonass_string_superframes=defaultdict(dict),
)
# Message generation entry point
def parse_frame(self, frame: bytes) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None:
# Quick header parse
msg_type = int.from_bytes(frame[2:4], 'big')
payload = frame[6:-2]
if msg_type == 0x0107:
body = Ubx.NavPvt.from_bytes(payload)
return self._gen_nav_pvt(body)
if msg_type == 0x0213:
# Manually parse RXM-SFRBX to avoid EOF on some frames
if len(payload) < 8:
return None
gnss_id = payload[0]
sv_id = payload[1]
freq_id = payload[3]
num_words = payload[4]
exp = 8 + 4 * num_words
if exp != len(payload):
return None
words: list[int] = []
off = 8
for _ in range(num_words):
words.append(int.from_bytes(payload[off : off + 4], 'little'))
off += 4
class _SfrbxView:
def __init__(self, gid: int, sid: int, fid: int, body: list[int]):
self.gnss_id = Ubx.GnssType(gid)
self.sv_id = sid
self.freq_id = fid
self.body = body
view = _SfrbxView(gnss_id, sv_id, freq_id, words)
return self._gen_rxm_sfrbx(view)
if msg_type == 0x0215:
body = Ubx.RxmRawx.from_bytes(payload)
return self._gen_rxm_rawx(body)
if msg_type == 0x0A09:
body = Ubx.MonHw.from_bytes(payload)
return self._gen_mon_hw(body)
if msg_type == 0x0A0B:
body = Ubx.MonHw2.from_bytes(payload)
return self._gen_mon_hw2(body)
if msg_type == 0x0135:
body = Ubx.NavSat.from_bytes(payload)
return self._gen_nav_sat(body)
return None
# NAV-PVT -> gpsLocationExternal
def _gen_nav_pvt(self, msg: Ubx.NavPvt) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]:
dat = messaging.new_message('gpsLocationExternal', valid=True)
gps = dat.gpsLocationExternal
gps.source = log.GpsLocationData.SensorSource.ublox
gps.flags = msg.flags
gps.hasFix = (msg.flags % 2) == 1
gps.latitude = msg.lat * 1e-07
gps.longitude = msg.lon * 1e-07
gps.altitude = msg.height * 1e-03
gps.speed = msg.g_speed * 1e-03
gps.bearingDeg = msg.head_mot * 1e-5
gps.horizontalAccuracy = msg.h_acc * 1e-03
gps.satelliteCount = msg.num_sv
# build UTC timestamp millis (NAV-PVT is in UTC)
# tolerate invalid or unset date values like C++ timegm
try:
utc_tt = calendar.timegm((msg.year, msg.month, msg.day, msg.hour, msg.min, msg.sec, 0, 0, 0))
except Exception:
utc_tt = 0
gps.unixTimestampMillis = int(utc_tt * 1e3 + (msg.nano * 1e-6))
# match C++ float32 rounding semantics exactly
gps.vNED = [
float(np.float32(msg.vel_n) * np.float32(1e-03)),
float(np.float32(msg.vel_e) * np.float32(1e-03)),
float(np.float32(msg.vel_d) * np.float32(1e-03)),
]
gps.verticalAccuracy = msg.v_acc * 1e-03
gps.speedAccuracy = msg.s_acc * 1e-03
gps.bearingAccuracyDeg = msg.head_acc * 1e-05
return ('gpsLocationExternal', dat)
# RXM-SFRBX dispatch to GPS or GLONASS ephemeris
def _gen_rxm_sfrbx(self, msg) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None:
if msg.gnss_id == Ubx.GnssType.gps:
return self._parse_gps_ephemeris(msg)
if msg.gnss_id == Ubx.GnssType.glonass:
return self._parse_glonass_ephemeris(msg)
return None
def _parse_gps_ephemeris(self, msg: Ubx.RxmSfrbx) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None:
# body is list of 10 words; convert to 30-byte subframe (strip parity/padding)
body = msg.body
if len(body) != 10:
return None
subframe_data = bytearray()
for word in body:
word >>= 6
subframe_data.append((word >> 16) & 0xFF)
subframe_data.append((word >> 8) & 0xFF)
subframe_data.append(word & 0xFF)
sf = Gps.from_bytes(bytes(subframe_data))
subframe_id = sf.how.subframe_id
if subframe_id < 1 or subframe_id > 3:
return None
self.caches.gps_subframes[msg.sv_id][subframe_id] = bytes(subframe_data)
if len(self.caches.gps_subframes[msg.sv_id]) != 3:
return None
dat = messaging.new_message('ubloxGnss', valid=True)
eph = dat.ubloxGnss.init('ephemeris')
eph.svId = msg.sv_id
iode_s2 = 0
iode_s3 = 0
iodc_lsb = 0
week = 0
# Subframe 1
sf1 = Gps.from_bytes(self.caches.gps_subframes[msg.sv_id][1])
s1 = sf1.body
assert isinstance(s1, Gps.Subframe1)
week = s1.week_no
week += 1024
if week < 1877:
week += 1024
eph.tgd = s1.t_gd * math.pow(2, -31)
eph.toc = s1.t_oc * math.pow(2, 4)
eph.af2 = s1.af_2 * math.pow(2, -55)
eph.af1 = s1.af_1 * math.pow(2, -43)
eph.af0 = s1.af_0 * math.pow(2, -31)
eph.svHealth = s1.sv_health
eph.towCount = sf1.how.tow_count
iodc_lsb = s1.iodc_lsb
# Subframe 2
sf2 = Gps.from_bytes(self.caches.gps_subframes[msg.sv_id][2])
s2 = sf2.body
assert isinstance(s2, Gps.Subframe2)
if s2.t_oe == 0 and sf2.how.tow_count * 6 >= (SECS_IN_WEEK - 2 * SECS_IN_HR):
week += 1
eph.crs = s2.c_rs * math.pow(2, -5)
eph.deltaN = s2.delta_n * math.pow(2, -43) * self.gpsPi
eph.m0 = s2.m_0 * math.pow(2, -31) * self.gpsPi
eph.cuc = s2.c_uc * math.pow(2, -29)
eph.ecc = s2.e * math.pow(2, -33)
eph.cus = s2.c_us * math.pow(2, -29)
eph.a = math.pow(s2.sqrt_a * math.pow(2, -19), 2.0)
eph.toe = s2.t_oe * math.pow(2, 4)
iode_s2 = s2.iode
# Subframe 3
sf3 = Gps.from_bytes(self.caches.gps_subframes[msg.sv_id][3])
s3 = sf3.body
assert isinstance(s3, Gps.Subframe3)
eph.cic = s3.c_ic * math.pow(2, -29)
eph.omega0 = s3.omega_0 * math.pow(2, -31) * self.gpsPi
eph.cis = s3.c_is * math.pow(2, -29)
eph.i0 = s3.i_0 * math.pow(2, -31) * self.gpsPi
eph.crc = s3.c_rc * math.pow(2, -5)
eph.omega = s3.omega * math.pow(2, -31) * self.gpsPi
eph.omegaDot = s3.omega_dot * math.pow(2, -43) * self.gpsPi
eph.iode = s3.iode
eph.iDot = s3.idot * math.pow(2, -43) * self.gpsPi
iode_s3 = s3.iode
eph.toeWeek = week
eph.tocWeek = week
# clear cache for this SV
self.caches.gps_subframes[msg.sv_id].clear()
if not (iodc_lsb == iode_s2 == iode_s3):
return None
return ('ubloxGnss', dat)
def _parse_glonass_ephemeris(self, msg: Ubx.RxmSfrbx) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None:
# words are 4 bytes each; Glonass parser expects 16 bytes (string)
body = msg.body
if len(body) != 4:
return None
string_bytes = bytearray()
for word in body:
for i in (3, 2, 1, 0):
string_bytes.append((word >> (8 * i)) & 0xFF)
gl = Glonass.from_bytes(bytes(string_bytes))
string_number = gl.string_number
if string_number < 1 or string_number > 5 or gl.idle_chip:
return None
# correlate by superframe and timing, similar to C++ logic
freq_id = msg.freq_id
superframe_unknown = False
needs_clear = False
for i in range(1, 6):
if i not in self.caches.glonass_strings[freq_id]:
continue
sf_prev = self.caches.glonass_string_superframes[freq_id].get(i, 0)
if sf_prev == 0 or gl.superframe_number == 0:
superframe_unknown = True
elif sf_prev != gl.superframe_number:
needs_clear = True
if superframe_unknown:
prev_time = self.caches.glonass_string_times[freq_id].get(i, 0.0)
if abs((prev_time - 2.0 * i) - (self.framer.last_log_time - 2.0 * string_number)) > 10:
needs_clear = True
if needs_clear:
self.caches.glonass_strings[freq_id].clear()
self.caches.glonass_string_superframes[freq_id].clear()
self.caches.glonass_string_times[freq_id].clear()
self.caches.glonass_strings[freq_id][string_number] = bytes(string_bytes)
self.caches.glonass_string_superframes[freq_id][string_number] = gl.superframe_number
self.caches.glonass_string_times[freq_id][string_number] = self.framer.last_log_time
if msg.sv_id == 255:
# unknown SV id
return None
if len(self.caches.glonass_strings[freq_id]) != 5:
return None
dat = messaging.new_message('ubloxGnss', valid=True)
eph = dat.ubloxGnss.init('glonassEphemeris')
eph.svId = msg.sv_id
eph.freqNum = msg.freq_id - 7
current_day = 0
tk = 0
# string 1
try:
s1 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][1]).data
except Exception:
return None
assert isinstance(s1, Glonass.String1)
eph.p1 = int(s1.p1)
tk = int(s1.t_k)
eph.tkDEPRECATED = tk
eph.xVel = float(s1.x_vel) * math.pow(2, -20)
eph.xAccel = float(s1.x_accel) * math.pow(2, -30)
eph.x = float(s1.x) * math.pow(2, -11)
# string 2
try:
s2 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][2]).data
except Exception:
return None
assert isinstance(s2, Glonass.String2)
eph.svHealth = int(s2.b_n >> 2)
eph.p2 = int(s2.p2)
eph.tb = int(s2.t_b)
eph.yVel = float(s2.y_vel) * math.pow(2, -20)
eph.yAccel = float(s2.y_accel) * math.pow(2, -30)
eph.y = float(s2.y) * math.pow(2, -11)
# string 3
try:
s3 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][3]).data
except Exception:
return None
assert isinstance(s3, Glonass.String3)
eph.p3 = int(s3.p3)
eph.gammaN = float(s3.gamma_n) * math.pow(2, -40)
eph.svHealth = int(eph.svHealth | (1 if s3.l_n else 0))
eph.zVel = float(s3.z_vel) * math.pow(2, -20)
eph.zAccel = float(s3.z_accel) * math.pow(2, -30)
eph.z = float(s3.z) * math.pow(2, -11)
# string 4
try:
s4 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][4]).data
except Exception:
return None
assert isinstance(s4, Glonass.String4)
current_day = int(s4.n_t)
eph.nt = current_day
eph.tauN = float(s4.tau_n) * math.pow(2, -30)
eph.deltaTauN = float(s4.delta_tau_n) * math.pow(2, -30)
eph.age = int(s4.e_n)
eph.p4 = int(s4.p4)
eph.svURA = float(self.glonass_URA_lookup.get(int(s4.f_t), 0.0))
# consistency check: SV slot number
# if it doesn't match, keep going but note mismatch (no logging here)
eph.svType = int(s4.m)
# string 5
try:
s5 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][5]).data
except Exception:
return None
assert isinstance(s5, Glonass.String5)
eph.n4 = int(s5.n_4)
tk_seconds = int(SECS_IN_HR * ((tk >> 7) & 0x1F) + SECS_IN_MIN * ((tk >> 1) & 0x3F) + (tk & 0x1) * 30)
eph.tkSeconds = tk_seconds
self.caches.glonass_strings[freq_id].clear()
return ('ubloxGnss', dat)
def _gen_rxm_rawx(self, msg: Ubx.RxmRawx) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]:
dat = messaging.new_message('ubloxGnss', valid=True)
mr = dat.ubloxGnss.init('measurementReport')
mr.rcvTow = msg.rcv_tow
mr.gpsWeek = msg.week
mr.leapSeconds = msg.leap_s
mb = mr.init('measurements', msg.num_meas)
for i, m in enumerate(msg.meas):
mb[i].svId = m.sv_id
mb[i].pseudorange = m.pr_mes
mb[i].carrierCycles = m.cp_mes
mb[i].doppler = m.do_mes
mb[i].gnssId = int(m.gnss_id.value)
mb[i].glonassFrequencyIndex = m.freq_id
mb[i].locktime = m.lock_time
mb[i].cno = m.cno
mb[i].pseudorangeStdev = 0.01 * (math.pow(2, (m.pr_stdev & 15)))
mb[i].carrierPhaseStdev = 0.004 * (m.cp_stdev & 15)
mb[i].dopplerStdev = 0.002 * (math.pow(2, (m.do_stdev & 15)))
ts = mb[i].init('trackingStatus')
trk = m.trk_stat
ts.pseudorangeValid = _bit(trk, 0)
ts.carrierPhaseValid = _bit(trk, 1)
ts.halfCycleValid = _bit(trk, 2)
ts.halfCycleSubtracted = _bit(trk, 3)
mr.numMeas = msg.num_meas
rs = mr.init('receiverStatus')
rs.leapSecValid = _bit(msg.rec_stat, 0)
rs.clkReset = _bit(msg.rec_stat, 2)
return ('ubloxGnss', dat)
def _gen_nav_sat(self, msg: Ubx.NavSat) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]:
dat = messaging.new_message('ubloxGnss', valid=True)
sr = dat.ubloxGnss.init('satReport')
sr.iTow = msg.itow
svs = sr.init('svs', msg.num_svs)
for i, s in enumerate(msg.svs):
svs[i].svId = s.sv_id
svs[i].gnssId = int(s.gnss_id.value)
svs[i].flagsBitfield = s.flags
svs[i].cno = s.cno
svs[i].elevationDeg = s.elev
svs[i].azimuthDeg = s.azim
svs[i].pseudorangeResidual = s.pr_res * 0.1
return ('ubloxGnss', dat)
def _gen_mon_hw(self, msg: Ubx.MonHw) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]:
dat = messaging.new_message('ubloxGnss', valid=True)
hw = dat.ubloxGnss.init('hwStatus')
hw.noisePerMS = msg.noise_per_ms
hw.flags = msg.flags
hw.agcCnt = msg.agc_cnt
hw.aStatus = int(msg.a_status.value)
hw.aPower = int(msg.a_power.value)
hw.jamInd = msg.jam_ind
return ('ubloxGnss', dat)
def _gen_mon_hw2(self, msg: Ubx.MonHw2) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]:
dat = messaging.new_message('ubloxGnss', valid=True)
hw = dat.ubloxGnss.init('hwStatus2')
hw.ofsI = msg.ofs_i
hw.magI = msg.mag_i
hw.ofsQ = msg.ofs_q
hw.magQ = msg.mag_q
# Map Ubx enum to cereal enum {undefined=0, rom=1, otp=2, configpins=3, flash=4}
cfg_map = {
Ubx.MonHw2.ConfigSource.rom: 1,
Ubx.MonHw2.ConfigSource.otp: 2,
Ubx.MonHw2.ConfigSource.config_pins: 3,
Ubx.MonHw2.ConfigSource.flash: 4,
}
hw.cfgSource = cfg_map.get(msg.cfg_source, 0)
hw.lowLevCfg = msg.low_lev_cfg
hw.postStatus = msg.post_status
return ('ubloxGnss', dat)
def main():
parser = UbloxMsgParser()
pm = messaging.PubMaster(['ubloxGnss', 'gpsLocationExternal'])
sock = messaging.sub_sock('ubloxRaw', timeout=100, conflate=False)
while True:
msg = messaging.recv_one(sock)
if msg is None:
continue
data = bytes(msg.ubloxRaw)
log_time = msg.logMonoTime * 1e-9
frames = parser.framer.add_data(log_time, data)
for frame in frames:
try:
res = parser.parse_frame(frame)
except Exception:
continue
if not res:
continue
service, dat = res
pm.send(service, dat)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,180 @@
"""
UBX protocol parser
"""
from enum import IntEnum
from typing import Annotated
from iqpilot.system.ubloxd import binary_struct as bs
class GnssType(IntEnum):
gps = 0
sbas = 1
galileo = 2
beidou = 3
imes = 4
qzss = 5
glonass = 6
class Ubx(bs.BinaryStruct):
GnssType = GnssType
class RxmRawx(bs.BinaryStruct):
class Measurement(bs.BinaryStruct):
pr_mes: Annotated[float, bs.f64]
cp_mes: Annotated[float, bs.f64]
do_mes: Annotated[float, bs.f32]
gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)]
sv_id: Annotated[int, bs.u8]
reserved2: Annotated[bytes, bs.bytes_field(1)]
freq_id: Annotated[int, bs.u8]
lock_time: Annotated[int, bs.u16]
cno: Annotated[int, bs.u8]
pr_stdev: Annotated[int, bs.u8]
cp_stdev: Annotated[int, bs.u8]
do_stdev: Annotated[int, bs.u8]
trk_stat: Annotated[int, bs.u8]
reserved3: Annotated[bytes, bs.bytes_field(1)]
rcv_tow: Annotated[float, bs.f64]
week: Annotated[int, bs.u16]
leap_s: Annotated[int, bs.s8]
num_meas: Annotated[int, bs.u8]
rec_stat: Annotated[int, bs.u8]
reserved1: Annotated[bytes, bs.bytes_field(3)]
meas: Annotated[list[Measurement], bs.array(Measurement, count_field='num_meas')]
class RxmSfrbx(bs.BinaryStruct):
gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)]
sv_id: Annotated[int, bs.u8]
reserved1: Annotated[bytes, bs.bytes_field(1)]
freq_id: Annotated[int, bs.u8]
num_words: Annotated[int, bs.u8]
reserved2: Annotated[bytes, bs.bytes_field(1)]
version: Annotated[int, bs.u8]
reserved3: Annotated[bytes, bs.bytes_field(1)]
body: Annotated[list[int], bs.array(bs.u32, count_field='num_words')]
class NavSat(bs.BinaryStruct):
class Nav(bs.BinaryStruct):
gnss_id: Annotated[GnssType | int, bs.enum(bs.u8, GnssType)]
sv_id: Annotated[int, bs.u8]
cno: Annotated[int, bs.u8]
elev: Annotated[int, bs.s8]
azim: Annotated[int, bs.s16]
pr_res: Annotated[int, bs.s16]
flags: Annotated[int, bs.u32]
itow: Annotated[int, bs.u32]
version: Annotated[int, bs.u8]
num_svs: Annotated[int, bs.u8]
reserved: Annotated[bytes, bs.bytes_field(2)]
svs: Annotated[list[Nav], bs.array(Nav, count_field='num_svs')]
class NavPvt(bs.BinaryStruct):
i_tow: Annotated[int, bs.u32]
year: Annotated[int, bs.u16]
month: Annotated[int, bs.u8]
day: Annotated[int, bs.u8]
hour: Annotated[int, bs.u8]
min: Annotated[int, bs.u8]
sec: Annotated[int, bs.u8]
valid: Annotated[int, bs.u8]
t_acc: Annotated[int, bs.u32]
nano: Annotated[int, bs.s32]
fix_type: Annotated[int, bs.u8]
flags: Annotated[int, bs.u8]
flags2: Annotated[int, bs.u8]
num_sv: Annotated[int, bs.u8]
lon: Annotated[int, bs.s32]
lat: Annotated[int, bs.s32]
height: Annotated[int, bs.s32]
h_msl: Annotated[int, bs.s32]
h_acc: Annotated[int, bs.u32]
v_acc: Annotated[int, bs.u32]
vel_n: Annotated[int, bs.s32]
vel_e: Annotated[int, bs.s32]
vel_d: Annotated[int, bs.s32]
g_speed: Annotated[int, bs.s32]
head_mot: Annotated[int, bs.s32]
s_acc: Annotated[int, bs.s32]
head_acc: Annotated[int, bs.u32]
p_dop: Annotated[int, bs.u16]
flags3: Annotated[int, bs.u8]
reserved1: Annotated[bytes, bs.bytes_field(5)]
head_veh: Annotated[int, bs.s32]
mag_dec: Annotated[int, bs.s16]
mag_acc: Annotated[int, bs.u16]
class MonHw2(bs.BinaryStruct):
class ConfigSource(IntEnum):
flash = 102
otp = 111
config_pins = 112
rom = 113
ofs_i: Annotated[int, bs.s8]
mag_i: Annotated[int, bs.u8]
ofs_q: Annotated[int, bs.s8]
mag_q: Annotated[int, bs.u8]
cfg_source: Annotated[ConfigSource | int, bs.enum(bs.u8, ConfigSource)]
reserved1: Annotated[bytes, bs.bytes_field(3)]
low_lev_cfg: Annotated[int, bs.u32]
reserved2: Annotated[bytes, bs.bytes_field(8)]
post_status: Annotated[int, bs.u32]
reserved3: Annotated[bytes, bs.bytes_field(4)]
class MonHw(bs.BinaryStruct):
class AntennaStatus(IntEnum):
init = 0
dontknow = 1
ok = 2
short = 3
open = 4
class AntennaPower(IntEnum):
false = 0
true = 1
dontknow = 2
pin_sel: Annotated[int, bs.u32]
pin_bank: Annotated[int, bs.u32]
pin_dir: Annotated[int, bs.u32]
pin_val: Annotated[int, bs.u32]
noise_per_ms: Annotated[int, bs.u16]
agc_cnt: Annotated[int, bs.u16]
a_status: Annotated[AntennaStatus | int, bs.enum(bs.u8, AntennaStatus)]
a_power: Annotated[AntennaPower | int, bs.enum(bs.u8, AntennaPower)]
flags: Annotated[int, bs.u8]
reserved1: Annotated[bytes, bs.bytes_field(1)]
used_mask: Annotated[int, bs.u32]
vp: Annotated[bytes, bs.bytes_field(17)]
jam_ind: Annotated[int, bs.u8]
reserved2: Annotated[bytes, bs.bytes_field(2)]
pin_irq: Annotated[int, bs.u32]
pull_h: Annotated[int, bs.u32]
pull_l: Annotated[int, bs.u32]
magic: Annotated[bytes, bs.const(bs.bytes_field(2), b"\xb5\x62")]
msg_type: Annotated[int, bs.u16be]
length: Annotated[int, bs.u16]
body: Annotated[
object,
bs.substream(
'length',
bs.switch(
'msg_type',
{
0x0107: NavPvt,
0x0213: RxmSfrbx,
0x0215: RxmRawx,
0x0A09: MonHw,
0x0A0B: MonHw2,
0x0135: NavSat,
},
),
),
]
checksum: Annotated[int, bs.u16]