Skip to main content

Async CAN / ISO-TP / UDS library for Car Bus Analyzer

Project description

carbus-lib

Асинхронная Python-библиотека для работы с CAN / CAN-FD адаптерами CAN-Hacker / Car Bus Analyzer: низкоуровневый CAN/LIN, ISO-TP, UDS и удалённый доступ к адаптеру через сеть.

  • 📡 carbus_async — работа с железом: CAN/CAN-FD, фильтры, терминатор 120 Ω, периодическая отправка, хуки
  • 📦 isotp_async — ISO-TP (ISO 15765-2): single- и multi-frame
  • 🩺 uds_async — UDS (ISO 14229): клиент и сервер (эмуляция ЭБУ)
  • 🌐 Remote / TCP-bridge — удалённый доступ к адаптеру через сеть, как к локальному

Python ≥ 3.10 · зависимости: pyserial, pyserial-asyncio · целиком на asyncio Протестировано на устройствах с Протоколом версии 22 Поддерживаемые устройства: https://canhacker.ru/shop/


Содержание


Возможности

  • CAN / CAN-FD: отправка и приём, BRS (bit-rate switching)
  • Настройка каналов по битрейту или по точным bit-timing
  • Фильтры по 11- и 29-битным ID, очистка фильтров
  • Управление встроенным терминатором 120 Ω
  • Периодическая отправка кадров (статичные данные или с модификацией на лету)
  • Хуки на приём кадров по ID и по маске данных
  • Маршрутизация принятых кадров по CAN-ID в отдельные очереди
  • ISO-TP (single + multi-frame)
  • UDS клиент и UDS сервер (эмуляция ЭБУ)
  • Удалённая работа через relay-сервер и TCP-мост
  • Подробное логирование всего протокольного трафика

Установка

Из PyPI:

python -m pip install carbus-lib

Либо editable-режим из репозитория (нужно для разработки):

git clone https://github.com/controllerzz/carbus_lib.git
cd carbus_lib
pip install -e .

Быстрый старт

Открыть устройство, настроить классический CAN-канал, отправить и принять кадр.

import asyncio
from carbus_async import CarBusDevice, CanMessage


async def main():
    dev = await CarBusDevice.open("COM6", baudrate=115200)

    # классический CAN, 500 kbit/s, канал 1
    await dev.open_can_channel(channel=1, nominal_bitrate=500_000)

    # внутренний терминатор 120 Ω (если поддерживается устройством)
    await dev.ensure_terminator(channel=1, enabled=True)

    # отправка кадра
    msg = CanMessage(can_id=0x7E0, data=b"\x02\x3E\x00\x00\x00\x00\x00\x00")
    await dev.send_can(msg, channel=1)

    # приём любого кадра -> (channel, CanMessage)
    ch, rx = await dev.receive_can()
    print("RX:", ch, hex(rx.can_id), rx.data.hex())

    await dev.close()


asyncio.run(main())

Классический CAN через open_can_channel поддерживает битрейты 10k / 20k / 33.3k / 50k / 62.5k / 83.3k / 95.2k / 100k / 125k / 250k / 400k / 500k / 800k / 1M.


CAN-FD и BRS

1. По стандартным битрейтам

await dev.open_can_channel(
    channel=1,
    nominal_bitrate=500_000,   # арбитражная фаза
    data_bitrate=2_000_000,    # фаза данных (используется при BRS)
    fd=True,
    brs=True,
)

Поддерживаемые битрейты (CAN-модуль 120 МГц, точка выборки ~80%):

  • nominal: 125k / 250k / 500k / 1M
  • data: 500k / 1M / 2M / 4M / 5M

На этих адаптерах FD-канал настраивается точными bit-timing, а не одиночным индексом скорости данных — иначе канал открывается, но FD/BRS-кадры не уходят в шину. open_can_channel делает это автоматически. Если тактовая модуля не 120 МГц, передайте её через can_clock_hz=....

2. По точным bit-timing

Для нестандартных скоростей задайте тайминги напрямую:

from carbus_async import CanTiming

await dev.open_can_channel_custom(
    channel=1,
    nominal_timing=CanTiming(prescaler=15, tq_seg1=12, tq_seg2=3, sjw=1),  # 500k @ 120 МГц
    data_timing=CanTiming(prescaler=6,  tq_seg1=7,  tq_seg2=2, sjw=1),     # 2M  @ 120 МГц
    fd=True,
    brs=True,
)

Отправка FD-кадра

Тип кадра задаётся флагами fd / brs в самом CanMessage:

# FD-кадр с переключением скорости (BRS), 16 байт данных
msg = CanMessage(can_id=0x100, data=bytes(range(16)), fd=True, brs=True)
await dev.send_can(msg, channel=1)

Допустимые длины данных CAN-FD: 0–8, 12, 16, 20, 24, 32, 48, 64 байт.


Приём и хуки

Способы приёма:

ch, msg = await dev.receive_can()                                  # с любого канала
msg = await dev.receive_can_on(channel=1)                          # только канал 1
msg = await dev.receive_can_on_timeout(channel=1, timeout=1.0)     # None при таймауте

Хуки (вызываются на каждый подходящий принятый кадр):

# по CAN-ID
@dev.on_can_id(0x7E0)
async def on_engine(ch, msg):
    print("ENGINE:", hex(msg.can_id), msg.data.hex())

# по CAN-ID + совпадению данных по маске
@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("DiagnosticSessionControl")

Периодическая отправка

from carbus_async import PeriodicCanSender

sender = PeriodicCanSender(dev)

# статичные данные, период 100 мс (классический CAN)
sender.add(
    "heartbeat",
    channel=1,
    can_id=0x123,
    data=b"\x01\x02\x03\x04\x05\x06\x07\x08",
    period_s=0.1,
)

# CAN-FD + BRS со счётчиком в первом байте
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,
    fd=True,
    brs=True,
    modify=mod,
)

# управление задачами
await sender.remove("cnt")   # остановить и удалить одну задачу
await sender.stop_all()      # остановить все

Параметры add(...): channel, can_id, data, period_s, modify (функция (tick, data) -> bytes, может быть async), extended, fd, brs, rtr, echo, confirm, autostart.


Фильтры

11-битные фильтры занимают индексы 0..27, 29-битные — 28..35.

# очистить все фильтры на канале 1
await dev.clear_all_filters(1)

# пропускать только ответы с ID 0x7E8 (11-бит)
await dev.set_std_id_filter(channel=1, index=0, can_id=0x7E8, mask=0x7FF)

# 29-битный фильтр
await dev.set_ext_id_filter(channel=1, index=28, can_id=0x18DAF110)

Терминатор

await dev.set_terminator(channel=1, enabled=True)
await dev.set_terminator(channel=2, enabled=False)

# проверка поддержки и включение
if await dev.has_terminator(channel=1):
    await dev.set_terminator(channel=1, enabled=True)

# включить, только если устройство это поддерживает
await dev.ensure_terminator(channel=1, enabled=True)

Информация об устройстве

info = await dev.get_device_info()

print("HW:", info.hardware_name)
print("FW:", info.firmware_version)
print("Serial:", info.serial_int)
print("Каналы:", info.channel_types)          # {1: 'CANFD', 2: 'CAN', ...}
print("Частоты:", info.channel_frequencies)   # {1: 120000000, ...} Гц

print("Фичи:",
      "gateway" if info.feature_gateway else "",
      "isotp" if info.feature_isotp else "",
      "txbuf" if info.feature_tx_buffer else "",
      "txtask" if info.feature_tx_task else "")

Маршрутизация по CAN-ID

CanIdRouter раскладывает принятые кадры по отдельным очередям на каждый CAN-ID — удобно, когда на одном канале несколько независимых потоков.

from carbus_async import CanIdRouter

router = CanIdRouter(dev, channel=1)
await router.start()

q = router.get_queue(0x7E8)      # очередь только для кадров с ID 0x7E8
msg = await q.get()
print(msg.data.hex())

await router.stop()

Роутер можно передать в open_isotp(..., router=router), чтобы держать несколько ISO-TP соединений на одном канале, не «перехватывая» кадры друг друга.


ISO-TP

ISO-TP-канал строится поверх CarBusDevice:

from isotp_async import open_isotp

isotp = await open_isotp(dev, channel=1, tx_id=0x7E0, rx_id=0x7E8)

# ReadDataByIdentifier F190 (VIN)
await isotp.send_pdu(b"\x22\xF1\x90")

# полный ответ (single или multi-frame)
resp = await isotp.recv_pdu(timeout=5.0)
print("ISO-TP:", resp.hex())

UDS клиент

from isotp_async import open_isotp
from uds_async import UdsClient

isotp = await open_isotp(dev, channel=1, tx_id=0x7E0, rx_id=0x7E8)
uds = UdsClient(isotp)

await uds.diagnostic_session_control(0x03)        # extended session
vin = await uds.read_data_by_identifier(0xF190)
print("VIN:", vin.decode(errors="ignore"))
await uds.tester_present()

Доступные сервисы: diagnostic_session_control, read_data_by_identifier, write_data_by_identifier, security_access_get_seed, security_access_send_key, tester_present.


UDS сервер

Эмуляция ЭБУ: на каждый сервис (SID) вешается обработчик, возвращающий полный ответный PDU. Отрицательный ответ — через UdsNegativeResponse.

from isotp_async import open_isotp
from uds_async import UdsServer
from uds_async.exceptions import UdsNegativeResponse

# слушаем запросы на 0x7E0, отвечаем на 0x7E8
isotp = await open_isotp(dev, channel=1, tx_id=0x7E8, rx_id=0x7E0)
server = UdsServer(isotp)

@server.service(0x22)   # ReadDataByIdentifier
async def on_rdbi(req: bytes) -> bytes:
    did = (req[1] << 8) | req[2]
    if did == 0xF190:
        return b"\x62\xF1\x90" + b"WVWZZZ1KZAW000001"
    raise UdsNegativeResponse(req_sid=0x22, nrc=0x31)   # requestOutOfRange

await server.serve_forever()

Удалённая работа

Через relay-сервер (любой IP)

Можно воспользоваться бесплатным сервером:

IP:   185.42.26.80
PORT: 9000

Сервер / Relay (машина с белым IP):

carbus-relay-server --host 0.0.0.0 --port 9000
# или
python -m carbus_async.remote.server --host 0.0.0.0 --port 9000

Агент (машина, к которой подключён адаптер):

carbus-relay-agent --port COM6 --baudrate 115200 --server <IP_СЕРВЕРА>:9000 --serial 5957 --password 1234
# или
python -m carbus_async.remote.agent --port COM6 --baudrate 115200 --server <IP_СЕРВЕРА>:9000 --serial 5957 --password 1234
параметр назначение
--server <IP>:9000 адрес и порт relay-сервера
--serial 5957 серийный номер устройства
--password 1234 пароль для удалённого доступа

Клиент (удалённая машина):

from carbus_async import open_remote_device

dev = await open_remote_device("185.42.26.80", 9000, serial=5957, password="1234")
# дальше — обычный API CarBusDevice

Через TCP-мост в локальной сети

Сервер (рядом с адаптером):

python -m carbus_async.tcp_bridge --serial COM6 --port 7000

Клиент (та же сеть) — тот же API, но через open_tcp:

import asyncio
from carbus_async import CarBusDevice, CanMessage


async def main():
    dev = await CarBusDevice.open_tcp("192.168.1.10", 7000)

    await dev.open_can_channel(channel=1, nominal_bitrate=500_000)

    msg = CanMessage(can_id=0x321, data=b"\x01\x02\x03\x04")
    await dev.send_can(msg, channel=1)

    ch, rx = await dev.receive_can()
    print("REMOTE RX:", ch, hex(rx.can_id), rx.data.hex())

    await dev.close()


asyncio.run(main())

Логирование

import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)

Логгеры:

  • carbus_async.wire.* — сырые кадры по USB/TCP (TX/RX)
  • carbus_async.device.* — высокоуровневые события, ошибки, BUS_ERROR
  • дополнительные логгеры в isotp_async / uds_async

Примеры

Готовые скрипты — в каталоге example/:

файл что показывает
can_periodic_message.py периодическая отправка (классический CAN)
canfd_periodic_message.py периодическая отправка CAN-FD / BRS
can_message_hook.py хуки на приём кадров
uds_service22_scaner.py скан UDS-сервиса 0x22 (RDBI)
uds_id_scaner.py перебор идентификаторов (DID)
uds_ecu_emulated.py эмуляция ЭБУ (UDS-сервер)
car_ecus_emulated.py эмуляция нескольких ЭБУ
remote_can_periodic_message.py периодика через relay-сервер
remote_can_bridge.py мост CAN ↔ удалённое устройство
remote_relay.py запуск relay-сервера

Лицензия

MIT — распространяется «как есть», без каких-либо гарантий. См. LICENSE.

Pull Requests и предложения по улучшению приветствуются 🚗

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

carbus_lib-0.1.9.tar.gz (37.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

carbus_lib-0.1.9-py3-none-any.whl (38.1 kB view details)

Uploaded Python 3

File details

Details for the file carbus_lib-0.1.9.tar.gz.

File metadata

  • Download URL: carbus_lib-0.1.9.tar.gz
  • Upload date:
  • Size: 37.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for carbus_lib-0.1.9.tar.gz
Algorithm Hash digest
SHA256 812674d5d57e5163624c80655802990daab12e417b7e72e5f0353200d5dd54ca
MD5 fa28614fdc1ae3f37fe6565fd880629f
BLAKE2b-256 6b6f4db7452dbf2d28d682b7881b6b4b8f0b620100f780a0ae9f156eaab12e97

See more details on using hashes here.

File details

Details for the file carbus_lib-0.1.9-py3-none-any.whl.

File metadata

  • Download URL: carbus_lib-0.1.9-py3-none-any.whl
  • Upload date:
  • Size: 38.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.9

File hashes

Hashes for carbus_lib-0.1.9-py3-none-any.whl
Algorithm Hash digest
SHA256 62a3a4242e79a68d7eb4d27bdbdc5e0590dac12a95aec43a261e123e762a6e52
MD5 3ed0afe880fcb7b35ab51cbd833623ce
BLAKE2b-256 fc8bb2e9c10d1a7fdaaa6aef282b4d31098cbf99a1c932a71ecb8aca828ee3db

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page