174 lines
6.3 KiB
Python
174 lines
6.3 KiB
Python
"""Classic-CAN segmentation for canonical SETProtocol v2 frames."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
|
||
from .core import (
|
||
CRC_SIZE,
|
||
FRAME_MAX,
|
||
HEADER_SIZE,
|
||
SOF,
|
||
FrameFlag,
|
||
SetProtocolError,
|
||
)
|
||
|
||
CAN_ID_MASK = 0x1FFFFFFF
|
||
CAN_ID_PREFIX = 0x12
|
||
CAN_ID_PREFIX_MASK = 0x1F000000
|
||
PCI_FIRST = 0x10
|
||
PCI_CONSECUTIVE = 0x20
|
||
PCI_FLOW_CONTROL = 0x30
|
||
PCI_TYPE_MASK = 0xF0
|
||
PCI_VALUE_MASK = 0x0F
|
||
FIRST_DATA_SIZE = 5
|
||
CONSECUTIVE_DATA_SIZE = 7
|
||
REASSEMBLY_TIMEOUT_MS = 500
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class CanAddress:
|
||
destination: int
|
||
source: int
|
||
priority: int = 0
|
||
channel: int = 0
|
||
|
||
def __post_init__(self) -> None:
|
||
if not 0 <= self.destination <= 0xFF:
|
||
raise SetProtocolError("CAN destination вне диапазона u8")
|
||
if not 0 <= self.source <= 0xFF:
|
||
raise SetProtocolError("CAN source вне диапазона u8")
|
||
if self.priority not in (0, 1):
|
||
raise SetProtocolError("CAN priority должен быть 0 или 1")
|
||
if not 0 <= self.channel <= 0x7F:
|
||
raise SetProtocolError("CAN channel вне диапазона 0..127")
|
||
|
||
def pack(self) -> int:
|
||
return (
|
||
(CAN_ID_PREFIX << 24)
|
||
| (self.destination << 16)
|
||
| (self.source << 8)
|
||
| (self.priority << 7)
|
||
| self.channel
|
||
)
|
||
|
||
@classmethod
|
||
def unpack(cls, can_id: int) -> "CanAddress":
|
||
if not 0 <= can_id <= CAN_ID_MASK:
|
||
raise SetProtocolError("CAN ID вне 29-битного диапазона")
|
||
if can_id & CAN_ID_PREFIX_MASK != CAN_ID_PREFIX << 24:
|
||
raise SetProtocolError("CAN ID не принадлежит SETProtocol v2")
|
||
return cls(
|
||
destination=(can_id >> 16) & 0xFF,
|
||
source=(can_id >> 8) & 0xFF,
|
||
priority=(can_id >> 7) & 1,
|
||
channel=can_id & 0x7F,
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class CanFrame:
|
||
can_id: int
|
||
data: bytes
|
||
|
||
def __post_init__(self) -> None:
|
||
CanAddress.unpack(self.can_id)
|
||
if not 1 <= len(self.data) <= 8:
|
||
raise SetProtocolError("classic CAN data должен содержать 1..8 байт")
|
||
|
||
|
||
def segment(packet: bytes, address: CanAddress) -> list[CanFrame]:
|
||
"""Split one complete encoded SETP frame into classic-CAN frames."""
|
||
if not HEADER_SIZE + CRC_SIZE <= len(packet) <= FRAME_MAX:
|
||
raise SetProtocolError("неверная длина SETP-пакета для CAN")
|
||
can_id = address.pack()
|
||
result = [
|
||
CanFrame(
|
||
can_id,
|
||
bytes((PCI_FIRST,))
|
||
+ len(packet).to_bytes(2, "little")
|
||
+ packet[:FIRST_DATA_SIZE],
|
||
)
|
||
]
|
||
sequence = 1
|
||
for offset in range(FIRST_DATA_SIZE, len(packet), CONSECUTIVE_DATA_SIZE):
|
||
chunk = packet[offset : offset + CONSECUTIVE_DATA_SIZE]
|
||
result.append(CanFrame(can_id, bytes((PCI_CONSECUTIVE | sequence,)) + chunk))
|
||
sequence = (sequence + 1) & PCI_VALUE_MASK
|
||
return result
|
||
|
||
|
||
class CanReassembler:
|
||
"""Reassemble one SETP packet from one active source/channel."""
|
||
|
||
def __init__(self, timeout_ms: int = REASSEMBLY_TIMEOUT_MS) -> None:
|
||
if timeout_ms <= 0:
|
||
raise ValueError("timeout_ms должен быть положительным")
|
||
self.timeout_ms = timeout_ms
|
||
self.reset()
|
||
|
||
def reset(self) -> None:
|
||
self._buffer = bytearray()
|
||
self._expected_length = 0
|
||
self._can_id: int | None = None
|
||
self._deadline_ms = 0
|
||
self._next_sequence = 1
|
||
|
||
def feed(self, frame: CanFrame, now_ms: int) -> bytes | None:
|
||
if self._can_id is not None and now_ms >= self._deadline_ms:
|
||
self.reset()
|
||
raise SetProtocolError("таймаут сборки SETProtocol CAN")
|
||
pci = frame.data[0] & PCI_TYPE_MASK
|
||
if pci == PCI_FIRST:
|
||
if len(frame.data) != 8:
|
||
raise SetProtocolError("первый CAN-сегмент должен иметь DLC 8")
|
||
total = int.from_bytes(frame.data[1:3], "little")
|
||
if not HEADER_SIZE + CRC_SIZE <= total <= FRAME_MAX:
|
||
raise SetProtocolError("неверная полная длина SETP CAN")
|
||
self._buffer = bytearray(frame.data[3:])
|
||
self._expected_length = total
|
||
self._can_id = frame.can_id
|
||
self._deadline_ms = now_ms + self.timeout_ms
|
||
self._next_sequence = 1
|
||
return None
|
||
if pci == PCI_CONSECUTIVE:
|
||
sequence = frame.data[0] & PCI_VALUE_MASK
|
||
if (
|
||
self._can_id is None
|
||
or frame.can_id != self._can_id
|
||
or sequence != self._next_sequence
|
||
or len(frame.data) < 2
|
||
):
|
||
self.reset()
|
||
raise SetProtocolError("нарушена последовательность SETP CAN")
|
||
remaining = self._expected_length - len(self._buffer)
|
||
chunk = frame.data[1:]
|
||
if len(chunk) > remaining:
|
||
self.reset()
|
||
raise SetProtocolError("CAN-сегмент длиннее остатка SETP-пакета")
|
||
self._buffer.extend(chunk)
|
||
self._next_sequence = (self._next_sequence + 1) & PCI_VALUE_MASK
|
||
self._deadline_ms = now_ms + self.timeout_ms
|
||
if len(self._buffer) != self._expected_length:
|
||
return None
|
||
packet = bytes(self._buffer)
|
||
can_id = self._can_id
|
||
self.reset()
|
||
assert can_id is not None
|
||
address = CanAddress.unpack(can_id)
|
||
if packet[:2] != SOF:
|
||
raise SetProtocolError("SETP CAN packet не содержит SOF")
|
||
source = int.from_bytes(packet[6:8], "little")
|
||
destination = int.from_bytes(packet[8:10], "little")
|
||
priority = int(bool(packet[3] & int(FrameFlag.PRIORITY)))
|
||
if (
|
||
source != address.source
|
||
or destination != address.destination
|
||
or priority != address.priority
|
||
):
|
||
raise SetProtocolError("SETP header не совпадает с CAN ID")
|
||
return packet
|
||
if pci == PCI_FLOW_CONTROL:
|
||
return None
|
||
raise SetProtocolError("неизвестный тип SETP CAN-сегмента")
|