frompyudskit.transportimportCANTransporttr=CANTransport(channel="can0",bustype="socketcan",rx_id=0x7E8,tx_id=0x7E0)tr.send(bytes.fromhex("22 F1 90"))resp=tr.recv(2000)print(resp)tr.close()
classCANTransport(Transport):"""CAN transport using python-can. Requires python-can installed."""def__init__(self,channel:str,bustype:str,rx_id:int,tx_id:int,config:Optional[TransportConfig]=None)->None:super().__init__(config=config)try:importcan# type: ignoreexceptExceptionasexc:# pragma: no coverraiseRuntimeError("python-can is required for CANTransport")fromexcself._can=canself.bus=can.Bus(channel=channel,bustype=bustype)self.rx_id=rx_idself.tx_id=tx_idself._reassembler=IsoTpReassembler()self._segmenter=IsoTpSegmenter()defsend(self,request_bytes:bytes)->None:forframeinself._segmenter.segment(request_bytes):msg=self._can.Message(arbitration_id=self.tx_id,data=frame,is_extended_id=False)self.bus.send(msg)defrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:timeout=(timeout_msorself.config.timeout_ms)/1000.0msg=self.bus.recv(timeout=timeout)ifmsgisNone:returnNonepayload=self._reassembler.feed(bytes(msg.data))returnpayloaddefclose(self)->None:try:self.bus.shutdown()exceptException:pass
classDoIPTransport(Transport):"""DoIP transport using doipy. Requires doipy installed."""def__init__(self,ip:str,logical_address:int,tx_id:int,rx_id:int,config:Optional[TransportConfig]=None)->None:super().__init__(config=config)try:fromdoipyimportDoIPClient# type: ignoreexceptExceptionasexc:# pragma: no coverraiseRuntimeError("doipy is required for DoIPTransport")fromexcself.client=DoIPClient(ip,logical_address)self.tx_id=tx_idself.rx_id=rx_iddefsend(self,request_bytes:bytes)->None:self.client.send_diagnostic_message(self.tx_id,request_bytes)defrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:timeout=(timeout_msorself.config.timeout_ms)/1000.0msg=self.client.receive_diagnostic_message(timeout)ifmsgisNone:returnNonereturnmsg.payloaddefclose(self)->None:try:self.client.close()exceptException:pass
classIsoTpReassembler:"""Minimal ISO-TP reassembler for incoming frames."""def__init__(self)->None:self._buf=bytearray()self._expected_len:Optional[int]=Noneself._next_seq:int=1deffeed(self,frame:bytes)->Optional[bytes]:ifnotframe:returnNonepci=frame[0]frame_type=(pci&0xF0)>>4ifframe_type==0x0:# Single Framelength=pci&0x0Freturnbytes(frame[1:1+length])ifframe_type==0x1:# First Framelength=((pci&0x0F)<<8)|frame[1]self._expected_len=lengthself._buf=bytearray(frame[2:])self._next_seq=1returnself._maybe_complete()ifframe_type==0x2:# Consecutive Frameseq=pci&0x0Fifseq!=(self._next_seq&0x0F):raiseIsoTpError("sequence mismatch")self._next_seq+=1self._buf.extend(frame[1:])returnself._maybe_complete()ifframe_type==0x3:# Flow Control (ignored by reassembler)returnNoneraiseIsoTpError("unknown PCI")def_maybe_complete(self)->Optional[bytes]:ifself._expected_lenisNone:returnNoneiflen(self._buf)>=self._expected_len:payload=bytes(self._buf[:self._expected_len])self._reset()returnpayloadreturnNonedef_reset(self)->None:self._buf=bytearray()self._expected_len=Noneself._next_seq=1
classMockTransport(Transport):"""Mock transport for tests and offline simulations."""def__init__(self,responses:Optional[list[bytes]]=None,handler:Optional[Callable[[bytes],Optional[bytes]]]=None,config:Optional[TransportConfig]=None,)->None:super().__init__(config=config)self.sent:list[bytes]=[]self._queue:Deque[bytes]=deque(responsesor[])self._handler=handlerdefsend(self,request_bytes:bytes)->None:self.sent.append(request_bytes)ifself._handler:resp=self._handler(request_bytes)ifresp:self._queue.append(resp)defrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:ifself._queue:returnself._queue.popleft()returnNonedefclose(self)->None:self._queue.clear()
classTransport(ABC):"""Abstract transport interface for UDS over CAN/DoIP/etc."""def__init__(self,config:Optional[TransportConfig]=None)->None:self.config=configorTransportConfig()@abstractmethoddefsend(self,request_bytes:bytes)->None:"""Send raw UDS payload bytes."""...@abstractmethoddefrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:"""Receive raw UDS payload bytes, or None on timeout."""...@abstractmethoddefclose(self)->None:"""Close underlying transport resources."""...
classUDSTransportClient:"""End-to-end UDS request/response over a Transport."""def__init__(self,transport:Transport,timing:Optional[UDSTiming]=None)->None:self.transport=transportself.timing=timingorUDSTiming()defrequest(self,request_bytes:bytes,timeout_ms:Optional[int]=None)->Optional[bytes]:self.transport.send(request_bytes)deadline=time.time()+((timeout_msorself.timing.overall_timeout_ms)/1000.0)wait_ms=self.timing.p2_mswhileTrue:remaining=deadline-time.time()ifremaining<=0:returnNoneresp=self.transport.recv(int(min(wait_ms,remaining*1000)))ifrespisNone:continuemsg=UDSMessage.from_bytes(resp)ifmsg.is_negative_responseandmsg.nrc==0x78:wait_ms=self.timing.p2_star_mscontinuereturnrespdefrequest_message(self,request_bytes:bytes,timeout_ms:Optional[int]=None)->Optional[UDSMessage]:resp=self.request(request_bytes,timeout_ms=timeout_ms)ifrespisNone:returnNonereturnUDSMessage.from_bytes(resp)
classTransport(ABC):"""Abstract transport interface for UDS over CAN/DoIP/etc."""def__init__(self,config:Optional[TransportConfig]=None)->None:self.config=configorTransportConfig()@abstractmethoddefsend(self,request_bytes:bytes)->None:"""Send raw UDS payload bytes."""...@abstractmethoddefrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:"""Receive raw UDS payload bytes, or None on timeout."""...@abstractmethoddefclose(self)->None:"""Close underlying transport resources."""...
classCANTransport(Transport):"""CAN transport using python-can. Requires python-can installed."""def__init__(self,channel:str,bustype:str,rx_id:int,tx_id:int,config:Optional[TransportConfig]=None)->None:super().__init__(config=config)try:importcan# type: ignoreexceptExceptionasexc:# pragma: no coverraiseRuntimeError("python-can is required for CANTransport")fromexcself._can=canself.bus=can.Bus(channel=channel,bustype=bustype)self.rx_id=rx_idself.tx_id=tx_idself._reassembler=IsoTpReassembler()self._segmenter=IsoTpSegmenter()defsend(self,request_bytes:bytes)->None:forframeinself._segmenter.segment(request_bytes):msg=self._can.Message(arbitration_id=self.tx_id,data=frame,is_extended_id=False)self.bus.send(msg)defrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:timeout=(timeout_msorself.config.timeout_ms)/1000.0msg=self.bus.recv(timeout=timeout)ifmsgisNone:returnNonepayload=self._reassembler.feed(bytes(msg.data))returnpayloaddefclose(self)->None:try:self.bus.shutdown()exceptException:pass
classDoIPTransport(Transport):"""DoIP transport using doipy. Requires doipy installed."""def__init__(self,ip:str,logical_address:int,tx_id:int,rx_id:int,config:Optional[TransportConfig]=None)->None:super().__init__(config=config)try:fromdoipyimportDoIPClient# type: ignoreexceptExceptionasexc:# pragma: no coverraiseRuntimeError("doipy is required for DoIPTransport")fromexcself.client=DoIPClient(ip,logical_address)self.tx_id=tx_idself.rx_id=rx_iddefsend(self,request_bytes:bytes)->None:self.client.send_diagnostic_message(self.tx_id,request_bytes)defrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:timeout=(timeout_msorself.config.timeout_ms)/1000.0msg=self.client.receive_diagnostic_message(timeout)ifmsgisNone:returnNonereturnmsg.payloaddefclose(self)->None:try:self.client.close()exceptException:pass
classIsoTpReassembler:"""Minimal ISO-TP reassembler for incoming frames."""def__init__(self)->None:self._buf=bytearray()self._expected_len:Optional[int]=Noneself._next_seq:int=1deffeed(self,frame:bytes)->Optional[bytes]:ifnotframe:returnNonepci=frame[0]frame_type=(pci&0xF0)>>4ifframe_type==0x0:# Single Framelength=pci&0x0Freturnbytes(frame[1:1+length])ifframe_type==0x1:# First Framelength=((pci&0x0F)<<8)|frame[1]self._expected_len=lengthself._buf=bytearray(frame[2:])self._next_seq=1returnself._maybe_complete()ifframe_type==0x2:# Consecutive Frameseq=pci&0x0Fifseq!=(self._next_seq&0x0F):raiseIsoTpError("sequence mismatch")self._next_seq+=1self._buf.extend(frame[1:])returnself._maybe_complete()ifframe_type==0x3:# Flow Control (ignored by reassembler)returnNoneraiseIsoTpError("unknown PCI")def_maybe_complete(self)->Optional[bytes]:ifself._expected_lenisNone:returnNoneiflen(self._buf)>=self._expected_len:payload=bytes(self._buf[:self._expected_len])self._reset()returnpayloadreturnNonedef_reset(self)->None:self._buf=bytearray()self._expected_len=Noneself._next_seq=1
classUDSTransportClient:"""End-to-end UDS request/response over a Transport."""def__init__(self,transport:Transport,timing:Optional[UDSTiming]=None)->None:self.transport=transportself.timing=timingorUDSTiming()defrequest(self,request_bytes:bytes,timeout_ms:Optional[int]=None)->Optional[bytes]:self.transport.send(request_bytes)deadline=time.time()+((timeout_msorself.timing.overall_timeout_ms)/1000.0)wait_ms=self.timing.p2_mswhileTrue:remaining=deadline-time.time()ifremaining<=0:returnNoneresp=self.transport.recv(int(min(wait_ms,remaining*1000)))ifrespisNone:continuemsg=UDSMessage.from_bytes(resp)ifmsg.is_negative_responseandmsg.nrc==0x78:wait_ms=self.timing.p2_star_mscontinuereturnrespdefrequest_message(self,request_bytes:bytes,timeout_ms:Optional[int]=None)->Optional[UDSMessage]:resp=self.request(request_bytes,timeout_ms=timeout_ms)ifrespisNone:returnNonereturnUDSMessage.from_bytes(resp)
classMockTransport(Transport):"""Mock transport for tests and offline simulations."""def__init__(self,responses:Optional[list[bytes]]=None,handler:Optional[Callable[[bytes],Optional[bytes]]]=None,config:Optional[TransportConfig]=None,)->None:super().__init__(config=config)self.sent:list[bytes]=[]self._queue:Deque[bytes]=deque(responsesor[])self._handler=handlerdefsend(self,request_bytes:bytes)->None:self.sent.append(request_bytes)ifself._handler:resp=self._handler(request_bytes)ifresp:self._queue.append(resp)defrecv(self,timeout_ms:Optional[int]=None)->Optional[bytes]:ifself._queue:returnself._queue.popleft()returnNonedefclose(self)->None:self._queue.clear()