controllerzz 99451aceb8 update 0.1.9
2026-06-17 20:44:18 +03:00
2025-12-11 09:04:25 +03:00
2026-06-17 20:44:18 +03:00
2026-06-17 20:44:18 +03:00
2026-06-17 20:44:18 +03:00
2026-06-17 20:44:18 +03:00
2026-06-17 20:44:18 +03:00
2025-12-11 08:59:01 +03:00
2025-12-24 19:27:14 +03:00
2026-06-17 20:44:18 +03:00
2026-06-17 20:44:18 +03:00

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

S
Description
No description provided
Readme MIT 126 KiB
Languages
Python 100%