mirror of
https://github.com/controllerzz/carbus_lib.git
synced 2026-07-15 08:20:14 +03:00
update 0.1.9
This commit is contained in:
@@ -206,3 +206,4 @@ marimo/_static/
|
|||||||
marimo/_lsp/
|
marimo/_lsp/
|
||||||
__marimo__/
|
__marimo__/
|
||||||
*.xml
|
*.xml
|
||||||
|
/RELEASE.md
|
||||||
|
|||||||
@@ -1,27 +1,67 @@
|
|||||||
# carbus-lib (async CAN / ISO-TP / UDS stack)
|
# carbus-lib
|
||||||
|
|
||||||
Асинхронная библиотека на Python для работы с CAN-адаптером **CAN-Hacker / Car Bus Analyzer**:
|
Асинхронная Python-библиотека для работы с CAN / CAN-FD адаптерами
|
||||||
|
**CAN-Hacker / Car Bus Analyzer**: низкоуровневый CAN/LIN, ISO-TP, UDS и удалённый
|
||||||
|
доступ к адаптеру через сеть.
|
||||||
|
|
||||||
- 📡 **`carbus_async`** – низкоуровневая работа с железкой (CAN/LIN, фильтры, терминаторы и т.д.)
|
- 📡 **`carbus_async`** — работа с железом: CAN/CAN-FD, фильтры, терминатор 120 Ω, периодическая отправка, хуки
|
||||||
- 📦 **`isotp_async`** – ISO-TP (ISO 15765-2) поверх CAN (single + multi-frame)
|
- 📦 **`isotp_async`** — ISO-TP (ISO 15765-2): single- и multi-frame
|
||||||
- 🩺 **`uds_async`** – UDS (ISO 14229) клиент и сервер (диагностика, чтение VIN и т.п.)
|
- 🩺 **`uds_async`** — UDS (ISO 14229): клиент и сервер (эмуляция ЭБУ)
|
||||||
- 🌐 **`Remote/TCP-bridge`** – удалённое подключение к адаптеру через сеть (как будто он воткнут локально)
|
- 🌐 **Remote / TCP-bridge** — удалённый доступ к адаптеру через сеть, как к локальному
|
||||||
|
|
||||||
> Python 3.10 и выше
|
> Python ≥ 3.10 · зависимости: `pyserial`, `pyserial-asyncio` · целиком на `asyncio`
|
||||||
> Никаких «магических» зависимостей — всё на `asyncio`.
|
> Протестировано на устройствах с **Протоколом версии 22**
|
||||||
> Поддерживаемые интерфейсы: https://canhacker.ru/shop/
|
> Поддерживаемые устройства: https://canhacker.ru/shop/
|
||||||
> _*Тестировалось на устройствах с Протоколом Версии 22_
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Содержание
|
||||||
|
|
||||||
|
- [Возможности](#возможности)
|
||||||
|
- [Установка](#установка)
|
||||||
|
- [Быстрый старт](#быстрый-старт)
|
||||||
|
- [CAN-FD и BRS](#can-fd-и-brs)
|
||||||
|
- [Приём и хуки](#приём-и-хуки)
|
||||||
|
- [Периодическая отправка](#периодическая-отправка)
|
||||||
|
- [Фильтры](#фильтры)
|
||||||
|
- [Терминатор](#терминатор)
|
||||||
|
- [Информация об устройстве](#информация-об-устройстве)
|
||||||
|
- [Маршрутизация по CAN-ID](#маршрутизация-по-can-id)
|
||||||
|
- [ISO-TP](#iso-tp)
|
||||||
|
- [UDS клиент](#uds-клиент)
|
||||||
|
- [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-мост
|
||||||
|
- Подробное логирование всего протокольного трафика
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
|
|
||||||
Через систему управления пакетами PIP
|
Из PyPI:
|
||||||
````bash
|
|
||||||
python -m pip install carbus-lib
|
|
||||||
````
|
|
||||||
|
|
||||||
Либо как editable-модуль из репозитория:
|
```bash
|
||||||
|
python -m pip install carbus-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Либо editable-режим из репозитория (нужно для разработки):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/controllerzz/carbus_lib.git
|
git clone https://github.com/controllerzz/carbus_lib.git
|
||||||
@@ -29,142 +69,137 @@ cd carbus_lib
|
|||||||
pip install -e .
|
pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
# carbus-lib
|
|
||||||
|
|
||||||
Асинхронная библиотека для работы с CAN / CAN-FD, ISO-TP и UDS.
|
|
||||||
Поддерживает локальное подключение через USB CDC и удалённую работу через TCP-bridge.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Возможности
|
## Быстрый старт
|
||||||
|
|
||||||
- CAN / CAN-FD отправка и приём
|
Открыть устройство, настроить классический CAN-канал, отправить и принять кадр.
|
||||||
- Настройка каналов, скоростей, режимов, BRS
|
|
||||||
- Фильтры ID, очистка фильтров, управление терминатором 120 Ω
|
|
||||||
- ISO-TP (single + multi-frame)
|
|
||||||
- UDS Client и UDS Server (эмуляция ЭБУ)
|
|
||||||
- TCP-мост: удалённая работа с адаптером так, как будто он подключён локально
|
|
||||||
- Логирование всего протокольного трафика
|
|
||||||
|
|
||||||
---
|
```python
|
||||||
|
|
||||||
## Работа с CAN
|
|
||||||
|
|
||||||
Простейший пример: открыть устройство, настроить канал и отправить / принять кадр.
|
|
||||||
|
|
||||||
````python
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from carbus_async.device import CarBusDevice
|
from carbus_async import CarBusDevice, CanMessage
|
||||||
from carbus_async.messages import CanMessage
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
dev = await CarBusDevice.open("COM6", baudrate=115200)
|
dev = await CarBusDevice.open("COM6", baudrate=115200)
|
||||||
|
|
||||||
# классический CAN 500 kbit/s
|
# классический CAN, 500 kbit/s, канал 1
|
||||||
await dev.open_can_channel(
|
await dev.open_can_channel(channel=1, nominal_bitrate=500_000)
|
||||||
channel=1,
|
|
||||||
nominal_bitrate=500_000,
|
|
||||||
)
|
|
||||||
|
|
||||||
# включаем терминатор 120 Ω на канале 1
|
# внутренний терминатор 120 Ω (если поддерживается устройством)
|
||||||
await dev.set_terminator(channel=1, enabled=True)
|
await dev.ensure_terminator(channel=1, enabled=True)
|
||||||
|
|
||||||
# отправка кадра 0x7E0 8 байт
|
# отправка кадра
|
||||||
msg = CanMessage(can_id=0x7E0, data=b"\x02\x3E\x00\x00\x00\x00\x00\x00")
|
msg = CanMessage(can_id=0x7E0, data=b"\x02\x3E\x00\x00\x00\x00\x00\x00")
|
||||||
await dev.send_can(msg, channel=1)
|
await dev.send_can(msg, channel=1)
|
||||||
|
|
||||||
# приём любого сообщения
|
# приём любого кадра -> (channel, CanMessage)
|
||||||
ch, rx = await dev.receive_can()
|
ch, rx = await dev.receive_can()
|
||||||
print("RX:", ch, rx)
|
print("RX:", ch, hex(rx.can_id), rx.data.hex())
|
||||||
|
|
||||||
await dev.close()
|
await dev.close()
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
````
|
|
||||||
|
|
||||||
## Настройка канала через Bit Timing
|
asyncio.run(main())
|
||||||
Возможность конфигруации скорости CAN канала через Bit Timing
|
```
|
||||||
````python
|
|
||||||
# CANFD+BRS 500/2000 kbit/s
|
Классический CAN через `open_can_channel` поддерживает битрейты
|
||||||
await dev.open_can_channel_custom(
|
10k / 20k / 33.3k / 50k / 62.5k / 83.3k / 95.2k / 100k / 125k / 250k / 400k / 500k / 800k / 1M.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CAN-FD и BRS
|
||||||
|
|
||||||
|
### 1. По стандартным битрейтам
|
||||||
|
|
||||||
|
```python
|
||||||
|
await dev.open_can_channel(
|
||||||
channel=1,
|
channel=1,
|
||||||
nominal_timing=CanTiming(
|
nominal_bitrate=500_000, # арбитражная фаза
|
||||||
prescaler=15,
|
data_bitrate=2_000_000, # фаза данных (используется при BRS)
|
||||||
tq_seg1=12,
|
|
||||||
tq_seg2=3,
|
|
||||||
sjw=1
|
|
||||||
),
|
|
||||||
data_timing=CanTiming(
|
|
||||||
prescaler=6,
|
|
||||||
tq_seg1=7,
|
|
||||||
tq_seg2=2,
|
|
||||||
sjw=1
|
|
||||||
),
|
|
||||||
fd=True,
|
fd=True,
|
||||||
brs=True,
|
brs=True,
|
||||||
)
|
)
|
||||||
````
|
```
|
||||||
|
|
||||||
## Получение информации об устройстве:
|
Поддерживаемые битрейты (CAN-модуль 120 МГц, точка выборки ~80%):
|
||||||
Получение инфмормации об устройстве и его фичах
|
|
||||||
````python
|
|
||||||
info = await dev.get_device_info()
|
|
||||||
|
|
||||||
print("HW:", info.hardware_name)
|
- **nominal:** 125k / 250k / 500k / 1M
|
||||||
print("FW:", info.firmware_version)
|
- **data:** 500k / 1M / 2M / 4M / 5M
|
||||||
print("Serial:", info.serial_int)
|
|
||||||
|
|
||||||
print("Features:",
|
> На этих адаптерах FD-канал настраивается точными bit-timing, а не одиночным
|
||||||
"gateway" if info.feature_gateway else "",
|
> индексом скорости данных — иначе канал открывается, но FD/BRS-кадры не уходят
|
||||||
"isotp" if info.feature_isotp else "",
|
> в шину. `open_can_channel` делает это автоматически. Если тактовая модуля не
|
||||||
"txbuf" if info.feature_tx_buffer else "",
|
> 120 МГц, передайте её через `can_clock_hz=...`.
|
||||||
"txtask" if info.feature_tx_task else "",
|
|
||||||
)
|
|
||||||
````
|
|
||||||
|
|
||||||
## Пример настройки фильтров:
|
### 2. По точным bit-timing
|
||||||
11 bit фильтры имеют index от 0 до 27 включительно,
|
|
||||||
29 bit фильтры имеют index от 28 до 35 включительно
|
|
||||||
````python
|
|
||||||
# очистить все фильтры на канале 1
|
|
||||||
await dev.clear_all_filters(1)
|
|
||||||
|
|
||||||
# разрешить только ответы с ID 0x7E8 (11-битный стандартный ID)
|
Для нестандартных скоростей задайте тайминги напрямую:
|
||||||
await dev.set_std_id_filter(
|
|
||||||
|
```python
|
||||||
|
from carbus_async import CanTiming
|
||||||
|
|
||||||
|
await dev.open_can_channel_custom(
|
||||||
channel=1,
|
channel=1,
|
||||||
index=0,
|
nominal_timing=CanTiming(prescaler=15, tq_seg1=12, tq_seg2=3, sjw=1), # 500k @ 120 МГц
|
||||||
can_id=0x7E8,
|
data_timing=CanTiming(prescaler=6, tq_seg1=7, tq_seg2=2, sjw=1), # 2M @ 120 МГц
|
||||||
mask=0x7FF,
|
fd=True,
|
||||||
|
brs=True,
|
||||||
)
|
)
|
||||||
````
|
```
|
||||||
|
|
||||||
## Управление терминатором 120 Ω:
|
### Отправка FD-кадра
|
||||||
Включаем терминатор на канале 1 и выключаем терминатор на канале 2
|
|
||||||
````python
|
|
||||||
await dev.set_terminator(channel=1, enabled=True)
|
|
||||||
await dev.set_terminator(channel=2, enabled=False)
|
|
||||||
````
|
|
||||||
|
|
||||||
Проверка наличия внутреннего терминатора у девайса
|
Тип кадра задаётся флагами `fd` / `brs` в самом `CanMessage`:
|
||||||
````python
|
|
||||||
if await dev.has_terminator():
|
|
||||||
await dev.set_terminator(channel=1, enabled=True)
|
|
||||||
print("Device has an internal terminator")
|
|
||||||
else:
|
|
||||||
print("Device does not have an internal terminator")
|
|
||||||
````
|
|
||||||
|
|
||||||
Включаем терминатор на канале 1, если это поддерживает девайс
|
```python
|
||||||
````python
|
# FD-кадр с переключением скорости (BRS), 16 байт данных
|
||||||
await dev.ensure_terminator(channel=1, enabled=True)
|
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 байт.
|
||||||
Отправка сообщений с статичными данными и периодом 100мс
|
|
||||||
````python
|
---
|
||||||
|
|
||||||
|
## Приём и хуки
|
||||||
|
|
||||||
|
Способы приёма:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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 при таймауте
|
||||||
|
```
|
||||||
|
|
||||||
|
Хуки (вызываются на каждый подходящий принятый кадр):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# по 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")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Периодическая отправка
|
||||||
|
|
||||||
|
```python
|
||||||
from carbus_async import PeriodicCanSender
|
from carbus_async import PeriodicCanSender
|
||||||
|
|
||||||
sender = PeriodicCanSender(dev)
|
sender = PeriodicCanSender(dev)
|
||||||
|
|
||||||
|
# статичные данные, период 100 мс (классический CAN)
|
||||||
sender.add(
|
sender.add(
|
||||||
"heartbeat",
|
"heartbeat",
|
||||||
channel=1,
|
channel=1,
|
||||||
@@ -172,14 +207,8 @@ sender.add(
|
|||||||
data=b"\x01\x02\x03\x04\x05\x06\x07\x08",
|
data=b"\x01\x02\x03\x04\x05\x06\x07\x08",
|
||||||
period_s=0.1,
|
period_s=0.1,
|
||||||
)
|
)
|
||||||
````
|
|
||||||
|
|
||||||
Отправка сообщений с модификацией данных и периодом 500мс
|
|
||||||
````python
|
|
||||||
from carbus_async import PeriodicCanSender
|
|
||||||
|
|
||||||
sender = PeriodicCanSender(dev)
|
|
||||||
|
|
||||||
|
# CAN-FD + BRS со счётчиком в первом байте
|
||||||
def mod(tick, data):
|
def mod(tick, data):
|
||||||
b = bytearray(data)
|
b = bytearray(data)
|
||||||
b[0] = tick & 0xFF
|
b[0] = tick & 0xFF
|
||||||
@@ -191,160 +220,280 @@ sender.add(
|
|||||||
can_id=0x100,
|
can_id=0x100,
|
||||||
data=b"\x00" * 8,
|
data=b"\x00" * 8,
|
||||||
period_s=0.5,
|
period_s=0.5,
|
||||||
modify=mod)
|
fd=True,
|
||||||
````
|
brs=True,
|
||||||
|
modify=mod,
|
||||||
|
|
||||||
## Хуки подписка на сообщение / сообщение + данные по маске:
|
|
||||||
Подписка по CAN ID
|
|
||||||
````python
|
|
||||||
@dev.on_can_id(0x7E0)
|
|
||||||
async def on_engine_req(ch, msg):
|
|
||||||
print("ENGINE:", hex(msg.can_id), msg.data.hex())
|
|
||||||
````
|
|
||||||
|
|
||||||
Подписка по CAN ID + маске данных
|
|
||||||
````python
|
|
||||||
@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("SessionControl")
|
# управление задачами
|
||||||
````
|
await sender.remove("cnt") # остановить и удалить одну задачу
|
||||||
|
await sender.stop_all() # остановить все
|
||||||
## ISO-TP (isotp_async)
|
```
|
||||||
ISO-TP канал строится поверх CarBusDevice:
|
|
||||||
````python
|
Параметры `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`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# очистить все фильтры на канале 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Терминатор
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Информация об устройстве
|
||||||
|
|
||||||
|
```python
|
||||||
|
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 —
|
||||||
|
удобно, когда на одном канале несколько независимых потоков.
|
||||||
|
|
||||||
|
```python
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```python
|
||||||
from isotp_async import open_isotp
|
from isotp_async import open_isotp
|
||||||
|
|
||||||
isotp = await open_isotp(dev, channel=1, tx_id=0x7E0, rx_id=0x7E8)
|
isotp = await open_isotp(dev, channel=1, tx_id=0x7E0, rx_id=0x7E8)
|
||||||
|
|
||||||
# отправить запрос ReadDataByIdentifier F190 (VIN)
|
# ReadDataByIdentifier F190 (VIN)
|
||||||
await isotp.send_pdu(b"\x22\xF1\x90")
|
await isotp.send_pdu(b"\x22\xF1\x90")
|
||||||
|
|
||||||
# получить полный ответ (single или multi-frame)
|
# полный ответ (single или multi-frame)
|
||||||
resp = await isotp.recv_pdu(timeout=5.0)
|
resp = await isotp.recv_pdu(timeout=5.0)
|
||||||
print("ISO-TP:", resp.hex())
|
print("ISO-TP:", resp.hex())
|
||||||
````
|
```
|
||||||
|
|
||||||
## UDS Client (uds_async.client)
|
---
|
||||||
|
|
||||||
Клиент UDS использует IsoTpChannel:
|
## UDS клиент
|
||||||
````python
|
|
||||||
|
```python
|
||||||
from isotp_async import open_isotp
|
from isotp_async import open_isotp
|
||||||
from uds_async import UdsClient
|
from uds_async import UdsClient
|
||||||
|
|
||||||
isotp = await open_isotp(dev, channel=1, tx_id=0x7E0, rx_id=0x7E8)
|
isotp = await open_isotp(dev, channel=1, tx_id=0x7E0, rx_id=0x7E8)
|
||||||
uds = UdsClient(isotp)
|
uds = UdsClient(isotp)
|
||||||
|
|
||||||
|
await uds.diagnostic_session_control(0x03) # extended session
|
||||||
vin = await uds.read_data_by_identifier(0xF190)
|
vin = await uds.read_data_by_identifier(0xF190)
|
||||||
print("VIN:", vin.decode(errors="ignore"))
|
print("VIN:", vin.decode(errors="ignore"))
|
||||||
````
|
await uds.tester_present()
|
||||||
|
```
|
||||||
|
|
||||||
## Удалённая работа через внешний сервер
|
Доступные сервисы: `diagnostic_session_control`, `read_data_by_identifier`,
|
||||||
### 0. Бесплатный Сервер/Relay
|
`write_data_by_identifier`, `security_access_get_seed`,
|
||||||
Можно воспользоваться бесплатным сервером:
|
`security_access_send_key`, `tester_present`.
|
||||||
|
|
||||||
IP: 185.42.26.80
|
---
|
||||||
PORT: 9000
|
|
||||||
|
|
||||||
### 1. Сервер/Relay
|
## UDS сервер
|
||||||
На сервере устанавливаем библиотеку carbus-lib, далее запускаем сервер
|
|
||||||
|
|
||||||
carbus-relay-server --host 0.0.0.0 --port 9000
|
Эмуляция ЭБУ: на каждый сервис (SID) вешается обработчик, возвращающий полный
|
||||||
или
|
ответный PDU. Отрицательный ответ — через `UdsNegativeResponse`.
|
||||||
|
|
||||||
python -m carbus_async.remote.server --host 0.0.0.0 --port 9000
|
```python
|
||||||
|
from isotp_async import open_isotp
|
||||||
|
from uds_async import UdsServer
|
||||||
|
from uds_async.exceptions import UdsNegativeResponse
|
||||||
|
|
||||||
### 2. Агент
|
# слушаем запросы на 0x7E0, отвечаем на 0x7E8
|
||||||
На машине куда подключен девайс запускаем агента
|
isotp = await open_isotp(dev, channel=1, tx_id=0x7E8, rx_id=0x7E0)
|
||||||
|
server = UdsServer(isotp)
|
||||||
|
|
||||||
carbus-relay-agent --port COM6 --baudrate 115200 --server <IP_СЕРВЕРА>:9000 --serial 5957 --password 1234
|
@server.service(0x22) # ReadDataByIdentifier
|
||||||
или
|
async def on_rdbi(req: bytes) -> bytes:
|
||||||
|
did = (req[1] << 8) | req[2]
|
||||||
python -m carbus_async.remote.agent --port COM6 --baudrate 115200 --server <IP_СЕРВЕРА>:9000 --serial 5957 --password 1234
|
if did == 0xF190:
|
||||||
|
return b"\x62\xF1\x90" + b"WVWZZZ1KZAW000001"
|
||||||
|
raise UdsNegativeResponse(req_sid=0x22, nrc=0x31) # requestOutOfRange
|
||||||
|
|
||||||
где
|
await server.serve_forever()
|
||||||
|
```
|
||||||
|
|
||||||
--server <IP_СЕРВЕРА>:9000 - адрес и порт сервера
|
---
|
||||||
--serial 5957 - серийный номер девайса
|
|
||||||
--password 1234 - пароль, требуется для получения уделнного доступа
|
|
||||||
|
|
||||||
### 3. Клиент (удалённая машина)
|
## Удалённая работа
|
||||||
Для получения удаленного доступа используем функцию ***open_remote_device***
|
|
||||||
|
|
||||||
````python
|
### Через relay-сервер (любой IP)
|
||||||
|
|
||||||
|
Можно воспользоваться бесплатным сервером:
|
||||||
|
|
||||||
|
```
|
||||||
|
IP: 185.42.26.80
|
||||||
|
PORT: 9000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Сервер / Relay** (машина с белым IP):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
carbus-relay-server --host 0.0.0.0 --port 9000
|
||||||
|
# или
|
||||||
|
python -m carbus_async.remote.server --host 0.0.0.0 --port 9000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Агент** (машина, к которой подключён адаптер):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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` | пароль для удалённого доступа |
|
||||||
|
|
||||||
|
**Клиент** (удалённая машина):
|
||||||
|
|
||||||
|
```python
|
||||||
from carbus_async import open_remote_device
|
from carbus_async import open_remote_device
|
||||||
dev = await open_remote_device(<IP_СЕРВЕРА>, 9000, serial=5957, password="1234")
|
|
||||||
````
|
|
||||||
|
|
||||||
## Удалённая работа в локальной сети через TCP (tcp_bridge)
|
dev = await open_remote_device("185.42.26.80", 9000, serial=5957, password="1234")
|
||||||
|
# дальше — обычный API CarBusDevice
|
||||||
|
```
|
||||||
|
|
||||||
### 1. Сервер (рядом с адаптером)
|
### Через TCP-мост в локальной сети
|
||||||
|
|
||||||
На машине, где физически подключён CAN-адаптер:
|
**Сервер** (рядом с адаптером):
|
||||||
|
|
||||||
python.exe -m carbus_async.tcp_bridge --serial COM6 --port 7000
|
```bash
|
||||||
|
python -m carbus_async.tcp_bridge --serial COM6 --port 7000
|
||||||
|
```
|
||||||
|
|
||||||
Адаптер открывается локально, а поверх него поднимается TCP-мост.
|
**Клиент** (та же сеть) — тот же API, но через `open_tcp`:
|
||||||
|
|
||||||
### 2. Клиент (удалённая машина)
|
```python
|
||||||
|
|
||||||
На другой машине можно использовать тот же API, как с локальным COM, но через `open_tcp`:
|
|
||||||
````python
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from carbus_async.device import CarBusDevice
|
from carbus_async import CarBusDevice, CanMessage
|
||||||
from carbus_async.messages import CanMessage
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
dev = await CarBusDevice.open_tcp("192.168.1.10", 7000)
|
dev = await CarBusDevice.open_tcp("192.168.1.10", 7000)
|
||||||
|
|
||||||
await dev.open_can_channel(
|
await dev.open_can_channel(channel=1, nominal_bitrate=500_000)
|
||||||
channel=1,
|
|
||||||
nominal_bitrate=500_000,
|
|
||||||
fd=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
msg = CanMessage(can_id=0x321, data=b"\x01\x02\x03\x04")
|
msg = CanMessage(can_id=0x321, data=b"\x01\x02\x03\x04")
|
||||||
await dev.send_can(msg, channel=1)
|
await dev.send_can(msg, channel=1)
|
||||||
|
|
||||||
ch, rx = await dev.receive_can()
|
ch, rx = await dev.receive_can()
|
||||||
print("REMOTE RX:", ch, rx)
|
print("REMOTE RX:", ch, hex(rx.can_id), rx.data.hex())
|
||||||
|
|
||||||
await dev.close()
|
await dev.close()
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
````
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Логирование
|
## Логирование
|
||||||
|
|
||||||
Для отладки удобно включить подробное логирование:
|
```python
|
||||||
````python
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
)
|
)
|
||||||
````
|
```
|
||||||
|
|
||||||
Логгеры:
|
Логгеры:
|
||||||
|
|
||||||
- `carbus_async.wire.*` — сырые кадры по USB/TCP (TX/RX)
|
- `carbus_async.wire.*` — сырые кадры по USB/TCP (TX/RX)
|
||||||
- `carbus_async.device.*` — высокоуровневые события, ошибки, BUS_ERROR
|
- `carbus_async.device.*` — высокоуровневые события, ошибки, `BUS_ERROR`
|
||||||
- дополнительные логгеры в isotp_async / uds_async
|
- дополнительные логгеры в `isotp_async` / `uds_async`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
Готовые скрипты — в каталоге [`example/`](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.
|
MIT — распространяется «как есть», без каких-либо гарантий. См. [LICENSE](LICENSE).
|
||||||
|
|
||||||
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ, НО НЕ ОГРАНИЧИВАЯСЬ ИМИ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ИСКАМ, ЗА УЩЕРБ ИЛИ ПО ИНЫМ ТРЕБОВАНИЯМ, В ТОМ ЧИСЛЕ, ПРИ ДЕЙСТВИИ КОНТРАКТА, ДЕЛИКТЕ ИЛИ ИНОЙ СИТУАЦИИ, ВОЗНИКШИМ ИЗ-ЗА ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Pull Requests и предложения по улучшению приветствуются 🚗
|
Pull Requests и предложения по улучшению приветствуются 🚗
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .device import CarBusDevice
|
from .device import CarBusDevice, CanTiming
|
||||||
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
|
from .can_router import CanIdRouter, RoutedCarBusCanTransport
|
||||||
@@ -7,6 +7,7 @@ from .remote.client import open_remote_device
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CarBusDevice",
|
"CarBusDevice",
|
||||||
|
"CanTiming",
|
||||||
"CanMessage",
|
"CanMessage",
|
||||||
"MessageDirection",
|
"MessageDirection",
|
||||||
"CarBusError",
|
"CarBusError",
|
||||||
|
|||||||
@@ -49,6 +49,40 @@ class CanTiming:
|
|||||||
sjw: int
|
sjw: int
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CAN_CLOCK_HZ = 120_000_000
|
||||||
|
|
||||||
|
# Проверенные bit-timing'и для CAN-модуля с тактовой 120 МГц (точка выборки ~80%).
|
||||||
|
# open_can_channel() для FD-канала задаёт скорость именно таймингами, а не
|
||||||
|
# одиночным индексом data-скорости (CC_BUS_SPEED_D): прошивка этих адаптеров
|
||||||
|
# применяет явные тайминги надёжно, а индекс data-скорости не отрабатывает —
|
||||||
|
# канал открывается, но FD/BRS-кадры не уходят в шину.
|
||||||
|
_NOMINAL_TIMINGS_120M: Dict[int, CanTiming] = {
|
||||||
|
1_000_000: CanTiming(prescaler=6, tq_seg1=15, tq_seg2=4, sjw=1), # 120/(6*20)
|
||||||
|
500_000: CanTiming(prescaler=15, tq_seg1=12, tq_seg2=3, sjw=1), # 120/(15*16)
|
||||||
|
250_000: CanTiming(prescaler=30, tq_seg1=12, tq_seg2=3, sjw=1), # 120/(30*16)
|
||||||
|
125_000: CanTiming(prescaler=60, tq_seg1=12, tq_seg2=3, sjw=1), # 120/(60*16)
|
||||||
|
}
|
||||||
|
_DATA_TIMINGS_120M: Dict[int, CanTiming] = {
|
||||||
|
5_000_000: CanTiming(prescaler=3, tq_seg1=5, tq_seg2=2, sjw=1), # 120/(3*8)
|
||||||
|
4_000_000: CanTiming(prescaler=3, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(3*10)
|
||||||
|
2_000_000: CanTiming(prescaler=6, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(6*10)
|
||||||
|
1_000_000: CanTiming(prescaler=12, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(12*10)
|
||||||
|
500_000: CanTiming(prescaler=24, tq_seg1=7, tq_seg2=2, sjw=1), # 120/(24*10)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_timing(
|
||||||
|
table: Dict[int, CanTiming], bitrate: int, clock_hz: int, kind: str
|
||||||
|
) -> CanTiming:
|
||||||
|
if clock_hz == DEFAULT_CAN_CLOCK_HZ and bitrate in table:
|
||||||
|
return table[bitrate]
|
||||||
|
raise ValueError(
|
||||||
|
f"Нет встроенного FD bit-timing для {kind}={bitrate} bps при тактовой "
|
||||||
|
f"{clock_hz} Гц. Доступно (для 120 МГц): {sorted(table)}. "
|
||||||
|
f"Задайте скорость явными таймингами через open_can_channel_custom(...)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DeviceInfoParam:
|
class DeviceInfoParam:
|
||||||
header: int
|
header: int
|
||||||
@@ -823,8 +857,33 @@ class CarBusDevice:
|
|||||||
non_iso: bool = False,
|
non_iso: bool = False,
|
||||||
nominal_index: Optional[int] = None,
|
nominal_index: Optional[int] = None,
|
||||||
data_index: Optional[int] = None,
|
data_index: Optional[int] = None,
|
||||||
|
can_clock_hz: Optional[int] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
# FD-канал настраиваем явными таймингами (рабочий путь): одиночный индекс
|
||||||
|
# data-скорости прошивкой не применяется, канал открывается, но FD/BRS-кадры
|
||||||
|
# не уходят в шину. Явное переопределение индексами/auto_detect не трогаем.
|
||||||
|
if fd and not (auto_detect or nominal_index is not None or data_index is not None):
|
||||||
|
if data_bitrate is None:
|
||||||
|
raise ValueError("fd=True требует указать data_bitrate")
|
||||||
|
clock = can_clock_hz or DEFAULT_CAN_CLOCK_HZ
|
||||||
|
await self.open_can_channel_custom(
|
||||||
|
channel,
|
||||||
|
nominal_timing=_resolve_timing(
|
||||||
|
_NOMINAL_TIMINGS_120M, nominal_bitrate, clock, "nominal"
|
||||||
|
),
|
||||||
|
data_timing=_resolve_timing(
|
||||||
|
_DATA_TIMINGS_120M, data_bitrate, clock, "data"
|
||||||
|
),
|
||||||
|
fd=True,
|
||||||
|
brs=brs,
|
||||||
|
listen_only=listen_only,
|
||||||
|
loopback=loopback,
|
||||||
|
retransmit=retransmit,
|
||||||
|
non_iso=non_iso,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if loopback:
|
if loopback:
|
||||||
mode_val = 0x02
|
mode_val = 0x02
|
||||||
elif listen_only:
|
elif listen_only:
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ class PeriodicJob:
|
|||||||
msg = CanMessage(
|
msg = CanMessage(
|
||||||
can_id=self.can_id,
|
can_id=self.can_id,
|
||||||
data=out,
|
data=out,
|
||||||
|
extended=self.extended,
|
||||||
|
rtr=self.rtr,
|
||||||
|
fd=self.fd,
|
||||||
|
brs=self.brs,
|
||||||
)
|
)
|
||||||
await dev.send_can(
|
await dev.send_can(
|
||||||
msg,
|
msg,
|
||||||
|
|||||||
+115
-48
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -10,6 +11,15 @@ import serial_asyncio
|
|||||||
|
|
||||||
log = logging.getLogger("carbus_remote.agent")
|
log = logging.getLogger("carbus_remote.agent")
|
||||||
|
|
||||||
|
RECONNECT_MIN_S = 1.0
|
||||||
|
RECONNECT_MAX_S = 15.0
|
||||||
|
|
||||||
|
CONNECT_TIMEOUT_S = 5.0
|
||||||
|
AUTH_TIMEOUT_S = 10.0
|
||||||
|
DATA_HELLO_TIMEOUT_S = 5.0
|
||||||
|
|
||||||
|
COM_REOPEN_DELAY_S = 0.25
|
||||||
|
|
||||||
|
|
||||||
def parse_hostport(s: str) -> tuple[str, int]:
|
def parse_hostport(s: str) -> tuple[str, int]:
|
||||||
if ":" not in s:
|
if ":" not in s:
|
||||||
@@ -38,10 +48,8 @@ async def pipe_bidirectional_streams(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
dst.close()
|
dst.close()
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
t1 = asyncio.create_task(pump(a_reader, b_writer), name="pipe_a_to_b")
|
t1 = asyncio.create_task(pump(a_reader, b_writer), name="pipe_a_to_b")
|
||||||
t2 = asyncio.create_task(pump(b_reader, a_writer), name="pipe_b_to_a")
|
t2 = asyncio.create_task(pump(b_reader, a_writer), name="pipe_b_to_a")
|
||||||
@@ -49,32 +57,35 @@ async def pipe_bidirectional_streams(
|
|||||||
await asyncio.wait({t1, t2}, return_when=asyncio.FIRST_COMPLETED)
|
await asyncio.wait({t1, t2}, return_when=asyncio.FIRST_COMPLETED)
|
||||||
|
|
||||||
for w in (a_writer, b_writer):
|
for w in (a_writer, b_writer):
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
w.close()
|
w.close()
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
await asyncio.gather(t1, t2, return_exceptions=True)
|
await asyncio.gather(t1, t2, return_exceptions=True)
|
||||||
|
|
||||||
for w in (a_writer, b_writer):
|
for w in (a_writer, b_writer):
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
await w.wait_closed()
|
await w.wait_closed()
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def open_serial_with_retry(port: str, baudrate: int, *, attempts: int = 3) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
async def open_serial_with_retry(
|
||||||
|
port: str,
|
||||||
|
baudrate: int,
|
||||||
|
*,
|
||||||
|
attempts: int = 5,
|
||||||
|
) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
|
||||||
|
|
||||||
last: Optional[BaseException] = None
|
last: Optional[BaseException] = None
|
||||||
for i in range(attempts):
|
for i in range(attempts):
|
||||||
try:
|
try:
|
||||||
return await serial_asyncio.open_serial_connection(url=port, baudrate=baudrate)
|
return await serial_asyncio.open_serial_connection(url=port, baudrate=baudrate)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
last = e
|
last = e
|
||||||
await asyncio.sleep(0.25 + 0.25 * i)
|
await asyncio.sleep(0.25 + 0.35 * i)
|
||||||
assert last is not None
|
assert last is not None
|
||||||
raise last
|
raise last
|
||||||
|
|
||||||
|
|
||||||
async def agent_run(
|
async def agent_run_once(
|
||||||
*,
|
*,
|
||||||
port: str,
|
port: str,
|
||||||
baudrate: int,
|
baudrate: int,
|
||||||
@@ -82,11 +93,15 @@ async def agent_run(
|
|||||||
serial: str,
|
serial: str,
|
||||||
password: str,
|
password: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
server_host, server_port = parse_hostport(server)
|
server_host, server_port = parse_hostport(server)
|
||||||
|
|
||||||
session_lock = asyncio.Lock()
|
session_lock = asyncio.Lock()
|
||||||
|
active_data_task: Optional[asyncio.Task] = None
|
||||||
|
|
||||||
async def open_data_session(session: str) -> None:
|
async def open_data_session(session: str) -> None:
|
||||||
|
nonlocal active_data_task
|
||||||
|
|
||||||
async with session_lock:
|
async with session_lock:
|
||||||
net_r: Optional[asyncio.StreamReader] = None
|
net_r: Optional[asyncio.StreamReader] = None
|
||||||
net_w: Optional[asyncio.StreamWriter] = None
|
net_w: Optional[asyncio.StreamWriter] = None
|
||||||
@@ -98,63 +113,71 @@ async def agent_run(
|
|||||||
|
|
||||||
net_r, net_w = await asyncio.wait_for(
|
net_r, net_w = await asyncio.wait_for(
|
||||||
asyncio.open_connection(server_host, server_port),
|
asyncio.open_connection(server_host, server_port),
|
||||||
timeout=5.0,
|
timeout=CONNECT_TIMEOUT_S,
|
||||||
)
|
)
|
||||||
|
|
||||||
net_w.write((json.dumps({"role": "agent_data", "session": session}) + "\n").encode("utf-8"))
|
net_w.write((json.dumps({"role": "agent_data", "session": session}) + "\n").encode("utf-8"))
|
||||||
await net_w.drain()
|
await net_w.drain()
|
||||||
|
|
||||||
line = await asyncio.wait_for(net_r.readline(), timeout=5.0)
|
line = await asyncio.wait_for(net_r.readline(), timeout=DATA_HELLO_TIMEOUT_S)
|
||||||
|
if not line:
|
||||||
|
raise ConnectionError("relay closed data socket before hello")
|
||||||
|
|
||||||
resp = json.loads(line.decode("utf-8", errors="ignore") or "{}")
|
resp = json.loads(line.decode("utf-8", errors="ignore") or "{}")
|
||||||
if not resp.get("ok"):
|
if not resp.get("ok"):
|
||||||
log.error("Data session refused %s: %s", session, resp)
|
log.error("Data session refused %s: %s", session, resp)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await asyncio.sleep(COM_REOPEN_DELAY_S)
|
||||||
log.info("Opening COM %s @ %d for session %s", port, baudrate, session)
|
log.info("Opening COM %s @ %d for session %s", port, baudrate, session)
|
||||||
dev_r, dev_w = await open_serial_with_retry(port, baudrate, attempts=3)
|
dev_r, dev_w = await open_serial_with_retry(port, baudrate, attempts=5)
|
||||||
|
|
||||||
log.info("Session %s accepted. Piping bytes (COM <-> relay).", session)
|
log.info("Session %s accepted. Piping bytes (COM <-> relay).", session)
|
||||||
await pipe_bidirectional_streams(dev_r, dev_w, net_r, net_w)
|
await pipe_bidirectional_streams(dev_r, dev_w, net_r, net_w)
|
||||||
|
|
||||||
log.info("Session %s finished.", session)
|
log.info("Session %s finished.", session)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
log.info("Data session %s cancelled", session)
|
||||||
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Data session %s crashed", session)
|
log.exception("Data session %s crashed", session)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if net_w is not None:
|
if net_w is not None:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
net_w.close()
|
net_w.close()
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
await net_w.wait_closed()
|
await net_w.wait_closed()
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if dev_w is not None:
|
if dev_w is not None:
|
||||||
try:
|
with contextlib.suppress(Exception):
|
||||||
dev_w.close()
|
dev_w.close()
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
await dev_w.wait_closed()
|
await dev_w.wait_closed()
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
# --- CONTROL CONNECT ---
|
||||||
log.info("Connecting to relay %s:%d (control)", server_host, server_port)
|
log.info("Connecting to relay %s:%d (control)", server_host, server_port)
|
||||||
ctrl_reader, ctrl_writer = await asyncio.open_connection(server_host, server_port)
|
ctrl_reader, ctrl_writer = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection(server_host, server_port),
|
||||||
ctrl_writer.write((json.dumps({"role": "agent", "serial": serial, "password": password}) + "\n").encode("utf-8"))
|
timeout=CONNECT_TIMEOUT_S,
|
||||||
await ctrl_writer.drain()
|
)
|
||||||
|
|
||||||
line = await asyncio.wait_for(ctrl_reader.readline(), timeout=10.0)
|
|
||||||
resp = json.loads(line.decode("utf-8", errors="ignore") or "{}")
|
|
||||||
if not resp.get("ok"):
|
|
||||||
ctrl_writer.close()
|
|
||||||
try:
|
|
||||||
await ctrl_writer.wait_closed()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
raise RuntimeError(f"relay refused agent: {resp}")
|
|
||||||
|
|
||||||
log.info("Agent registered OK. Waiting for sessions... (serial=%s)", serial)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# AUTH
|
||||||
|
ctrl_writer.write((json.dumps({"role": "agent", "serial": serial, "password": password}) + "\n").encode("utf-8"))
|
||||||
|
await ctrl_writer.drain()
|
||||||
|
|
||||||
|
line = await asyncio.wait_for(ctrl_reader.readline(), timeout=AUTH_TIMEOUT_S)
|
||||||
|
if not line:
|
||||||
|
raise ConnectionError("relay closed control socket during auth")
|
||||||
|
|
||||||
|
resp = json.loads(line.decode("utf-8", errors="ignore") or "{}")
|
||||||
|
if not resp.get("ok"):
|
||||||
|
raise RuntimeError(f"relay refused agent: {resp}")
|
||||||
|
|
||||||
|
log.info("Agent registered OK. Waiting for sessions... (serial=%s)", serial)
|
||||||
|
|
||||||
|
# CONTROL LOOP
|
||||||
while True:
|
while True:
|
||||||
line = await ctrl_reader.readline()
|
line = await ctrl_reader.readline()
|
||||||
if not line:
|
if not line:
|
||||||
@@ -168,17 +191,62 @@ async def agent_run(
|
|||||||
|
|
||||||
if msg.get("cmd") == "open_session":
|
if msg.get("cmd") == "open_session":
|
||||||
session = str(msg.get("session", "")).strip()
|
session = str(msg.get("session", "")).strip()
|
||||||
if session:
|
if not session:
|
||||||
asyncio.create_task(open_data_session(session), name=f"agent_data_session_{session}")
|
continue
|
||||||
|
|
||||||
|
# если уже идёт data-session — игнорируем новый запрос (или можно логировать busy)
|
||||||
|
if active_data_task is not None and not active_data_task.done():
|
||||||
|
log.warning("Data session already running; ignoring open_session=%s", session)
|
||||||
|
continue
|
||||||
|
|
||||||
|
active_data_task = asyncio.create_task(
|
||||||
|
open_data_session(session),
|
||||||
|
name=f"agent_data_session_{session}",
|
||||||
|
)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
try:
|
if active_data_task is not None and not active_data_task.done():
|
||||||
ctrl_writer.close()
|
active_data_task.cancel()
|
||||||
await ctrl_writer.wait_closed()
|
with contextlib.suppress(Exception):
|
||||||
except Exception:
|
await active_data_task
|
||||||
pass
|
|
||||||
|
|
||||||
log.info("Agent stopped.")
|
with contextlib.suppress(Exception):
|
||||||
|
ctrl_writer.close()
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
await ctrl_writer.wait_closed()
|
||||||
|
|
||||||
|
log.info("Agent control session ended.")
|
||||||
|
|
||||||
|
|
||||||
|
async def agent_supervisor(
|
||||||
|
*,
|
||||||
|
port: str,
|
||||||
|
baudrate: int,
|
||||||
|
server: str,
|
||||||
|
serial: str,
|
||||||
|
password: str,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
delay = RECONNECT_MIN_S
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await agent_run_once(
|
||||||
|
port=port,
|
||||||
|
baudrate=baudrate,
|
||||||
|
server=server,
|
||||||
|
serial=serial,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
delay = RECONNECT_MIN_S
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Agent crashed: %s", e)
|
||||||
|
|
||||||
|
log.info("Reconnect in %.1f sec...", delay)
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
delay = min(RECONNECT_MAX_S, delay * 1.7)
|
||||||
|
|
||||||
|
|
||||||
async def main_async(
|
async def main_async(
|
||||||
@@ -188,7 +256,7 @@ async def main_async(
|
|||||||
serial: str,
|
serial: str,
|
||||||
password: str,
|
password: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
await agent_run(
|
await agent_supervisor(
|
||||||
port=port,
|
port=port,
|
||||||
baudrate=baudrate,
|
baudrate=baudrate,
|
||||||
server=server,
|
server=server,
|
||||||
@@ -224,4 +292,3 @@ def cli() -> None:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from carbus_async import CarBusDevice, PeriodicCanSender
|
||||||
|
from carbus_async.device import CanTiming
|
||||||
|
|
||||||
|
|
||||||
|
async def main(is_debug=False):
|
||||||
|
dev = await CarBusDevice.open("COM6")
|
||||||
|
|
||||||
|
# await dev.open_can_channel_custom(
|
||||||
|
# channel=1,
|
||||||
|
#
|
||||||
|
# nominal_timing=CanTiming(
|
||||||
|
# prescaler=15,
|
||||||
|
# tq_seg1=12,
|
||||||
|
# tq_seg2=3,
|
||||||
|
# sjw=1
|
||||||
|
# ),
|
||||||
|
# data_timing=CanTiming(
|
||||||
|
# prescaler=6,
|
||||||
|
# tq_seg1=7,
|
||||||
|
# tq_seg2=2,
|
||||||
|
# sjw=1
|
||||||
|
# ),
|
||||||
|
# fd=True,
|
||||||
|
# brs=True,
|
||||||
|
# )
|
||||||
|
|
||||||
|
await dev.open_can_channel(
|
||||||
|
channel=1,
|
||||||
|
nominal_bitrate=500_000,
|
||||||
|
data_bitrate=2_000_000,
|
||||||
|
fd=True,
|
||||||
|
brs=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await dev.ensure_terminator(channel=1, enabled=True)
|
||||||
|
|
||||||
|
sender = PeriodicCanSender(dev)
|
||||||
|
|
||||||
|
def mod(tick, data):
|
||||||
|
b = bytearray(data)
|
||||||
|
b[0] = tick & 0xFF
|
||||||
|
return bytes(b)
|
||||||
|
|
||||||
|
sender.add(
|
||||||
|
"cnt",
|
||||||
|
channel=1,
|
||||||
|
fd=True,
|
||||||
|
brs=True,
|
||||||
|
can_id=0x100,
|
||||||
|
data=b"\x00" * 8,
|
||||||
|
period_s=0.5,
|
||||||
|
modify=mod
|
||||||
|
)
|
||||||
|
|
||||||
|
sender.add(
|
||||||
|
"heartbeat",
|
||||||
|
channel=1,
|
||||||
|
can_id=0x123,
|
||||||
|
data=b"\x01\x02\x03\x04\x05\x06\x07\x08",
|
||||||
|
period_s=0.05,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.Event().wait()
|
||||||
|
finally:
|
||||||
|
await sender.stop_all()
|
||||||
|
await dev.close()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from carbus_async.device import CarBusDevice
|
||||||
|
|
||||||
|
# подстрой импорт под свой проект
|
||||||
|
from carbus_async.remote.client import open_remote_device
|
||||||
|
|
||||||
|
log = logging.getLogger("carbus.can_bridge.simple")
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_hooks(dev: CarBusDevice) -> None:
|
||||||
|
# фикс для твоей ошибки: где-то в CarBusDevice используется _can_hooks,
|
||||||
|
# но он не создаётся в open/open_stream
|
||||||
|
if not hasattr(dev, "_can_hooks"):
|
||||||
|
dev._can_hooks = [] # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
|
||||||
|
async def _open_local(port: str, baudrate: int) -> CarBusDevice:
|
||||||
|
dev = await CarBusDevice.open(port, baudrate=baudrate, use_can=True, use_lin=False)
|
||||||
|
_ensure_hooks(dev)
|
||||||
|
return dev
|
||||||
|
|
||||||
|
|
||||||
|
async def _open_remote(relay: str, serial: int, password: str) -> CarBusDevice:
|
||||||
|
if ":" not in relay:
|
||||||
|
raise ValueError("relay must be host:port")
|
||||||
|
host, p = relay.rsplit(":", 1)
|
||||||
|
dev = await open_remote_device(host, int(p), serial=serial, password=password)
|
||||||
|
_ensure_hooks(dev)
|
||||||
|
return dev
|
||||||
|
|
||||||
|
|
||||||
|
async def _setup_can_500k(dev: CarBusDevice, channel: int) -> None:
|
||||||
|
await dev.open_can_channel(channel=channel, nominal_bitrate=500_000, fd=False)
|
||||||
|
await dev.set_std_id_filter(
|
||||||
|
channel=1,
|
||||||
|
index=0,
|
||||||
|
can_id=0x700,
|
||||||
|
mask=0x700,
|
||||||
|
)
|
||||||
|
await dev.ensure_terminator(channel=1, enabled=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def _pump(
|
||||||
|
name: str,
|
||||||
|
src: CarBusDevice,
|
||||||
|
src_ch: int,
|
||||||
|
dst: CarBusDevice,
|
||||||
|
dst_ch: int,
|
||||||
|
stop: asyncio.Event,
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
while not stop.is_set():
|
||||||
|
ch, msg = await src.receive_can()
|
||||||
|
if ch != src_ch:
|
||||||
|
continue
|
||||||
|
await dst.send_can(msg, channel=dst_ch, confirm=False, echo=False)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("[%s] stopped by error: %r", name, e)
|
||||||
|
finally:
|
||||||
|
stop.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def main_async() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description="Simple CAN bridge local<->remote via relay (no filters)")
|
||||||
|
# defaults чтобы запускать из IDE
|
||||||
|
ap.add_argument("--local-port", default="COM6")
|
||||||
|
ap.add_argument("--local-baudrate", type=int, default=115200)
|
||||||
|
ap.add_argument("--local-channel", type=int, default=1)
|
||||||
|
|
||||||
|
ap.add_argument("--relay", default="185.42.26.80:9000")
|
||||||
|
ap.add_argument("--serial", type=int, default=5957)
|
||||||
|
ap.add_argument("--password", default="1234")
|
||||||
|
ap.add_argument("--remote-channel", type=int, default=1)
|
||||||
|
|
||||||
|
ap.add_argument("--log-level", default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"])
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, args.log_level),
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||||
|
)
|
||||||
|
|
||||||
|
stop = asyncio.Event()
|
||||||
|
local: Optional[CarBusDevice] = None
|
||||||
|
remote: Optional[CarBusDevice] = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.info("Open local device %s ...", args.local_port)
|
||||||
|
local = await _open_local(args.local_port, args.local_baudrate)
|
||||||
|
await _setup_can_500k(local, args.local_channel)
|
||||||
|
log.info("Local CAN ready: ch=%d @ 500k", args.local_channel)
|
||||||
|
|
||||||
|
log.info("Open remote device via relay %s (serial=%d) ...", args.relay, args.serial)
|
||||||
|
remote = await _open_remote(args.relay, args.serial, args.password)
|
||||||
|
await _setup_can_500k(remote, args.remote_channel)
|
||||||
|
log.info("Remote CAN ready: ch=%d @ 500k", args.remote_channel)
|
||||||
|
|
||||||
|
t1 = asyncio.create_task(
|
||||||
|
_pump("local->remote", local, args.local_channel, remote, args.remote_channel, stop),
|
||||||
|
name="pump_local_remote",
|
||||||
|
)
|
||||||
|
t2 = asyncio.create_task(
|
||||||
|
_pump("remote->local", remote, args.remote_channel, local, args.local_channel, stop),
|
||||||
|
name="pump_remote_local",
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info("CAN bridge running. Ctrl+C to stop.")
|
||||||
|
await asyncio.Event().wait()
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
stop.set()
|
||||||
|
|
||||||
|
# аккуратно гасим
|
||||||
|
for dev in (remote, local):
|
||||||
|
if dev is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
await dev.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.info("CAN bridge stopped.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main_async())
|
||||||
@@ -25,7 +25,7 @@ async def main(is_debug=False):
|
|||||||
channel=1,
|
channel=1,
|
||||||
can_id=0x100,
|
can_id=0x100,
|
||||||
data=b"\x00" * 8,
|
data=b"\x00" * 8,
|
||||||
period_s=0.001,
|
period_s=0.1,
|
||||||
modify=mod)
|
modify=mod)
|
||||||
|
|
||||||
sender.add(
|
sender.add(
|
||||||
@@ -33,7 +33,7 @@ async def main(is_debug=False):
|
|||||||
channel=1,
|
channel=1,
|
||||||
can_id=0x123,
|
can_id=0x123,
|
||||||
data=b"\x01\x02\x03\x04\x05\x06\x07\x08",
|
data=b"\x01\x02\x03\x04\x05\x06\x07\x08",
|
||||||
period_s=0.03,
|
period_s=0.05,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class IsoTpChannel:
|
|||||||
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
|
||||||
|
tx_gap_s = 0.001
|
||||||
|
|
||||||
async def send_pdu(self, data: bytes) -> None:
|
async def send_pdu(self, data: bytes) -> None:
|
||||||
length = len(data)
|
length = len(data)
|
||||||
@@ -88,7 +89,6 @@ class IsoTpChannel:
|
|||||||
raise RuntimeError(f"ISO-TP: unsupported FlowStatus=0x{fs:02X}")
|
raise RuntimeError(f"ISO-TP: unsupported FlowStatus=0x{fs:02X}")
|
||||||
|
|
||||||
if bs == 0x00:
|
if bs == 0x00:
|
||||||
# 0 => "unlimited"
|
|
||||||
bs = 0
|
bs = 0
|
||||||
|
|
||||||
st_min = _st_min_to_seconds(st_min_raw)
|
st_min = _st_min_to_seconds(st_min_raw)
|
||||||
@@ -139,14 +139,17 @@ class IsoTpChannel:
|
|||||||
await self.can.send(msg)
|
await self.can.send(msg)
|
||||||
|
|
||||||
seq_num = (seq_num + 1) & 0x0F
|
seq_num = (seq_num + 1) & 0x0F
|
||||||
if seq_num == 0:
|
# if seq_num == 0:
|
||||||
seq_num = 0x1
|
# seq_num = 0x1
|
||||||
|
|
||||||
frames_in_block += 1
|
frames_in_block += 1
|
||||||
|
|
||||||
if st_min > 0:
|
if st_min > 0:
|
||||||
await asyncio.sleep(st_min)
|
await asyncio.sleep(st_min)
|
||||||
|
|
||||||
|
if self.tx_gap_s > 0:
|
||||||
|
await asyncio.sleep(self.tx_gap_s)
|
||||||
|
|
||||||
async def recv_pdu(self, timeout: float = 1.0) -> Optional[bytes]:
|
async def recv_pdu(self, timeout: float = 1.0) -> Optional[bytes]:
|
||||||
|
|
||||||
first = await self.can.recv(timeout=timeout)
|
first = await self.can.recv(timeout=timeout)
|
||||||
@@ -221,6 +224,9 @@ class IsoTpChannel:
|
|||||||
if st_min > 0:
|
if st_min > 0:
|
||||||
await asyncio.sleep(st_min)
|
await asyncio.sleep(st_min)
|
||||||
|
|
||||||
|
if tx_gap_s > 0:
|
||||||
|
await asyncio.sleep(tx_gap_s)
|
||||||
|
|
||||||
return bytes(payload[:total_length])
|
return bytes(payload[:total_length])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "carbus-lib"
|
name = "carbus-lib"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
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"
|
||||||
|
|||||||
+54
-12
@@ -15,28 +15,29 @@ class UdsClient:
|
|||||||
|
|
||||||
async def _request(self, payload: bytes) -> bytes:
|
async def _request(self, payload: bytes) -> bytes:
|
||||||
await self.isotp.send_pdu(payload)
|
await self.isotp.send_pdu(payload)
|
||||||
resp = await self.isotp.recv_pdu(timeout=self.p2_timeout)
|
|
||||||
if resp is None:
|
while True:
|
||||||
raise TimeoutError("UDS response timeout")
|
resp = await self.isotp.recv_pdu(timeout=self.p2_timeout)
|
||||||
|
|
||||||
|
if resp is None:
|
||||||
|
raise TimeoutError("UDS response timeout")
|
||||||
|
|
||||||
|
if resp[0] == 0x7F and resp[2] == 0x78:
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
if resp[0] == 0x7F:
|
if resp[0] == 0x7F:
|
||||||
if len(resp) < 3:
|
if len(resp) < 3:
|
||||||
raise UdsError("Malformed UDS negative response")
|
raise UdsError("Malformed UDS negative response")
|
||||||
|
|
||||||
sid = resp[1]
|
sid = resp[1]
|
||||||
nrc = resp[2]
|
nrc = resp[2]
|
||||||
raise UdsNegativeResponse(req_sid=sid, nrc=nrc)
|
raise UdsNegativeResponse(req_sid=sid, nrc=nrc)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
async def diagnostic_session_control(self, session: int) -> bytes:
|
|
||||||
req = bytes([0x10, session & 0xFF])
|
|
||||||
resp = await self._request(req)
|
|
||||||
if resp[0] != 0x50:
|
|
||||||
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for DSC")
|
|
||||||
return resp
|
|
||||||
|
|
||||||
async def tester_present(self, suppress_response: bool = False) -> Optional[bytes]:
|
async def tester_present(self, suppress_response: bool = False) -> Optional[bytes]:
|
||||||
|
|
||||||
sub = 0x80 if suppress_response else 0x00
|
sub = 0x80 if suppress_response else 0x00
|
||||||
req = bytes([0x3E, sub])
|
req = bytes([0x3E, sub])
|
||||||
|
|
||||||
@@ -45,19 +46,60 @@ class UdsClient:
|
|||||||
resp = await self._request(req)
|
resp = await self._request(req)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
resp = await self._request(req)
|
resp = await self._request(req)
|
||||||
|
|
||||||
if resp[0] != 0x7E:
|
if resp[0] != 0x7E:
|
||||||
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for TesterPresent")
|
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for TesterPresent")
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def diagnostic_session_control(self, session: int) -> bytes:
|
||||||
|
req = bytes([0x10, session & 0xFF])
|
||||||
|
resp = await self._request(req)
|
||||||
|
|
||||||
|
if resp[0] != 0x50:
|
||||||
|
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for DSC")
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
async def read_data_by_identifier(self, did: int) -> bytes:
|
async def read_data_by_identifier(self, did: int) -> bytes:
|
||||||
|
|
||||||
req = bytes([0x22, (did >> 8) & 0xFF, did & 0xFF])
|
req = bytes([0x22, (did >> 8) & 0xFF, did & 0xFF])
|
||||||
resp = await self._request(req)
|
resp = await self._request(req)
|
||||||
|
|
||||||
if resp[0] != 0x62:
|
if resp[0] != 0x62:
|
||||||
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for RDBI")
|
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for RDBI")
|
||||||
|
|
||||||
if len(resp) < 3:
|
if len(resp) < 3:
|
||||||
raise UdsError("Malformed RDBI response")
|
raise UdsError("Malformed RDBI response")
|
||||||
|
|
||||||
return resp[3:]
|
return resp[3:]
|
||||||
|
|
||||||
|
async def write_data_by_identifier(self, did: int, data: bytes) -> bytes:
|
||||||
|
req = bytes([0x2E, (did >> 8) & 0xFF, did & 0xFF, *data])
|
||||||
|
resp = await self._request(req)
|
||||||
|
|
||||||
|
if resp[0] != 0x6E:
|
||||||
|
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for WDBI")
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
async def security_access_get_seed(self, level: int, data: bytes | None = None) -> bytes:
|
||||||
|
req = bytes([0x27, level & 0xFF]) + (data or b"")
|
||||||
|
resp = await self._request(req)
|
||||||
|
|
||||||
|
if resp[0] != 0x67:
|
||||||
|
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for SAGS")
|
||||||
|
|
||||||
|
return resp[2:]
|
||||||
|
|
||||||
|
async def security_access_send_key(self, level: int, key: bytes) -> bytes:
|
||||||
|
req = bytes([0x27, level & 0xFF]) + key
|
||||||
|
resp = await self._request(req)
|
||||||
|
|
||||||
|
if resp[0] != 0x67:
|
||||||
|
raise UdsError(f"Unexpected SID 0x{resp[0]:02X} for SASK")
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|||||||
Reference in New Issue
Block a user