diff --git a/README.md b/README.md index 357c7e0..7ae3037 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ asyncio.run(main()) ```` ## Настройка канала через Bit Timing +Возможность конфигруации скорости CAN канала через Bit Timing ````python # CANFD+BRS 500/2000 kbit/s await dev.open_can_channel_custom( @@ -105,6 +106,7 @@ await dev.open_can_channel_custom( ```` ## Получение информации об устройстве: +Получение инфмормации об устройстве и его фичах ````python info = await dev.get_device_info() @@ -121,6 +123,8 @@ print("Features:", ```` ## Пример настройки фильтров: +11 bit фильтры имеют index от 0 до 27 включительно, +29 bit фитры имеют index от 28 до 35 включительно ````python # очистить все фильтры на канале 1 await dev.clear_all_filters(1) @@ -135,12 +139,32 @@ await dev.set_std_id_filter( ```` ## Управление терминатором 120 Ω: +Включаем терминатор на канале 1 и выключаем терминатор на канале 2 ````python await dev.set_terminator(channel=1, enabled=True) await dev.set_terminator(channel=2, enabled=False) ```` +## Хуки подписка на сообщение / сообщение + данные по маске: +Подписка по CAN ID +````python +@dev.on_can_id(0x7E0) +async def on_engine_req(ch, msg): + print("ENGINE:", hex(msg.can_id), msg.data.hex()) +```` + +Подписка по CAN ID + маске данных +````python +@dev.on_can_match( + can_id=0x7E0, + value=b"\x02\x10\x00", + mask=b"\xFF\xFF\x00", +) +async def on_session_control(ch, msg): + print("SessionControl") +```` + ## ISO-TP (isotp_async) ISO-TP канал строится поверх CarBusDevice: ````python diff --git a/carbus_async/device.py b/carbus_async/device.py index 9b138c7..65097d8 100644 --- a/carbus_async/device.py +++ b/carbus_async/device.py @@ -5,7 +5,7 @@ import contextlib import logging import struct from dataclasses import dataclass, field -from typing import Dict, Optional, Tuple, List +from typing import Dict, Optional, Tuple, List, Awaitable, Callable import serial_asyncio @@ -400,6 +400,30 @@ class _PendingRequest: command: int +CanHook = Callable[[int, CanMessage], Awaitable[None]] +CanPred = Callable[[int, CanMessage], bool] + +@dataclass(frozen=True) +class _CanHookRule: + can_id: int | None # None => любой ID + value: bytes | None # None => матч только по ID/predicate + mask: bytes | None + offset: int + handler: CanHook + predicate: CanPred | None = None + + +def _match_masked(data: bytes, *, offset: int, value: bytes, mask: bytes) -> bool: + if len(value) != len(mask): + raise ValueError("mask and value must have same length") + end = offset + len(value) + if offset < 0 or len(data) < end: + return False + for i in range(len(value)): + if (data[offset + i] & mask[i]) != (value[i] & mask[i]): + return False + return True + @dataclass class CarBusDevice: port: str @@ -414,6 +438,8 @@ class CarBusDevice: _seq_counter: int = field(init=False, default=0, repr=False) _reader_task: Optional[asyncio.Task] = field(init=False, default=None, repr=False) _closed: bool = field(init=False, default=False, repr=False) + _can_hooks: List[_CanHookRule] = field(init=False, repr=False) + _can_hook_sem: asyncio.Semaphore = field(init=False, repr=False) _log: logging.Logger = field(init=False, repr=False) _wire_log: logging.Logger = field(init=False, repr=False) @@ -502,6 +528,9 @@ class CarBusDevice: self._seq_counter = 0 self._reader_task = None self._closed = False + self._can_hooks = [] + self._can_hook_sem = asyncio.Semaphore(200) + async def close(self) -> None: if self._closed: @@ -530,6 +559,69 @@ class CarBusDevice: ) self._pending.clear() + def on_can_id(self, can_id: int, *, predicate: CanPred | None = None): + """Хук на каждый принятый CAN кадр с данным can_id.""" + def deco(fn: CanHook) -> CanHook: + self._can_hooks.append(_CanHookRule( + can_id=can_id, + value=None, mask=None, offset=0, + handler=fn, + predicate=predicate, + )) + return fn + return deco + + def on_can_match( + self, + *, + can_id: int | None = None, + value: bytes, + mask: bytes | None = None, + offset: int = 0, + predicate: CanPred | None = None, + ): + """ + Хук по CAN-ID (или любой) + совпадение по маске. + Проверка: (data[offset+i] & mask[i]) == (value[i] & mask[i]) + """ + if mask is None: + mask = bytes([0xFF]) * len(value) + + def deco(fn: CanHook) -> CanHook: + self._can_hooks.append(_CanHookRule( + can_id=can_id, + value=value, + mask=mask, + offset=offset, + handler=fn, + predicate=predicate, + )) + return fn + return deco + + def _fire_can_hooks(self, channel: int, msg: CanMessage) -> None: + if not self._can_hooks: + return + + data = bytes(msg.data) + for rule in self._can_hooks: + if rule.can_id is not None and rule.can_id != msg.can_id: + continue + if rule.predicate is not None and not rule.predicate(channel, msg): + continue + if rule.value is not None: + if not _match_masked(data, offset=rule.offset, value=rule.value, mask=rule.mask or b""): + continue + + asyncio.create_task(self._run_can_hook(rule.handler, channel, msg)) + + async def _run_can_hook(self, fn: CanHook, channel: int, msg: CanMessage) -> None: + async with self._can_hook_sem: + try: + await fn(channel, msg) + except Exception: + self._log.exception("CAN hook failed (ch=%s id=0x%X)", channel, msg.can_id) + def _start_reader(self) -> None: if self._reader_task is None or self._reader_task.done(): self._reader_task = asyncio.create_task( @@ -1196,6 +1288,8 @@ class CarBusDevice: data=data, ) + self._fire_can_hooks(channel, msg) + await self._rx_queue.put((channel, msg)) if channel != 0: diff --git a/example/can_message_hook.py b/example/can_message_hook.py new file mode 100644 index 0000000..af455a8 --- /dev/null +++ b/example/can_message_hook.py @@ -0,0 +1,51 @@ +import asyncio + +from carbus_async import CarBusDevice, CanMessage +from isotp_async import open_isotp +from uds_async import UdsClient +import signal +import logging + + +async def wait_forever() -> None: + stop = asyncio.Event() + loop = asyncio.get_running_loop() + + for sig in (signal.SIGINT, signal.SIGTERM): + try: + loop.add_signal_handler(sig, stop.set) + except NotImplementedError: + pass + + try: + await stop.wait() + finally: + return + + +async def main(is_debug=False): + dev = await CarBusDevice.open("COM6") + + await dev.open_can_channel( + channel=1, + nominal_bitrate=500_000, + ) + + await dev.set_terminator(channel=1, enabled=True) + + @dev.on_can_id(0x7E0) + async def hook(ch: int, msg: CanMessage): + print("RX", ch, hex(msg.can_id), bytes(msg.data).hex()) + + @dev.on_can_match(can_id=0x7E0, value=b"\x02\x3E\x00") + async def tp(ch: int, msg: CanMessage): + print("TesterPresent!") + + print("Running. Press Ctrl+C to stop.") + try: + await wait_forever() + finally: + await dev.close() + + +asyncio.run(main()) diff --git a/main.py b/main.py index 24770cc..c4faa6e 100644 --- a/main.py +++ b/main.py @@ -6,11 +6,6 @@ from uds_async import UdsClient import logging -logging.basicConfig( - level=logging.DEBUG, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", -) - async def main(is_debug=False): if is_debug: diff --git a/pyproject.toml b/pyproject.toml index dda1b5e..0df09e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "carbus-lib" -version = "0.1.3" +version = "0.1.4" description = "Async CAN / ISO-TP / UDS library for Car Bus Analyzer" readme = "README.md" requires-python = ">=3.10"