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
Возможность конфигруации скорости 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

View File

@ -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:

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
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
async def main(is_debug=False):
if is_debug:

View File

@ -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"