This commit is contained in:
controllerzz
2025-12-11 09:04:25 +03:00
parent 0506c6393d
commit 957d825a3b
19 changed files with 2303 additions and 1 deletions

22
uds_async/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
from .client import UdsClient
from .server import UdsServer
try:
from .types import (
UdsRequest,
UdsResponse,
UdsPositiveResponse,
UdsNegativeResponse,
ResponseCode,
)
__all__ = [
"UdsClient",
"UdsServer",
]
except ImportError:
# если types.py нет или переименован — хотя бы клиент и сервер доступны
__all__ = [
"UdsClient",
"UdsServer",
]

63
uds_async/client.py Normal file
View File

@@ -0,0 +1,63 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from isotp_async.transport import IsoTpChannel
from .exceptions import UdsError, UdsNegativeResponse
@dataclass
class UdsClient:
isotp: IsoTpChannel
p2_timeout: float = 1.0
async def _request(self, payload: bytes) -> bytes:
await self.isotp.send_pdu(payload)
resp = await self.isotp.recv_pdu(timeout=self.p2_timeout)
if resp is None:
raise TimeoutError("UDS response timeout")
if resp[0] == 0x7F:
if len(resp) < 3:
raise UdsError("Malformed UDS negative response")
sid = resp[1]
nrc = resp[2]
raise UdsNegativeResponse(req_sid=sid, nrc=nrc)
return resp
async def diagnostic_session_control(self, session: int) -> bytes:
req = bytes([0x10, session & 0xFF])
resp = await self._request(req)
if resp[0] != 0x50:
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for DSC")
return resp
async def tester_present(self, suppress_response: bool = False) -> Optional[bytes]:
sub = 0x80 if suppress_response else 0x00
req = bytes([0x3E, sub])
if suppress_response:
try:
resp = await self._request(req)
except TimeoutError:
return None
return resp
resp = await self._request(req)
if resp[0] != 0x7E:
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for TesterPresent")
return resp
async def read_data_by_identifier(self, did: int) -> bytes:
req = bytes([0x22, (did >> 8) & 0xFF, did & 0xFF])
resp = await self._request(req)
if resp[0] != 0x62:
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for RDBI")
if len(resp) < 3:
raise UdsError("Malformed RDBI response")
return resp[3:]

16
uds_async/exceptions.py Normal file
View File

@@ -0,0 +1,16 @@
from __future__ import annotations
from dataclasses import dataclass
class UdsError(Exception):
...
@dataclass
class UdsNegativeResponse(UdsError):
req_sid: int
nrc: int
def __str__(self) -> str:
return f"UDS NRC 0x{self.nrc:02X} for SID 0x{self.req_sid:02X}"

62
uds_async/server.py Normal file
View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from typing import Awaitable, Callable, Dict, Optional
from isotp_async.transport import IsoTpChannel
from .exceptions import UdsNegativeResponse
UdsHandler = Callable[[bytes], Awaitable[Optional[bytes]]]
@dataclass
class UdsServer:
isotp: IsoTpChannel
p2_timeout: float = 1.0
handlers: Dict[int, UdsHandler] = field(default_factory=dict)
async def serve_forever(self) -> None:
while True:
try:
req = await self.isotp.recv_pdu(timeout=self.p2_timeout)
except asyncio.CancelledError:
break
if not req:
continue
sid = req[0]
handler = self.handlers.get(sid)
if handler is None:
# ServiceNotSupported
await self._send_negative_response(sid, 0x11)
continue
try:
resp = await handler(req)
if resp is None:
continue
await self.isotp.send_pdu(resp)
except UdsNegativeResponse as e:
await self._send_negative_response(e.req_sid, e.nrc)
except Exception:
# General programming failure (0x72)
await self._send_negative_response(sid, 0x72)
async def _send_negative_response(self, sid: int, nrc: int) -> None:
payload = bytes([0x7F, sid & 0xFF, nrc & 0xFF])
await self.isotp.send_pdu(payload)
def add_handler(self, sid: int, handler: UdsHandler) -> None:
self.handlers[sid & 0xFF] = handler
def service(self, sid: int):
def decorator(func: UdsHandler) -> UdsHandler:
self.add_handler(sid, func)
return func
return decorator