Skip to main content

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 и предложения по улучшению приветствуются 🚗

Release files for carbus-lib 0.1.9

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for carbus-lib 0.1.9
File Size Uploaded
carbus_lib-0.1.9.tar.gz 37.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for carbus-lib 0.1.9
File Interpreter ABI Platform
carbus_lib-0.1.9-py3-none-any.whl Python 3 none any Details

Total release size: 75.8 kB

Release files / carbus_lib-0.1.9.tar.gz

Download URL carbus_lib-0.1.9.tar.gz
Size 37.7 kB
Tags Source
SHA-256 checksum
How to use checksums
812674d5d57e5163624c80655802990daab12e417b7e72e5f0353200d5dd54ca
BLAKE2b-256 checksum
How to use checksums
6b6f4db7452dbf2d28d682b7881b6b4b8f0b620100f780a0ae9f156eaab12e97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Release files / carbus_lib-0.1.9-py3-none-any.whl

Download URL carbus_lib-0.1.9-py3-none-any.whl
Size 38.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
62a3a4242e79a68d7eb4d27bdbdc5e0590dac12a95aec43a261e123e762a6e52
BLAKE2b-256 checksum
How to use checksums
fc8bb2e9c10d1a7fdaaa6aef282b4d31098cbf99a1c932a71ecb8aca828ee3db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.9

Release history Release notifications | RSS feed

This release

0.1.9 This release

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page