add CanIdRouter and examle
This commit is contained in:
parent
3032d674c8
commit
e016892428
|
|
@ -1,6 +1,7 @@
|
||||||
from .device import CarBusDevice
|
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
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CarBusDevice",
|
"CarBusDevice",
|
||||||
|
|
@ -9,4 +10,6 @@ __all__ = [
|
||||||
"CarBusError",
|
"CarBusError",
|
||||||
"CommandError",
|
"CommandError",
|
||||||
"SyncError",
|
"SyncError",
|
||||||
|
"CanIdRouter",
|
||||||
|
"RoutedCarBusCanTransport",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
|
from carbus_async import CanMessage
|
||||||
|
from isotp_async.iface import CanTransport
|
||||||
|
|
||||||
|
|
||||||
|
class CanIdRouter:
|
||||||
|
def __init__(self, dev, channel: int, queue_size: int = 256):
|
||||||
|
self._dev = dev
|
||||||
|
self._channel = channel
|
||||||
|
self._queues: Dict[int, asyncio.Queue] = {}
|
||||||
|
self._queue_size = queue_size
|
||||||
|
self._task: Optional[asyncio.Task] = None
|
||||||
|
self._stop = asyncio.Event()
|
||||||
|
|
||||||
|
def get_queue(self, can_id: int) -> asyncio.Queue:
|
||||||
|
q = self._queues.get(can_id)
|
||||||
|
if q is None:
|
||||||
|
q = asyncio.Queue(maxsize=self._queue_size)
|
||||||
|
self._queues[can_id] = q
|
||||||
|
return q
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
if self._task is None:
|
||||||
|
self._task = asyncio.create_task(self._run())
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
self._stop.set()
|
||||||
|
if self._task:
|
||||||
|
self._task.cancel()
|
||||||
|
with contextlib.suppress(asyncio.CancelledError):
|
||||||
|
await self._task
|
||||||
|
self._task = None
|
||||||
|
|
||||||
|
async def _run(self):
|
||||||
|
while not self._stop.is_set():
|
||||||
|
ch, msg = await self._dev.receive_can()
|
||||||
|
if ch != self._channel:
|
||||||
|
continue
|
||||||
|
|
||||||
|
q = self._queues.get(msg.can_id)
|
||||||
|
if q is None:
|
||||||
|
# никто не подписан на этот CAN-ID — просто игнор
|
||||||
|
continue
|
||||||
|
|
||||||
|
# если очередь забита — можно дропать самый старый или новый
|
||||||
|
if q.full():
|
||||||
|
_ = q.get_nowait()
|
||||||
|
q.put_nowait(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class RoutedCarBusCanTransport(CanTransport):
|
||||||
|
def __init__(self, dev, channel: int, rx_id: int, router: CanIdRouter) -> None:
|
||||||
|
self._dev = dev
|
||||||
|
self._channel = channel
|
||||||
|
self._rx_id = rx_id
|
||||||
|
self._router = router
|
||||||
|
self._queue = router.get_queue(rx_id)
|
||||||
|
|
||||||
|
async def send(self, msg: CanMessage) -> None:
|
||||||
|
await self._dev.send_can(msg, channel=self._channel, confirm=False, echo=False)
|
||||||
|
|
||||||
|
async def recv(self, timeout: Optional[float] = None) -> Optional[CanMessage]:
|
||||||
|
try:
|
||||||
|
if timeout is None:
|
||||||
|
return await self._queue.get()
|
||||||
|
return await asyncio.wait_for(self._queue.get(), timeout=timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return None
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
import asyncio
|
||||||
|
import pickle
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from carbus_async import CanIdRouter, RoutedCarBusCanTransport
|
||||||
|
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",
|
||||||
|
# )
|
||||||
|
|
||||||
|
TESTER_ID = 0x740
|
||||||
|
ECU_ID = 0x760
|
||||||
|
|
||||||
|
DEFAULT_PORT = "COM6"
|
||||||
|
DEFAULT_BAUD = 115200
|
||||||
|
DEFAULT_CAN_CH = 1
|
||||||
|
DEFAULT_CAN_BITRATE = 500_000
|
||||||
|
|
||||||
|
CHANNEL = 1
|
||||||
|
|
||||||
|
IN_FILE_ECU = Path("ecu_uds22_params.pkl")
|
||||||
|
IN_FILE_ABS = Path("uds22_params.pkl")
|
||||||
|
|
||||||
|
|
||||||
|
def save_dict_pickle(path: str | Path, data: dict[int, bytes]) -> None:
|
||||||
|
path = Path(path)
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with path.open("wb") as f:
|
||||||
|
pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
|
|
||||||
|
def load_dict_pickle(path: str | Path) -> dict[int, bytes]:
|
||||||
|
path = Path(path)
|
||||||
|
with path.open("rb") as f:
|
||||||
|
obj = pickle.load(f)
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
raise TypeError(f"Expected dict in pickle file, got {type(obj).__name__}")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_device(
|
||||||
|
port: str = DEFAULT_PORT,
|
||||||
|
baudrate: int = DEFAULT_BAUD,
|
||||||
|
can_channel: int = DEFAULT_CAN_CH,
|
||||||
|
nominal_bitrate: int = DEFAULT_CAN_BITRATE,
|
||||||
|
) -> CarBusDevice:
|
||||||
|
dev = await CarBusDevice.open(port, baudrate=baudrate)
|
||||||
|
|
||||||
|
await dev.open_can_channel(channel=can_channel, nominal_bitrate=nominal_bitrate)
|
||||||
|
await dev.set_terminator(channel=can_channel, enabled=True)
|
||||||
|
|
||||||
|
await dev.clear_all_filters(can_channel)
|
||||||
|
await dev.set_std_id_filter(channel=can_channel, index=0, can_id=0x700, mask=0x700)
|
||||||
|
|
||||||
|
return dev
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_uds(dev: CarBusDevice, can_channel: int = DEFAULT_CAN_CH) -> UdsServer:
|
||||||
|
ecu_isotp = await open_isotp(dev, channel=can_channel, tx_id=ECU_ID, rx_id=TESTER_ID)
|
||||||
|
ecu_uds = UdsServer(ecu_isotp)
|
||||||
|
return ecu_uds
|
||||||
|
|
||||||
|
|
||||||
|
NRC_INCORRECT_LENGTH = 0x13
|
||||||
|
NRC_REQUEST_OUT_OF_RANGE = 0x31
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UdsServiceState:
|
||||||
|
did_store: Dict[int, bytes]
|
||||||
|
|
||||||
|
|
||||||
|
def _require_len(req: bytes, n: int, sid: int) -> None:
|
||||||
|
if len(req) < n:
|
||||||
|
raise UdsNegativeResponse(sid, NRC_INCORRECT_LENGTH)
|
||||||
|
|
||||||
|
|
||||||
|
def _did_from_req(req: bytes, sid: int) -> int:
|
||||||
|
_require_len(req, 3, sid)
|
||||||
|
return (req[1] << 8) | req[2]
|
||||||
|
|
||||||
|
|
||||||
|
def _rdbi_positive(req: bytes, payload: bytes) -> bytes:
|
||||||
|
return bytes((0x62, req[1], req[2])) + payload
|
||||||
|
|
||||||
|
|
||||||
|
def _wdbi_positive(req: bytes) -> bytes:
|
||||||
|
return bytes((0x6E, req[1], req[2]))
|
||||||
|
|
||||||
|
|
||||||
|
async def services_init(uds, *, in_file: str | Path) -> UdsServiceState:
|
||||||
|
did_store: Dict[int, bytes] = load_dict_pickle(path=in_file)
|
||||||
|
state = UdsServiceState(did_store=did_store)
|
||||||
|
|
||||||
|
@uds.service(0x10)
|
||||||
|
async def handle_session_control(req: bytes) -> bytes:
|
||||||
|
print(f"UDS OpenSession {hex(req[1])}")
|
||||||
|
_require_len(req, 2, 0x10)
|
||||||
|
session = req[1]
|
||||||
|
return bytes((0x50, session, 0x00, 0x32, 0x01, 0xF4))
|
||||||
|
|
||||||
|
@uds.service(0x11)
|
||||||
|
async def handle_session_control(req: bytes) -> bytes:
|
||||||
|
print(f"UDS ECU Reset {hex(req[1])}")
|
||||||
|
return bytes((0x51, req[1]))
|
||||||
|
|
||||||
|
@uds.service(0x14)
|
||||||
|
async def handle_session_control(req: bytes) -> bytes:
|
||||||
|
return bytes((0x54, 0xFF, 0xFF, 0xFF, 0xFF))
|
||||||
|
|
||||||
|
@uds.service(0x19)
|
||||||
|
async def handle_session_control(req: bytes) -> bytes:
|
||||||
|
return bytes((0x59, 0x00))
|
||||||
|
|
||||||
|
@uds.service(0x22)
|
||||||
|
async def handle_rdbi(req: bytes) -> bytes:
|
||||||
|
did = _did_from_req(req, 0x22)
|
||||||
|
|
||||||
|
payload = state.did_store.get(did)
|
||||||
|
|
||||||
|
if payload is None:
|
||||||
|
raise UdsNegativeResponse(0x22, NRC_REQUEST_OUT_OF_RANGE)
|
||||||
|
|
||||||
|
print(f"UDS Read Param {hex(did)}: {payload.hex()}")
|
||||||
|
return _rdbi_positive(req, payload)
|
||||||
|
|
||||||
|
@uds.service(0x27)
|
||||||
|
async def handle_security_access(req: bytes) -> bytes:
|
||||||
|
_require_len(req, 2, 0x27)
|
||||||
|
sub = req[1]
|
||||||
|
|
||||||
|
is_seed_request = bool(sub & 0x01)
|
||||||
|
if is_seed_request:
|
||||||
|
return bytes((0x67, sub, 0x11, 0x22, 0x33, 0x44))
|
||||||
|
|
||||||
|
_require_len(req, 6, 0x27)
|
||||||
|
return bytes((0x67, sub))
|
||||||
|
|
||||||
|
@uds.service(0x2E)
|
||||||
|
async def handle_wdbi(req: bytes) -> bytes:
|
||||||
|
_require_len(req, 4, 0x2E)
|
||||||
|
did = (req[1] << 8) | req[2]
|
||||||
|
state.did_store[did] = bytes(req[3:])
|
||||||
|
|
||||||
|
print(f"UDS Write Param {hex(did)}: {bytes(req[3:]).hex()}")
|
||||||
|
|
||||||
|
return _wdbi_positive(req)
|
||||||
|
|
||||||
|
@uds.service(0x31)
|
||||||
|
async def handle_routine_control(req: bytes) -> bytes:
|
||||||
|
_require_len(req, 4, 0x31)
|
||||||
|
print(f"UDS Routine Control {hex(req[1])} {hex(req[2])} {hex(req[3])}")
|
||||||
|
return bytes((0x71, req[1], req[2], req[3]))
|
||||||
|
|
||||||
|
@uds.service(0x3E)
|
||||||
|
async def handle_tester_present(req: bytes) -> bytes:
|
||||||
|
sub = req[1] if len(req) > 1 else 0x00
|
||||||
|
return bytes((0x7E, sub))
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
dev: CarBusDevice | None = None
|
||||||
|
try:
|
||||||
|
dev = await setup_device()
|
||||||
|
|
||||||
|
router = CanIdRouter(dev, channel=CHANNEL)
|
||||||
|
await router.start()
|
||||||
|
|
||||||
|
# ECU #1
|
||||||
|
tr_engine = RoutedCarBusCanTransport(dev, CHANNEL, rx_id=0x7E0, router=router)
|
||||||
|
isotp_engine = IsoTpConnection(can=tr_engine, tx_id=0x7E8, rx_id=0x7E0)
|
||||||
|
uds_engine = UdsServer(isotp_engine)
|
||||||
|
await services_init(uds_engine, in_file=IN_FILE_ECU)
|
||||||
|
|
||||||
|
# ECU #2
|
||||||
|
tr_abs = RoutedCarBusCanTransport(dev, CHANNEL, rx_id=0x740, router=router)
|
||||||
|
isotp_abs = IsoTpConnection(can=tr_abs, tx_id=0x760, rx_id=0x740)
|
||||||
|
uds_abs = UdsServer(isotp_abs)
|
||||||
|
await services_init(uds_abs, in_file=IN_FILE_ABS)
|
||||||
|
|
||||||
|
print("UDS servers running")
|
||||||
|
|
||||||
|
await asyncio.gather(
|
||||||
|
uds_engine.serve_forever(),
|
||||||
|
uds_abs.serve_forever(),
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if dev is not None:
|
||||||
|
await dev.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
Binary file not shown.
|
|
@ -36,7 +36,6 @@ async def setup_device(cfg: CanScanConfig) -> CarBusDevice:
|
||||||
mask=0x700,
|
mask=0x700,
|
||||||
)
|
)
|
||||||
|
|
||||||
await dev.clear_receive_buffer()
|
|
||||||
return dev
|
return dev
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,16 @@ from uds_async import UdsClient
|
||||||
from uds_async.exceptions import UdsNegativeResponse, UdsError
|
from uds_async.exceptions import UdsNegativeResponse, UdsError
|
||||||
|
|
||||||
|
|
||||||
TESTER_ID = 0x740
|
TESTER_ID = 0x7E0
|
||||||
ECU_ID = 0x760
|
ECU_ID = 0x7E8
|
||||||
|
|
||||||
DEFAULT_PORT = "COM6"
|
DEFAULT_PORT = "COM6"
|
||||||
DEFAULT_BAUD = 115200
|
DEFAULT_BAUD = 115200
|
||||||
DEFAULT_CAN_CH = 1
|
DEFAULT_CAN_CH = 1
|
||||||
DEFAULT_CAN_BITRATE = 500_000
|
DEFAULT_CAN_BITRATE = 500_000
|
||||||
|
|
||||||
DID_RANGE = range(0x10000)
|
DID_RANGE = range(0x0000, 0x10000)
|
||||||
OUT_FILE = Path("uds22_params.pkl")
|
OUT_FILE = Path("ecu_uds22_params.pkl")
|
||||||
|
|
||||||
|
|
||||||
def save_dict_pickle(path: str | Path, data: dict[int, bytes]) -> None:
|
def save_dict_pickle(path: str | Path, data: dict[int, bytes]) -> None:
|
||||||
|
|
@ -47,11 +47,12 @@ async def setup_device(
|
||||||
dev = await CarBusDevice.open(port, baudrate=baudrate)
|
dev = await CarBusDevice.open(port, baudrate=baudrate)
|
||||||
|
|
||||||
await dev.open_can_channel(channel=can_channel, nominal_bitrate=nominal_bitrate)
|
await dev.open_can_channel(channel=can_channel, nominal_bitrate=nominal_bitrate)
|
||||||
await dev.set_terminator(channel=can_channel, enabled=True)
|
|
||||||
|
|
||||||
await dev.clear_all_filters(can_channel)
|
# await dev.clear_all_filters(can_channel)
|
||||||
await dev.set_std_id_filter(channel=can_channel, index=0, can_id=0x700, mask=0x700)
|
await dev.set_std_id_filter(channel=can_channel, index=0, can_id=0x700, mask=0x700)
|
||||||
|
|
||||||
|
await dev.set_terminator(channel=can_channel, enabled=True)
|
||||||
|
|
||||||
return dev
|
return dev
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
from .carbus_iface import CarBusCanTransport
|
from .carbus_iface import CarBusCanTransport
|
||||||
from .transport import IsoTpChannel
|
from .transport import IsoTpChannel, IsoTpConnection
|
||||||
from .api import open_isotp
|
from .api import open_isotp
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CarBusCanTransport",
|
"CarBusCanTransport",
|
||||||
"IsoTpChannel",
|
"IsoTpChannel",
|
||||||
|
"IsoTpConnection",
|
||||||
"open_isotp",
|
"open_isotp",
|
||||||
]
|
]
|
||||||
|
|
@ -3,7 +3,8 @@ from __future__ import annotations
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from isotp_async import IsoTpChannel, CarBusCanTransport
|
# from carbus_async import CanIdRouter, RoutedCarBusCanTransport
|
||||||
|
from isotp_async import IsoTpChannel, CarBusCanTransport, IsoTpConnection
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -13,9 +14,16 @@ class IsoTpEndpoint:
|
||||||
channel: int = 1
|
channel: int = 1
|
||||||
|
|
||||||
|
|
||||||
async def open_isotp(dev: Any, *, endpoint: IsoTpEndpoint | None = None,
|
async def open_isotp(
|
||||||
channel: int = 1, tx_id: int | None = None, rx_id: int | None = None,
|
dev: Any,
|
||||||
**channel_kwargs) -> IsoTpChannel:
|
*,
|
||||||
|
endpoint: IsoTpEndpoint | None = None,
|
||||||
|
channel: int = 1,
|
||||||
|
tx_id: int | None = None,
|
||||||
|
rx_id: int | None = None,
|
||||||
|
router: Any | None = None,
|
||||||
|
**channel_kwargs,
|
||||||
|
) -> IsoTpChannel:
|
||||||
|
|
||||||
if endpoint is not None:
|
if endpoint is not None:
|
||||||
channel = endpoint.channel
|
channel = endpoint.channel
|
||||||
|
|
@ -25,5 +33,30 @@ async def open_isotp(dev: Any, *, endpoint: IsoTpEndpoint | None = None,
|
||||||
if tx_id is None or rx_id is None:
|
if tx_id is None or rx_id is None:
|
||||||
raise ValueError("tx_id and rx_id are required (or pass endpoint=...)")
|
raise ValueError("tx_id and rx_id are required (or pass endpoint=...)")
|
||||||
|
|
||||||
|
# ЛЕНИВЫЕ ИМПОРТЫ, чтобы не было circular import:
|
||||||
|
if router is None:
|
||||||
|
from isotp_async.carbus_iface import CarBusCanTransport # <-- подстрой путь под твой проект
|
||||||
can_tr = CarBusCanTransport(dev, channel=channel, rx_id=rx_id)
|
can_tr = CarBusCanTransport(dev, channel=channel, rx_id=rx_id)
|
||||||
|
else:
|
||||||
|
from carbus_async.can_router import RoutedCarBusCanTransport # <-- подстрой путь
|
||||||
|
can_tr = RoutedCarBusCanTransport(dev, channel=channel, rx_id=rx_id, router=router)
|
||||||
|
|
||||||
return IsoTpChannel(can_tr, tx_id=tx_id, rx_id=rx_id, **channel_kwargs)
|
return IsoTpChannel(can_tr, tx_id=tx_id, rx_id=rx_id, **channel_kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IsoTpCanEndpoint:
|
||||||
|
rx_id: int # что слушаем (request -> ECU)
|
||||||
|
tx_id: int # куда отвечаем (ECU -> tester)
|
||||||
|
|
||||||
|
|
||||||
|
class IsoTpNetwork:
|
||||||
|
def __init__(self, base_send, router: CanIdRouter):
|
||||||
|
self._send = base_send
|
||||||
|
self._router = router
|
||||||
|
|
||||||
|
def endpoint(self, ep: IsoTpCanEndpoint) -> IsoTpConnection:
|
||||||
|
transport = RoutedCarBusCanTransport(
|
||||||
|
dev=..., channel=..., rx_id=ep.rx_id, router=self._router
|
||||||
|
)
|
||||||
|
return IsoTpConnection(transport=transport, tx_id=ep.tx_id)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class IsoTpChannel:
|
||||||
tx_id: int
|
tx_id: int
|
||||||
rx_id: int
|
rx_id: int
|
||||||
|
|
||||||
block_size: int = 8
|
block_size: int = 0
|
||||||
st_min_ms: int = 0
|
st_min_ms: int = 0
|
||||||
fc_timeout: float = 1.0
|
fc_timeout: float = 1.0
|
||||||
cf_timeout: float = 1.0
|
cf_timeout: float = 1.0
|
||||||
|
|
@ -214,10 +214,24 @@ class IsoTpChannel:
|
||||||
|
|
||||||
payload.extend(cf.data[1:])
|
payload.extend(cf.data[1:])
|
||||||
expected_sn = (expected_sn + 1) & 0x0F
|
expected_sn = (expected_sn + 1) & 0x0F
|
||||||
if expected_sn == 0:
|
# надо подумать, гдето нужно так((((
|
||||||
expected_sn = 1
|
# if expected_sn == 0:
|
||||||
|
# expected_sn = 1
|
||||||
|
|
||||||
if st_min > 0:
|
if st_min > 0:
|
||||||
await asyncio.sleep(st_min)
|
await asyncio.sleep(st_min)
|
||||||
|
|
||||||
return bytes(payload[:total_length])
|
return bytes(payload[:total_length])
|
||||||
|
|
||||||
|
|
||||||
|
class IsoTpConnection(IsoTpChannel):
|
||||||
|
async def send(self, payload: bytes) -> None:
|
||||||
|
await self.send_pdu(payload)
|
||||||
|
|
||||||
|
async def recv(self, timeout: float = 1.0) -> Optional[bytes]:
|
||||||
|
return await self.recv_pdu(timeout=timeout)
|
||||||
|
|
||||||
|
async def request(self, payload: bytes, timeout: float = 1.0) -> Optional[bytes]:
|
||||||
|
|
||||||
|
await self.send_pdu(payload)
|
||||||
|
return await self.recv_pdu(timeout=timeout)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "carbus-lib"
|
name = "carbus-lib"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
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