add canmessages hook

This commit is contained in:
controllerzz 2025-12-16 19:35:05 +03:00
parent e016892428
commit e3ca999777
5 changed files with 171 additions and 7 deletions

View File

@ -83,6 +83,7 @@ asyncio.run(main())
```` ````
## Настройка канала через Bit Timing ## Настройка канала через Bit Timing
Возможность конфигруации скорости CAN канала через Bit Timing
````python ````python
# CANFD+BRS 500/2000 kbit/s # CANFD+BRS 500/2000 kbit/s
await dev.open_can_channel_custom( await dev.open_can_channel_custom(
@ -105,6 +106,7 @@ await dev.open_can_channel_custom(
```` ````
## Получение информации об устройстве: ## Получение информации об устройстве:
Получение инфмормации об устройстве и его фичах
````python ````python
info = await dev.get_device_info() info = await dev.get_device_info()
@ -121,6 +123,8 @@ print("Features:",
```` ````
## Пример настройки фильтров: ## Пример настройки фильтров:
11 bit фильтры имеют index от 0 до 27 включительно,
29 bit фитры имеют index от 28 до 35 включительно
````python ````python
# очистить все фильтры на канале 1 # очистить все фильтры на канале 1
await dev.clear_all_filters(1) await dev.clear_all_filters(1)
@ -135,12 +139,32 @@ await dev.set_std_id_filter(
```` ````
## Управление терминатором 120 Ω: ## Управление терминатором 120 Ω:
Включаем терминатор на канале 1 и выключаем терминатор на канале 2
````python ````python
await dev.set_terminator(channel=1, enabled=True) await dev.set_terminator(channel=1, enabled=True)
await dev.set_terminator(channel=2, enabled=False) 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 (isotp_async)
ISO-TP канал строится поверх CarBusDevice: ISO-TP канал строится поверх CarBusDevice:
````python ````python

View File

@ -5,7 +5,7 @@ import contextlib
import logging import logging
import struct import struct
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Optional, Tuple, List from typing import Dict, Optional, Tuple, List, Awaitable, Callable
import serial_asyncio import serial_asyncio
@ -400,6 +400,30 @@ class _PendingRequest:
command: int 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 @dataclass
class CarBusDevice: class CarBusDevice:
port: str port: str
@ -414,6 +438,8 @@ class CarBusDevice:
_seq_counter: int = field(init=False, default=0, repr=False) _seq_counter: int = field(init=False, default=0, repr=False)
_reader_task: Optional[asyncio.Task] = field(init=False, default=None, repr=False) _reader_task: Optional[asyncio.Task] = field(init=False, default=None, repr=False)
_closed: bool = field(init=False, default=False, 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) _log: logging.Logger = field(init=False, repr=False)
_wire_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._seq_counter = 0
self._reader_task = None self._reader_task = None
self._closed = False self._closed = False
self._can_hooks = []
self._can_hook_sem = asyncio.Semaphore(200)
async def close(self) -> None: async def close(self) -> None:
if self._closed: if self._closed:
@ -530,6 +559,69 @@ class CarBusDevice:
) )
self._pending.clear() 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: def _start_reader(self) -> None:
if self._reader_task is None or self._reader_task.done(): if self._reader_task is None or self._reader_task.done():
self._reader_task = asyncio.create_task( self._reader_task = asyncio.create_task(
@ -1196,6 +1288,8 @@ class CarBusDevice:
data=data, data=data,
) )
self._fire_can_hooks(channel, msg)
await self._rx_queue.put((channel, msg)) await self._rx_queue.put((channel, msg))
if channel != 0: if channel != 0:

View File

@ -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())

View File

@ -6,11 +6,6 @@ from uds_async import UdsClient
import logging import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
async def main(is_debug=False): async def main(is_debug=False):
if is_debug: if is_debug:

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "carbus-lib" name = "carbus-lib"
version = "0.1.3" version = "0.1.4"
description = "Async CAN / ISO-TP / UDS library for Car Bus Analyzer" description = "Async CAN / ISO-TP / UDS library for Car Bus Analyzer"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"