diff --git a/README.md b/README.md index 7ae3037..657af05 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,42 @@ await dev.set_terminator(channel=2, enabled=False) ```` +## Отправка периодичных сообщений: +С константными данными +````python +from carbus_async import PeriodicCanSender + +sender = PeriodicCanSender(dev) +sender.add( + "heartbeat", + channel=1, + can_id=0x123, + data=b"\x01\x02\x03\x04\x05\x06\x07\x08", + period_s=0.1, +) +```` + +С модификацией данных +````python +from carbus_async import PeriodicCanSender + +sender = PeriodicCanSender(dev) + +def mod(tick, data): + b = bytearray(data) + b[0] = tick & 0xFF + return bytes(b) + +sender.add( + "cnt", + channel=1, + can_id=0x100, + data=b"\x00" * 8, + period_s=0.5, + modify=mod) +```` + + ## Хуки подписка на сообщение / сообщение + данные по маске: Подписка по CAN ID ````python diff --git a/carbus_async/__init__.py b/carbus_async/__init__.py index b34659e..9d434fe 100644 --- a/carbus_async/__init__.py +++ b/carbus_async/__init__.py @@ -2,6 +2,7 @@ from .device import CarBusDevice from .messages import CanMessage, MessageDirection from .exceptions import CarBusError, CommandError, SyncError from .can_router import CanIdRouter, RoutedCarBusCanTransport +from .periodic import PeriodicCanSender, PeriodicJob __all__ = [ "CarBusDevice", @@ -12,4 +13,6 @@ __all__ = [ "SyncError", "CanIdRouter", "RoutedCarBusCanTransport", + "PeriodicCanSender", + "PeriodicJob", ] diff --git a/carbus_async/periodic.py b/carbus_async/periodic.py new file mode 100644 index 0000000..0bbb573 --- /dev/null +++ b/carbus_async/periodic.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import asyncio +import time +from dataclasses import dataclass +from typing import Awaitable, Callable, Optional, Union + +from .messages import CanMessage +from .device import CarBusDevice + +ModifyFn = Callable[[int, bytes], Union[bytes, Awaitable[bytes]]] + + +@dataclass +class PeriodicJob: + name: str + can_id: int + data: bytes + period_s: float + channel: int = 1 + extended: bool = False + fd: bool = False + brs: bool = False + rtr: bool = False + echo: bool = False + confirm: bool = False + modify: Optional[ModifyFn] = None + + _task: Optional[asyncio.Task] = None + _stop: asyncio.Event = asyncio.Event() + + def start(self, dev: CarBusDevice) -> None: + if self._task is not None and not self._task.done(): + return + self._stop = asyncio.Event() + self._task = asyncio.create_task(self._run(dev), name=f"PeriodicJob:{self.name}") + + def _done(t: asyncio.Task): + exc = t.exception() + if exc: + print(f"[PeriodicJob:{self.name}] crashed:", repr(exc)) + + self._task.add_done_callback(_done) + + async def stop(self) -> None: + self._stop.set() + if self._task is not None: + self._task.cancel() + with asyncio.CancelledError.__suppress_context__ if False else None: + pass + try: + await self._task + except asyncio.CancelledError: + pass + self._task = None + + async def _run(self, dev: CarBusDevice) -> None: + tick = 0 + next_t = time.perf_counter() + + while not self._stop.is_set(): + now = time.perf_counter() + if now < next_t: + await asyncio.sleep(next_t - now) + next_t += self.period_s + + out = self.data + if self.modify is not None: + res = self.modify(tick, out) + out = await res if asyncio.iscoroutine(res) else res + + msg = CanMessage( + can_id=self.can_id, + data=out, + ) + await dev.send_can( + msg, + channel=self.channel, + confirm=self.confirm, + echo=self.echo, + ) + tick += 1 + + +class PeriodicCanSender: + def __init__(self, dev: CarBusDevice): + self._dev = dev + self._jobs: dict[str, PeriodicJob] = {} + + def add( + self, + name: str, + *, + channel: int, + can_id: int, + data: bytes, + period_s: float, + modify: Optional[ModifyFn] = None, + extended: bool = False, + fd: bool = False, + brs: bool = False, + rtr: bool = False, + echo: bool = False, + confirm: bool = False, + autostart: bool = True, + ) -> PeriodicJob: + if name in self._jobs: + raise ValueError(f"Periodic job '{name}' already exists") + + job = PeriodicJob( + name=name, + can_id=can_id, + data=data, + period_s=period_s, + channel=channel, + extended=extended, + fd=fd, + brs=brs, + rtr=rtr, + echo=echo, + confirm=confirm, + modify=modify, + ) + self._jobs[name] = job + if autostart: + job.start(self._dev) + return job + + def get(self, name: str) -> PeriodicJob: + return self._jobs[name] + + async def remove(self, name: str) -> None: + job = self._jobs.pop(name, None) + if job is not None: + await job.stop() + + async def stop_all(self) -> None: + await asyncio.gather(*(job.stop() for job in self._jobs.values()), return_exceptions=True) + self._jobs.clear() diff --git a/example/can_message_hook.py b/example/can_message_hook.py index af455a8..138fd37 100644 --- a/example/can_message_hook.py +++ b/example/can_message_hook.py @@ -1,10 +1,7 @@ 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: diff --git a/example/can_periodic_message.py b/example/can_periodic_message.py new file mode 100644 index 0000000..3b0aa6b --- /dev/null +++ b/example/can_periodic_message.py @@ -0,0 +1,48 @@ +import asyncio + +from carbus_async import CarBusDevice, PeriodicCanSender + + +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) + + sender = PeriodicCanSender(dev) + + def mod(tick, data): + b = bytearray(data) + b[0] = tick & 0xFF + return bytes(b) + + sender.add( + "cnt", + channel=1, + can_id=0x100, + data=b"\x00" * 8, + period_s=0.5, + modify=mod) + + sender.add( + "heartbeat", + channel=1, + can_id=0x123, + data=b"\x01\x02\x03\x04\x05\x06\x07\x08", + period_s=0.1, + ) + + try: + await asyncio.Event().wait() + finally: + await sender.stop_all() + await dev.close() + + return + + +asyncio.run(main()) diff --git a/example/car_ecus_emulated.py b/example/car_ecus_emulated.py index 8bea28a..ef40434 100644 --- a/example/car_ecus_emulated.py +++ b/example/car_ecus_emulated.py @@ -4,18 +4,18 @@ from dataclasses import dataclass from pathlib import Path from typing import Any, Dict -from carbus_async import CanIdRouter, RoutedCarBusCanTransport +from carbus_async import CanIdRouter, RoutedCarBusCanTransport, PeriodicCanSender 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", -# ) +import logging + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", +) TESTER_ID = 0x740 ECU_ID = 0x760 diff --git a/pyproject.toml b/pyproject.toml index 0df09e8..eac3a0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "carbus-lib" -version = "0.1.4" +version = "0.1.5" description = "Async CAN / ISO-TP / UDS library for Car Bus Analyzer" readme = "README.md" requires-python = ">=3.10"