add canmessages hook
This commit is contained in:
parent
e016892428
commit
e3ca999777
24
README.md
24
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
5
main.py
5
main.py
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue