Files
templates/python/setprotocol/can.py

174 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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-сегмента")