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-FD и BRS
- Приём и хуки
- Периодическая отправка
- Фильтры
- Терминатор
- Информация об устройстве
- Маршрутизация по CAN-ID
- ISO-TP
- UDS клиент
- UDS сервер
- Удалённая работа
- Логирование
- Примеры
- Лицензия
Возможности
- 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 и предложения по улучшению приветствуются 🚗