From e016892428342bc84de4adad9d5d72c2237022a2 Mon Sep 17 00:00:00 2001 From: controllerzz <79202101363@mail.ru> Date: Tue, 16 Dec 2025 12:34:37 +0300 Subject: [PATCH] add CanIdRouter and examle --- carbus_async/__init__.py | 3 + carbus_async/can_router.py | 71 +++++++++++ example/car_ecus_emulated.py | 205 ++++++++++++++++++++++++++++++++ example/ecu_uds22_params.pkl | Bin 0 -> 2081 bytes example/uds_id_scaner.py | 1 - example/uds_service22_scaner.py | 13 +- isotp_async/__init__.py | 3 +- isotp_async/api.py | 43 ++++++- isotp_async/transport.py | 20 +++- pyproject.toml | 2 +- 10 files changed, 344 insertions(+), 17 deletions(-) create mode 100644 carbus_async/can_router.py create mode 100644 example/car_ecus_emulated.py create mode 100644 example/ecu_uds22_params.pkl diff --git a/carbus_async/__init__.py b/carbus_async/__init__.py index a8ef2ad..b34659e 100644 --- a/carbus_async/__init__.py +++ b/carbus_async/__init__.py @@ -1,6 +1,7 @@ from .device import CarBusDevice from .messages import CanMessage, MessageDirection from .exceptions import CarBusError, CommandError, SyncError +from .can_router import CanIdRouter, RoutedCarBusCanTransport __all__ = [ "CarBusDevice", @@ -9,4 +10,6 @@ __all__ = [ "CarBusError", "CommandError", "SyncError", + "CanIdRouter", + "RoutedCarBusCanTransport", ] diff --git a/carbus_async/can_router.py b/carbus_async/can_router.py new file mode 100644 index 0000000..afbdb6d --- /dev/null +++ b/carbus_async/can_router.py @@ -0,0 +1,71 @@ +import asyncio +import contextlib +from typing import Dict, Optional, Tuple + +from carbus_async import CanMessage +from isotp_async.iface import CanTransport + + +class CanIdRouter: + def __init__(self, dev, channel: int, queue_size: int = 256): + self._dev = dev + self._channel = channel + self._queues: Dict[int, asyncio.Queue] = {} + self._queue_size = queue_size + self._task: Optional[asyncio.Task] = None + self._stop = asyncio.Event() + + def get_queue(self, can_id: int) -> asyncio.Queue: + q = self._queues.get(can_id) + if q is None: + q = asyncio.Queue(maxsize=self._queue_size) + self._queues[can_id] = q + return q + + async def start(self): + if self._task is None: + self._task = asyncio.create_task(self._run()) + + async def stop(self): + self._stop.set() + if self._task: + self._task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await self._task + self._task = None + + async def _run(self): + while not self._stop.is_set(): + ch, msg = await self._dev.receive_can() + if ch != self._channel: + continue + + q = self._queues.get(msg.can_id) + if q is None: + # никто не подписан на этот CAN-ID — просто игнор + continue + + # если очередь забита — можно дропать самый старый или новый + if q.full(): + _ = q.get_nowait() + q.put_nowait(msg) + + +class RoutedCarBusCanTransport(CanTransport): + def __init__(self, dev, channel: int, rx_id: int, router: CanIdRouter) -> None: + self._dev = dev + self._channel = channel + self._rx_id = rx_id + self._router = router + self._queue = router.get_queue(rx_id) + + async def send(self, msg: CanMessage) -> None: + await self._dev.send_can(msg, channel=self._channel, confirm=False, echo=False) + + async def recv(self, timeout: Optional[float] = None) -> Optional[CanMessage]: + try: + if timeout is None: + return await self._queue.get() + return await asyncio.wait_for(self._queue.get(), timeout=timeout) + except asyncio.TimeoutError: + return None diff --git a/example/car_ecus_emulated.py b/example/car_ecus_emulated.py new file mode 100644 index 0000000..8bea28a --- /dev/null +++ b/example/car_ecus_emulated.py @@ -0,0 +1,205 @@ +import asyncio +import pickle +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict + +from carbus_async import CanIdRouter, RoutedCarBusCanTransport +from carbus_async.device import CarBusDevice +from isotp_async import open_isotp, IsoTpConnection +from uds_async import UdsServer +from uds_async.exceptions import UdsNegativeResponse, UdsError + +# import logging +# +# logging.basicConfig( +# level=logging.DEBUG, +# format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +# ) + +TESTER_ID = 0x740 +ECU_ID = 0x760 + +DEFAULT_PORT = "COM6" +DEFAULT_BAUD = 115200 +DEFAULT_CAN_CH = 1 +DEFAULT_CAN_BITRATE = 500_000 + +CHANNEL = 1 + +IN_FILE_ECU = Path("ecu_uds22_params.pkl") +IN_FILE_ABS = Path("uds22_params.pkl") + + +def save_dict_pickle(path: str | Path, data: dict[int, bytes]) -> None: + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("wb") as f: + pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL) + + +def load_dict_pickle(path: str | Path) -> dict[int, bytes]: + path = Path(path) + with path.open("rb") as f: + obj = pickle.load(f) + if not isinstance(obj, dict): + raise TypeError(f"Expected dict in pickle file, got {type(obj).__name__}") + return obj + + +async def setup_device( + port: str = DEFAULT_PORT, + baudrate: int = DEFAULT_BAUD, + can_channel: int = DEFAULT_CAN_CH, + nominal_bitrate: int = DEFAULT_CAN_BITRATE, +) -> CarBusDevice: + dev = await CarBusDevice.open(port, baudrate=baudrate) + + await dev.open_can_channel(channel=can_channel, nominal_bitrate=nominal_bitrate) + await dev.set_terminator(channel=can_channel, enabled=True) + + await dev.clear_all_filters(can_channel) + await dev.set_std_id_filter(channel=can_channel, index=0, can_id=0x700, mask=0x700) + + return dev + + +async def setup_uds(dev: CarBusDevice, can_channel: int = DEFAULT_CAN_CH) -> UdsServer: + ecu_isotp = await open_isotp(dev, channel=can_channel, tx_id=ECU_ID, rx_id=TESTER_ID) + ecu_uds = UdsServer(ecu_isotp) + return ecu_uds + + +NRC_INCORRECT_LENGTH = 0x13 +NRC_REQUEST_OUT_OF_RANGE = 0x31 + + +@dataclass +class UdsServiceState: + did_store: Dict[int, bytes] + + +def _require_len(req: bytes, n: int, sid: int) -> None: + if len(req) < n: + raise UdsNegativeResponse(sid, NRC_INCORRECT_LENGTH) + + +def _did_from_req(req: bytes, sid: int) -> int: + _require_len(req, 3, sid) + return (req[1] << 8) | req[2] + + +def _rdbi_positive(req: bytes, payload: bytes) -> bytes: + return bytes((0x62, req[1], req[2])) + payload + + +def _wdbi_positive(req: bytes) -> bytes: + return bytes((0x6E, req[1], req[2])) + + +async def services_init(uds, *, in_file: str | Path) -> UdsServiceState: + did_store: Dict[int, bytes] = load_dict_pickle(path=in_file) + state = UdsServiceState(did_store=did_store) + + @uds.service(0x10) + async def handle_session_control(req: bytes) -> bytes: + print(f"UDS OpenSession {hex(req[1])}") + _require_len(req, 2, 0x10) + session = req[1] + return bytes((0x50, session, 0x00, 0x32, 0x01, 0xF4)) + + @uds.service(0x11) + async def handle_session_control(req: bytes) -> bytes: + print(f"UDS ECU Reset {hex(req[1])}") + return bytes((0x51, req[1])) + + @uds.service(0x14) + async def handle_session_control(req: bytes) -> bytes: + return bytes((0x54, 0xFF, 0xFF, 0xFF, 0xFF)) + + @uds.service(0x19) + async def handle_session_control(req: bytes) -> bytes: + return bytes((0x59, 0x00)) + + @uds.service(0x22) + async def handle_rdbi(req: bytes) -> bytes: + did = _did_from_req(req, 0x22) + + payload = state.did_store.get(did) + + if payload is None: + raise UdsNegativeResponse(0x22, NRC_REQUEST_OUT_OF_RANGE) + + print(f"UDS Read Param {hex(did)}: {payload.hex()}") + return _rdbi_positive(req, payload) + + @uds.service(0x27) + async def handle_security_access(req: bytes) -> bytes: + _require_len(req, 2, 0x27) + sub = req[1] + + is_seed_request = bool(sub & 0x01) + if is_seed_request: + return bytes((0x67, sub, 0x11, 0x22, 0x33, 0x44)) + + _require_len(req, 6, 0x27) + return bytes((0x67, sub)) + + @uds.service(0x2E) + async def handle_wdbi(req: bytes) -> bytes: + _require_len(req, 4, 0x2E) + did = (req[1] << 8) | req[2] + state.did_store[did] = bytes(req[3:]) + + print(f"UDS Write Param {hex(did)}: {bytes(req[3:]).hex()}") + + return _wdbi_positive(req) + + @uds.service(0x31) + async def handle_routine_control(req: bytes) -> bytes: + _require_len(req, 4, 0x31) + print(f"UDS Routine Control {hex(req[1])} {hex(req[2])} {hex(req[3])}") + return bytes((0x71, req[1], req[2], req[3])) + + @uds.service(0x3E) + async def handle_tester_present(req: bytes) -> bytes: + sub = req[1] if len(req) > 1 else 0x00 + return bytes((0x7E, sub)) + + return state + + +async def main() -> None: + dev: CarBusDevice | None = None + try: + dev = await setup_device() + + router = CanIdRouter(dev, channel=CHANNEL) + await router.start() + + # ECU #1 + tr_engine = RoutedCarBusCanTransport(dev, CHANNEL, rx_id=0x7E0, router=router) + isotp_engine = IsoTpConnection(can=tr_engine, tx_id=0x7E8, rx_id=0x7E0) + uds_engine = UdsServer(isotp_engine) + await services_init(uds_engine, in_file=IN_FILE_ECU) + + # ECU #2 + tr_abs = RoutedCarBusCanTransport(dev, CHANNEL, rx_id=0x740, router=router) + isotp_abs = IsoTpConnection(can=tr_abs, tx_id=0x760, rx_id=0x740) + uds_abs = UdsServer(isotp_abs) + await services_init(uds_abs, in_file=IN_FILE_ABS) + + print("UDS servers running") + + await asyncio.gather( + uds_engine.serve_forever(), + uds_abs.serve_forever(), + ) + + finally: + if dev is not None: + await dev.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/example/ecu_uds22_params.pkl b/example/ecu_uds22_params.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5cbb37bd5b4897a65f23a5d4f1b7d483bc512fa2 GIT binary patch literal 2081 zcmc(gOKclO7=UNJer285Nm@|L(+5;Q$nNfVH|teFiZekKnog*aRzm8*q5_dht<(dh z90*OCNAm*Gr z`xE|};os*HLV!-GldqhrNiXt3chn6kkuL*TDS5IaU0E4*ed&e1Uk$@hx+4=p8eRF`mu6p-Q1bS6Do!LOIsenhio z3n#mwaxb19eYq4oSHuhShpMnASI{4+3?~fwW94+R2K|Y0I>UkfRN1&X(4VOcnl8|v zs|@c2&^MLS*(jiYQAJGify;|r+%vKgM1Ixx!fL$^dPgOW-UnlCC`*7zY>Yo@Y(kQd zBBTi!LYB})a0od7h8kG5(k-P}EBpA}GQzL2N48-^OAm*fu+fb`oGxTu!qy z>?}Hc156j&K|3ihId1P}_ptM5>R({@a(y4WpX(2Bc@TYQ2TYAUME)7f;&6pC)p`>nw?>1*id)Rq)f!)jQWB0S?8p^W4%r{g|=QPmfHwz7QV7ID%Syp89R;Agn*uazX=IiG# zyjnbQLV;Oos4VS#&y^vV<%Y`Ou?VUI0hpD>zr7I5Dvr>3BY;`MZZfEPbv*t-y&~QE zsEb2EL*;ifpaV%+t@C7V=tW>etWqayV`H@Q;|H@y#QSfxvB&A*v7#(X?4RWRt$|YU z_%V}?RdVb*Ig%*NUUy( z^y*S;0m!aze#dXeSC%c3e{tn-yYYVuz&sr*fA91YBSKuv32}IvS}gnmqISQ4&F`O$ WEB0b#-2o%x@pC4kH4h;!mHz?g9{H{S literal 0 HcmV?d00001 diff --git a/example/uds_id_scaner.py b/example/uds_id_scaner.py index b1d9c86..d8e1339 100644 --- a/example/uds_id_scaner.py +++ b/example/uds_id_scaner.py @@ -36,7 +36,6 @@ async def setup_device(cfg: CanScanConfig) -> CarBusDevice: mask=0x700, ) - await dev.clear_receive_buffer() return dev diff --git a/example/uds_service22_scaner.py b/example/uds_service22_scaner.py index 1786b7e..730dfc7 100644 --- a/example/uds_service22_scaner.py +++ b/example/uds_service22_scaner.py @@ -10,16 +10,16 @@ from uds_async import UdsClient from uds_async.exceptions import UdsNegativeResponse, UdsError -TESTER_ID = 0x740 -ECU_ID = 0x760 +TESTER_ID = 0x7E0 +ECU_ID = 0x7E8 DEFAULT_PORT = "COM6" DEFAULT_BAUD = 115200 DEFAULT_CAN_CH = 1 DEFAULT_CAN_BITRATE = 500_000 -DID_RANGE = range(0x10000) -OUT_FILE = Path("uds22_params.pkl") +DID_RANGE = range(0x0000, 0x10000) +OUT_FILE = Path("ecu_uds22_params.pkl") def save_dict_pickle(path: str | Path, data: dict[int, bytes]) -> None: @@ -47,11 +47,12 @@ async def setup_device( dev = await CarBusDevice.open(port, baudrate=baudrate) await dev.open_can_channel(channel=can_channel, nominal_bitrate=nominal_bitrate) - await dev.set_terminator(channel=can_channel, enabled=True) - await dev.clear_all_filters(can_channel) + # await dev.clear_all_filters(can_channel) await dev.set_std_id_filter(channel=can_channel, index=0, can_id=0x700, mask=0x700) + await dev.set_terminator(channel=can_channel, enabled=True) + return dev diff --git a/isotp_async/__init__.py b/isotp_async/__init__.py index a510d5a..a614cd6 100644 --- a/isotp_async/__init__.py +++ b/isotp_async/__init__.py @@ -1,9 +1,10 @@ from .carbus_iface import CarBusCanTransport -from .transport import IsoTpChannel +from .transport import IsoTpChannel, IsoTpConnection from .api import open_isotp __all__ = [ "CarBusCanTransport", "IsoTpChannel", + "IsoTpConnection", "open_isotp", ] \ No newline at end of file diff --git a/isotp_async/api.py b/isotp_async/api.py index 4fdbf57..6974478 100644 --- a/isotp_async/api.py +++ b/isotp_async/api.py @@ -3,7 +3,8 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from isotp_async import IsoTpChannel, CarBusCanTransport +# from carbus_async import CanIdRouter, RoutedCarBusCanTransport +from isotp_async import IsoTpChannel, CarBusCanTransport, IsoTpConnection @dataclass(frozen=True) @@ -13,9 +14,16 @@ class IsoTpEndpoint: channel: int = 1 -async def open_isotp(dev: Any, *, endpoint: IsoTpEndpoint | None = None, - channel: int = 1, tx_id: int | None = None, rx_id: int | None = None, - **channel_kwargs) -> IsoTpChannel: +async def open_isotp( + dev: Any, + *, + endpoint: IsoTpEndpoint | None = None, + channel: int = 1, + tx_id: int | None = None, + rx_id: int | None = None, + router: Any | None = None, + **channel_kwargs, +) -> IsoTpChannel: if endpoint is not None: channel = endpoint.channel @@ -25,5 +33,30 @@ async def open_isotp(dev: Any, *, endpoint: IsoTpEndpoint | None = None, if tx_id is None or rx_id is None: raise ValueError("tx_id and rx_id are required (or pass endpoint=...)") - can_tr = CarBusCanTransport(dev, channel=channel, rx_id=rx_id) + # ЛЕНИВЫЕ ИМПОРТЫ, чтобы не было circular import: + if router is None: + from isotp_async.carbus_iface import CarBusCanTransport # <-- подстрой путь под твой проект + can_tr = CarBusCanTransport(dev, channel=channel, rx_id=rx_id) + else: + from carbus_async.can_router import RoutedCarBusCanTransport # <-- подстрой путь + can_tr = RoutedCarBusCanTransport(dev, channel=channel, rx_id=rx_id, router=router) + return IsoTpChannel(can_tr, tx_id=tx_id, rx_id=rx_id, **channel_kwargs) + + +@dataclass(frozen=True) +class IsoTpCanEndpoint: + rx_id: int # что слушаем (request -> ECU) + tx_id: int # куда отвечаем (ECU -> tester) + + +class IsoTpNetwork: + def __init__(self, base_send, router: CanIdRouter): + self._send = base_send + self._router = router + + def endpoint(self, ep: IsoTpCanEndpoint) -> IsoTpConnection: + transport = RoutedCarBusCanTransport( + dev=..., channel=..., rx_id=ep.rx_id, router=self._router + ) + return IsoTpConnection(transport=transport, tx_id=ep.tx_id) diff --git a/isotp_async/transport.py b/isotp_async/transport.py index efa681f..b0fea0b 100644 --- a/isotp_async/transport.py +++ b/isotp_async/transport.py @@ -24,7 +24,7 @@ class IsoTpChannel: tx_id: int rx_id: int - block_size: int = 8 + block_size: int = 0 st_min_ms: int = 0 fc_timeout: float = 1.0 cf_timeout: float = 1.0 @@ -214,10 +214,24 @@ class IsoTpChannel: payload.extend(cf.data[1:]) expected_sn = (expected_sn + 1) & 0x0F - if expected_sn == 0: - expected_sn = 1 + # надо подумать, гдето нужно так(((( + # if expected_sn == 0: + # expected_sn = 1 if st_min > 0: await asyncio.sleep(st_min) return bytes(payload[:total_length]) + + +class IsoTpConnection(IsoTpChannel): + async def send(self, payload: bytes) -> None: + await self.send_pdu(payload) + + async def recv(self, timeout: float = 1.0) -> Optional[bytes]: + return await self.recv_pdu(timeout=timeout) + + async def request(self, payload: bytes, timeout: float = 1.0) -> Optional[bytes]: + + await self.send_pdu(payload) + return await self.recv_pdu(timeout=timeout) diff --git a/pyproject.toml b/pyproject.toml index 3fcbc67..dda1b5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "carbus-lib" -version = "0.1.2" +version = "0.1.3" description = "Async CAN / ISO-TP / UDS library for Car Bus Analyzer" readme = "README.md" requires-python = ">=3.10"