"""Message layer version 1 of the micrOBU station-internal link. Mirror of the firmware's ``link_protocol.hpp``; the two are checked against each other by ``tests/test_station_link.py``. All integers little-endian; ETSI payloads opaque. """ from __future__ import annotations import enum import struct from dataclasses import dataclass, field from typing import List, Optional MAXIMUM_MESSAGE = 512 HEADER_SIZE = 4 class Opcode(enum.IntEnum): STATION_CONFIGURE = 0x01 POTI_UPDATE = 0x02 BTP_DATA_REQUEST = 0x03 CREDENTIALS_PROVISION = 0x04 CREDENTIALS_ERASE = 0x05 SF_IDCHANGE_SUBSCRIBE = 0x06 SF_IDCHANGE_UNSUBSCRIBE = 0x07 SF_IDCHANGE_EVENT_RESPONSE = 0x08 SF_IDCHANGE_TRIGGER = 0x09 SF_ID_LOCK = 0x0A SF_ID_UNLOCK = 0x0B STATUS_REQUEST = 0x0C RESULT = 0x80 BTP_DATA_INDICATION = 0x81 SF_IDCHANGE_EVENT = 0x82 MF_SET_REQUEST = 0x83 STATUS = 0x84 class Code(enum.IntEnum): """RESULT codes: vanetza_idf::Result first, then link codes.""" accepted = 0 invalid_argument = 1 unsupported = 2 wrong_entry_point = 3 security_unavailable = 4 resource_limit = 5 rejected = 6 time_regression = 7 identity_change_pending = 8 unknown_opcode = 0x10 malformed = 0x11 not_configured = 0x12 busy = 0x13 no_credentials = 0x14 FLAG_FRAGMENT_FIRST = 0x01 FLAG_FRAGMENT_MORE = 0x02 class IdChangeCommand(enum.IntEnum): PREPARE = 0 COMMIT = 1 ABORT = 2 DEREG = 3 class TransportType(enum.IntEnum): SHB = 1 GBC = 2 TSB = 3 GAC = 4 GUC = 5 VERIFICATION_REPORTS = ['unsecured', 'Success', 'False_Signature', 'Invalid_Certificate', 'Revoked_Certificate', 'Inconsistent_Chain', 'Invalid_Timestamp', 'Duplicate_Message', 'Invalid_Mobility_Data', 'Unsigned_Message', 'Signer_Certificate_Not_Found', 'Unsupported_Signer_Identifier_Type', 'Incompatible_Protocol', 'Configuration_Problem'] class DecodeError(ValueError): pass class _Reader: def __init__(self, data: bytes, at: int = 0): self.data = data self.at = at def _take(self, n: int) -> bytes: if self.at + n > len(self.data): raise DecodeError('truncated body') chunk = self.data[self.at:self.at + n] self.at += n return chunk def u8(self) -> int: return self._take(1)[0] def u16(self) -> int: return struct.unpack(' int: return struct.unpack(' int: return struct.unpack(' int: return struct.unpack(' bytes: return self._take(n) def rest(self) -> bytes: chunk = self.data[self.at:] self.at = len(self.data) return chunk def done(self): if self.at != len(self.data): raise DecodeError('trailing octets') @dataclass class Header: opcode: int flags: int = 0 sequence: int = 0 @dataclass class Message: header: Header body: bytes = b'' def encode_message(message: Message) -> bytes: if HEADER_SIZE + len(message.body) > MAXIMUM_MESSAGE: raise ValueError('message exceeds %d octets' % MAXIMUM_MESSAGE) return struct.pack(' Message: if len(octets) < HEADER_SIZE or len(octets) > MAXIMUM_MESSAGE: raise DecodeError('bad message length %d' % len(octets)) opcode, flags, sequence = struct.unpack(' bytes: assert len(self.mid) == 6 return struct.pack(' 'StationConfigure': if len(body) != 16: raise DecodeError('StationConfigure length') f = struct.unpack(' 'StationInfo': if len(detail) != 18: raise DecodeError('StationInfo length') return cls(detail[:8], detail[8:16], detail[16] != 0, detail[17]) @dataclass class PotiUpdate: timestamp_ms: int latitude: int longitude: int semi_major_cm: int = 0 semi_minor_cm: int = 0 orientation_deci_degree: int = 0 altitude_cm: Optional[int] = None speed_cm_s: Optional[int] = None heading_deci_degree: Optional[int] = None pai: bool = False def encode(self) -> bytes: flags = ((1 if self.altitude_cm is not None else 0) | (2 if self.speed_cm_s is not None else 0) | (4 if self.heading_deci_degree is not None else 0) | (8 if self.pai else 0)) return struct.pack(' 'PotiUpdate': if len(body) != 31: raise DecodeError('PotiUpdate length') t, lat, lon, a, b, o, flags, alt, spd, hdg = struct.unpack(' bytes: return struct.pack(' 'DestinationArea': return cls(r.u8(), r.i32(), r.i32(), r.u16(), r.u16(), r.u16()) @dataclass class BtpDataRequest: fl_sdu: bytes btp_type: int = 1 destination_port: int = 2018 destination_port_info: int = 0 gn_packet_transport_type: int = TransportType.SHB gn_communication_profile: int = 1 gn_security_profile: int = 0 gn_traffic_class: int = 0xFF gn_maximum_packet_lifetime: int = 0xFF gn_maximum_hop_limit: int = 0 gn_repetition_interval_ms: int = 0 gn_repetition_maximum_ms: int = 0 its_aid: int = 638 permissions: bytes = b'' context: bytes = b'' area: Optional[DestinationArea] = None def encode(self) -> bytes: if len(self.permissions) > 31: raise ValueError('SSP longer than 31 octets') out = struct.pack(' 'BtpDataRequest': r = _Reader(body) f = struct.unpack(' str: return VERIFICATION_REPORTS[self.security_report] if self.security_report < len(VERIFICATION_REPORTS) else str(self.security_report) def encode(self) -> bytes: out = struct.pack(' 'BtpDataIndication': r = _Reader(body) f = struct.unpack(' bytes: return struct.pack(' 'CredentialsProvision': r = _Reader(body) total, offset = r.u16(), r.u16() segment = r.bytes(r.u8()) r.done() return cls(total, offset, segment) def provision_segments(bundle: bytes, segment_size: int = 200) -> List[CredentialsProvision]: """Split a VCR1 bundle into CREDENTIALS_PROVISION messages.""" return [CredentialsProvision(len(bundle), at, bundle[at:at + segment_size]) for at in range(0, len(bundle), segment_size)] @dataclass class IdChangeEvent: subscription: int command: int id: bytes subscriber_data: bytes = b'' def encode(self) -> bytes: return struct.pack(' 'IdChangeEvent': r = _Reader(body) s, c, i = struct.unpack(' bytes: return struct.pack(' 'IdChangeEventResponse': if len(body) != 9: raise DecodeError('IdChangeEventResponse length') s, c = struct.unpack(' bytes: out = struct.pack(' 'MfSetRequest': r = _Reader(body) fac, ref, count = struct.unpack(' bytes: return struct.pack(STATUS_FORMAT, *[getattr(self, name) for name in STATUS_FIELDS]) @classmethod def decode(cls, body: bytes) -> 'Status': if len(body) != struct.calcsize(STATUS_FORMAT): raise DecodeError('Status length %d' % len(body)) return cls(**dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, body)))) @dataclass class Result: code: int detail: bytes = b'' def encode(self) -> bytes: return bytes([int(self.code), len(self.detail)]) + self.detail @classmethod def decode(cls, body: bytes) -> 'Result': r = _Reader(body) code = r.u8() detail = r.bytes(r.u8()) r.done() return cls(code, detail) @property def name(self) -> str: try: return Code(self.code).name except ValueError: return 'code_0x%02x' % self.code