v0.1.5 add periodic messages

This commit is contained in:
controllerzz 2025-12-16 20:14:43 +03:00
parent e3ca999777
commit 8015b6b5f2
7 changed files with 234 additions and 11 deletions

View File

@ -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 Подписка по CAN ID
````python ````python

View File

@ -2,6 +2,7 @@ from .device import CarBusDevice
from .messages import CanMessage, MessageDirection from .messages import CanMessage, MessageDirection
from .exceptions import CarBusError, CommandError, SyncError from .exceptions import CarBusError, CommandError, SyncError
from .can_router import CanIdRouter, RoutedCarBusCanTransport from .can_router import CanIdRouter, RoutedCarBusCanTransport
from .periodic import PeriodicCanSender, PeriodicJob
__all__ = [ __all__ = [
"CarBusDevice", "CarBusDevice",
@ -12,4 +13,6 @@ __all__ = [
"SyncError", "SyncError",
"CanIdRouter", "CanIdRouter",
"RoutedCarBusCanTransport", "RoutedCarBusCanTransport",
"PeriodicCanSender",
"PeriodicJob",
] ]

139
carbus_async/periodic.py Normal file
View File

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

View File

@ -1,10 +1,7 @@
import asyncio import asyncio
from carbus_async import CarBusDevice, CanMessage from carbus_async import CarBusDevice, CanMessage
from isotp_async import open_isotp
from uds_async import UdsClient
import signal import signal
import logging
async def wait_forever() -> None: async def wait_forever() -> None:

View File

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

View File

@ -4,18 +4,18 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict 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 carbus_async.device import CarBusDevice
from isotp_async import open_isotp, IsoTpConnection from isotp_async import open_isotp, IsoTpConnection
from uds_async import UdsServer from uds_async import UdsServer
from uds_async.exceptions import UdsNegativeResponse, UdsError from uds_async.exceptions import UdsNegativeResponse, UdsError
# import logging import logging
#
# logging.basicConfig( logging.basicConfig(
# level=logging.DEBUG, level=logging.DEBUG,
# format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
# ) )
TESTER_ID = 0x740 TESTER_ID = 0x740
ECU_ID = 0x760 ECU_ID = 0x760

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "carbus-lib" name = "carbus-lib"
version = "0.1.4" version = "0.1.5"
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"