Меня зовут Груздев Дмитрий Михайлович, я инженер АСУТП. Промышленная автоматизация — это измерительные каналы, полевые шины и возня с тем, что «должно работать по стандарту», а по факту работает как получилось у производителя железки. Эта статья — про то, как привычка проверять всё измерением помогла найти конкретное ограничение в самом массовом диагностическом адаптере.

Завязка: плавающий контакт, который не ловится сканером

У меня VW Polo Sedan, и в какой-то момент парктроник начал жить своей жизнью: иногда пищал корректно, иногда выдавал ложное препятствие, иногда молчал. Классическая картина плавающего контакта в жгуте — код ошибки то есть, то нет.

Первым делом я взял то, что берут все: дешёвый ELM327 и телефон с популярным приложением. Оно уверенно показывало двигатель и наотрез отказывалось видеть блок парковочной системы. Ничего удивительного: универсальные приложения работают с OBD-II, а OBD-II обязан покрывать только то, что влияет на выбросы.

Потом взял нормальный сканер, который блок видел, — и упёрся во вторую стену, которая оказалась важнее первой.

Штатный сканер показывает факт, но не показывает момент. На вопрос «есть ли ошибка» он отвечает хорошо. Но при плавающем дефекте вопрос другой: «в какой момент она появляется и что я в этот момент трогал руками». Пока подключаешься, ждёшь инициализацию, листаешь меню до нужного блока — момент пропадания прошёл. А главное, обе руки заняты сканером, а не жгутом: шевелить проводку и одновременно смотреть в экран в одиночку невозможно.

Отсюда идея, из которой выросло всё остальное: мне нужен не сканер, а регистратор. Программа сидит в цикле, сама опрашивает блок, пищит в момент появления или исчезновения ошибки и пишет CSV с метками времени. Руки свободны, хронология остаётся на диске.

Первая версия называлась PDC_Diag, была консольной и занимала около 2400 строк. Сейчас это VAG Diag v5.1: около 6400 строк в app/ и tests/, веб-интерфейс, четыре типа адаптеров, тесты и CI. А по дороге нашлось ограничение, ради которого я и пишу статью.

UDS и ISO-TP: два абзаца для тех, кто не в теме

Диагностика блоков за пределами OBD-II идёт по протоколу UDS (Unified Diagnostic Services) поверх CAN: посылаешь блоку номер сервиса с параметрами, блок отвечает. 0x22 — прочитать данные по идентификатору, 0x19 02 — список ошибок, 0x14 — стереть, 0x10 — переключить сессию, 0x3E — «диагност ещё здесь». В программе ровно эти пять, 11-битные адреса; TP 2.0 я сознательно не реализовывал — на моей машине он не нужен.

Проблема в том, что CAN-кадр несёт максимум 8 байт данных, а список ошибок в них не помещается. Поверх CAN живёт транспортный уровень ISO-TP (ISO 15765-2). Короткий ответ уезжает одним кадром: первый байт — длина, дальше данные, хвост добивается заполнителем. Длинный ответ идёт многокадрово: блок шлёт первый кадр (First Frame), в заголовке которого лежит полная заявленная длина сообщения, и останавливается. Продолжать он не имеет права, пока получатель не пришлёт кадр разрешения на продолжение — Flow Control, начинающийся с байта 0x30. Только после него блок досылает остаток последовательными кадрами.

Пауза с ожиданием разрешения — то место, где всё ломается.

Главное: ELM327 не даёт разрешения на продолжение блокам кузова

Я направил первую рабочую версию на блок парковочной системы и получил странное. Запрос списка ошибок уходит, приходит ровно один кадр — и всё, дальше тишина. А следующий запрос блок отклоняет с кодом «занят» (0x21), и так примерно секунду, после чего оживает. Долго думал, что виноват мой код: переписывал разбор, менял таймауты, ставил задержки. Ничего. Тогда полез смотреть, что реально уходит на шину.

Картина сложилась такая. Прошивка ELM327 ведёт многокадровый обмен ISO-TP только с одной парой адресов — 0x7E0 для запроса и 0x7E8 для ответа. Это адреса блока управления двигателем, зашитые намертво. Увидев первый кадр от 0x7E8, адаптер честно отправляет туда кадр разрешения и дочитывает сообщение целиком.

Блоки кузовной электроники живут по другим адресам и, что важнее, с другим смещением между запросом и ответом: у меня запрос уходит на 0x70A, а ответ приходит с 0x774. Это не «запрос плюс восемь». Адаптер такой ответ видит и показывает — первый кадр отдаёт исправно. Но кадр разрешения в этот адрес не посылает: правило зашито.

Дальше всё механически: блок отдал первый кадр и встал в ожидание. Разрешения нет, блок держит незавершённую передачу и на любой новый запрос отвечает «занят», пока не сработает транспортный таймаут.

Проблема не в моём коде, не в машине и не в блоке. Проблема в 500-рублёвой железке, которая делает вид, что она универсальный интерфейс к CAN, а на самом деле это интерфейс к блоку двигателя с боковым режимом «покажу сырые кадры, дальше сам».

Что я пробовал и почему это не сработало

Дальше был месяц перебора — каждый вариант на живой машине, потому что теоретически ни один не проверяется. Результат отрицательный, и именно поэтому его стоит опубликовать.

Способ

Что произошло

ATCRA + ATFCSH + ATFCSD300000 + ATFCSM1 — задать адрес приёма и вручную настроить параметры кадра разрешения

Все четыре команды принимаются с OK. Продолжение всё равно не запрашивается. Настройки применяются только к «своей» паре адресов

ATCAF0 (отключить автоформатирование) и ручная отправка кадра разрешения

Адаптер приписывает свой служебный байт поверх нашего — на шину уходит не то, что мы отправили

ATAR (автоматический приём)

Заставляет ждать ответ строго по правилу «адрес запроса плюс восемь». Ответ с 0x774 на запрос 0x70A под это правило не подходит и просто отбрасывается

ATCRAXXX (маска «принимать любой адрес»)

Дешёвые клоны отвечают ? — команда не поддерживается прошивкой

Доказательство подмены кадра

Самое убедительное получилось случайно, когда я пытался отправить кадр разрешения вручную в режиме ATCAF0. В этом режиме байт длины формируешь сам. Отправляю 03 22 F1 97 — «длина 3, сервис 0x22, идентификатор F1 97». А блок ругается так, будто номер сервиса у него 0x03: он принял за сервис мой собственный счётчик длины. Значит, между моей строкой и шиной кто-то вставил впереди ещё байт — служебный, который адаптер дописывает сам.

Дальше арифметика простая. Кадр разрешения обязан начинаться с байта 0x30. Адаптер приписывает перед ним свой байт — и на шину уходит нечто, у чего первый полубайт не 3. Для блока это не Flow Control, а мусор: он молча его выбрасывает и продолжает ждать разрешения, которого не будет никогда.

После этого я перестал искать волшебную комбинацию AT-команд. Её нет.

Что удалось выжать без полного решения

Тут можно было бы написать «купите нормальное железо». Но у меня была реальная машина с плавающим контактом, и результат нужен был сегодня, а не после доставки. Инженерный компромисс: раз целиком список не читается, выжмем максимум из того, что приходит.

Первое: количество ошибок определяется точно. Полная заявленная длина лежит в заголовке первого кадра — того, который адаптер отдаёт исправно, — а каждая запись в ответе на 19 02 занимает фиксированное число байт. Зная длину, я знаю сколько ошибок в блоке, даже если не могу прочитать их все. Для регистратора это ровно то, что нужно: важна динамика счётчика, а не перечень.

Второе: первая запись приходит целиком. В первом кадре после заголовка хватает места на одну полную запись — значит, у меня есть код первой ошибки и её тип отказа.

Третье, неожиданно продуктивное: фильтрация по признаку меняет состав списка. Сервис 19 02 принимает маску состояния — блок возвращает только ошибки с совпадающими битами. Меняя маску, я меняю выборку, и первой оказывается разная ошибка. Прогоняя серию запросов с разными масками, вытаскиваю несколько кодов вместо одного.

# app/pdc_diag.py — выборка кодов по разным маскам состояния.
# Полный список не дочитывается, но при каждой маске первой
# в ответе оказывается другая запись. Собираем их в объединение.

STATUS_MASKS = ("FF", "01", "02", "08", "20", "04", "10", "40", "80")

def collect_dtcs_by_masks(elm, req_id, resp_id):
    found = {}
    for mask in STATUS_MASKS:
        frames = elm.request(req_id, resp_id, "1902" + mask)
        head = parse_first_frame(frames)
        if head is None:
            continue
        # заявленная длина -> точное число записей, даже если список оборван
        found["_count"] = (head.total_len - 3) // 4   # точное число ошибок
        rec = head.first_record()          # первая запись приходит целиком
        if rec:
            found[rec.code] = rec          # дубли схлопываются сами
        time.sleep(2.0)                    # иначе блок отвечает "занят" (0x21)
    return found

Пауза в две секунды — не перестраховка, а измеренная величина: меньше — и блок ещё держит незавершённую передачу.

Отдельно скажу: это костыль. Честный, документированный, работающий — но костыль. Чтения списка целиком он не заменяет, он позволяет закрыть конкретную задачу конкретным вечером.

Настоящее решение: USB-CAN по протоколу SLCAN

Правильный выход оказался и дешевле, и проще, чем я думал. Есть USB-CAN адаптеры, работающие по текстовому протоколу SLCAN — CANable, CANtact и совместимые, стоят примерно как приличный ELM327.

Принципиальная разница: SLCAN-адаптер не имеет логики транспортного уровня вообще. Он отдаёт сырые кадры шины как есть и отправляет сырые кадры, которые ему дали: никаких зашитых адресов, приписанных байтов и «умных» правил. Вся сборка ISO-TP, включая кадр разрешения, делается в моей программе.

И длинные списки читаются целиком, со всех блоков, с первого раза.

# app/pdc_slcan.py — приём многокадрового ответа с ручной отправкой
# кадра разрешения на продолжение (Flow Control).

FC_CONTINUE = bytes([0x30, 0x00, 0x00, 0, 0, 0, 0, 0])  # BS=0, STmin=0

def read_isotp(self, tx_id, rx_id, timeout=2.0):
    deadline = time.time() + timeout
    data, expected, next_sn = bytearray(), 0, 1

    while time.time() < deadline:
        frame = self._read_frame(rx_id)
        if frame is None:
            continue
        pci = frame[0] >> 4

        if pci == 0x1:                       # первый кадр
            expected = ((frame[0] & 0x0F) << 8) | frame[1]
            data += frame[2:]
            self._send_frame(tx_id, FC_CONTINUE)   # то, чего не делает ELM327
            continue

        if pci == 0x2:                       # последовательный кадр
            if (frame[0] & 0x0F) != next_sn:
                raise IsoTpError("нарушен порядок кадров")
            next_sn = (next_sn + 1) & 0x0F
            data += frame[1:]
            if len(data) >= expected:
                return bytes(data[:expected])

    raise IsoTpError("ответ оборван: получено %d из %d байт" % (len(data), expected))

Восемь строк логики — ровно то, чего не хватает в прошивке за 500 рублей. Обратная сторона, сборка одиночного запроса:

# app/pdc_core.py — одиночный кадр ISO-TP: байт длины впереди,
# хвост добивается заполнителем до восьми байт.

PAD = 0xAA   # заполнитель; блок его игнорирует

def build_single_frame(payload: bytes) -> bytes:
    if len(payload) > 7:
        raise ValueError("не помещается в одиночный кадр")
    frame = bytes([len(payload)]) + payload
    return frame + bytes([PAD]) * (8 - len(frame))

# build_single_frame(b"\x22\xF1\x97")  ->  03 22 F1 97 AA AA AA AA

Именно этот байт длины 0x03 блок и принимал за номер сервиса, когда ELM327 приписывал перед ним свой байт.

И детектор обрыва, общий для любого транспорта:

# app/pdc_core.py — определить, что длинный ответ оборван,
# и всё равно вытащить заявленную длину.

def analyse_response(frames):
    """Возвращает (данные, полные_ли_данные, заявленная_длина)."""
    if not frames:
        return b"", False, 0

    if (frames[0][0] >> 4) != 0x1:                  # короткий ответ
        n = frames[0][0] & 0x0F
        return frames[0][1:1 + n], True, n

    total = ((frames[0][0] & 0x0F) << 8) | frames[0][1]   # заявленная длина
    data = bytearray(frames[0][2:])
    for f in frames[1:]:
        data += f[1:]

    complete = len(data) >= total
    if not complete:
        log.warning("длинный ответ оборван: %d из %d байт — "
                    "адаптер не прислал кадр разрешения", len(data), total)
    return bytes(data[:total]), complete, total

Программа не притворяется, что всё хорошо: в отчёт идёт, сколько байт получено из скольких заявленных.

Приём, который спас архитектуру: новый интерфейс притворяется старым

К моменту, когда я добрался до SLCAN, программа уже работала с ELM327 по Wi-Fi, USB и Bluetooth. Добавление четвёртого, принципиально другого типа адаптера должно было означать переписывание половины кода. Не означало — благодаря приёму, применённому дважды.

Новый способ связи повторяет методы старого, и код выше по стеку не меняется вовсе.

Первый случай. Изначально ELM327 подключался по Wi-Fi, то есть через сетевой сокет: класс Elm327 вызывал sendall, recv, settimeout, close. Когда понадобился USB-адаптер, я не стал добавлять ветвления «если COM-порт, то иначе», а написал класс SerialChannel, повторяющий ровно эти четыре метода.

# app/pdc_serial.py — COM-порт, притворяющийся сетевым сокетом.

class SerialChannel:
    """Повторяет интерфейс socket: sendall / recv / settimeout / close."""

    def __init__(self, port, baudrate=38400, timeout=5.0):
        self._ser = _open_serial(port, baudrate, timeout)

    def sendall(self, data: bytes) -> None:
        self._ser.write(data)
        self._ser.flush()

    def recv(self, bufsize: int = 4096) -> bytes:
        return self._ser.read(bufsize) or b""

    def settimeout(self, value) -> None:
        self._ser.timeout = value

    def close(self) -> None:
        self._ser.close()


# Подмена в одну строку. Класс Elm327 не знает, что работает не по сети.
elm = Elm327()
elm.sock = SerialChannel("COM3", baudrate=38400)   # вместо socket.create_connection(...)
elm.init()                                          # дальше всё как обычно

Ни одной правки в Elm327. Bluetooth приехал бесплатно: в Windows он монтируется как виртуальный COM-порт, то есть это тот же SerialChannel с другим именем.

Второй случай — тот же приём этажом выше. SlcanAdapter повторяет методы Elm327: cmd, set_header, accept_all, identify. Его cmd() принимает запрос в том же виде и возвращает ответ строками того же текстового формата, раскладывая собранные данные обратно в кадры. Внутри работает совсем другой протокол с собственной сборкой ISO-TP, наружу торчит привычный интерфейс.

Весь прикладной разбор — расшифровка кодов, сессии, монитор, экспорт в CSV — остался общим. Итог: четыре типа адаптера и ни одной правки в прикладном коде.

Соблазн был обратный: «правильная» абстракция транспорта с базовым классом, реестром и фабрикой. Хорошо, что не стал — повторение существующего интерфейса оказалось короче и надёжнее, потому что не требовало трогать работающий код.

Семь вещей, которые невозможно было предугадать

Ниже — таблица из документации проекта. Каждая строка появилась не потому, что я так спроектировал, а потому что что-то сломалось на живой машине и я потратил вечер на причину. Такие решения выглядят костылями, пока не знаешь их историю.

Место

Почему так

Чистка буфера перед каждой командой

Иначе остаток ответа одного блока читается как ответ следующего, и имена блоков в списке перемешиваются между собой

Минимум служебных команд перед запросом

Дешёвый адаптер захлёбывается: после десятка AT-команд подряд начинает возвращать пустоту вместо ответов

Снятие фильтра через ATCF000 + ATCM000

ATAR заставляет ждать ответ по правилу «запрос плюс восемь», а ATCRAXXX клоны просто не понимают

Повтор при ответе «занят» (0x21)

Блок остаётся занят примерно секунду после незавершённой длинной передачи

Пауза 2 секунды между выборками ошибок

По той же причине — иначе вся серия масок вернёт 0x21

Заявленная длина берётся из заголовка первого кадра

Позволяет знать точное число ошибок, даже когда список не дочитан

Расширенная сессия перед стиранием

Без переключения сессии сервисом 0x10 блок отвечает отказом на команду стирания

Отслеживание ответа ?

Непонятая адаптером команда молча не выполняется, и дальше программа работает на неверных допущениях

Дороже всех обошлась последняя строка. Символ ? означает «команда не поддержана»: ничего не падает, ничего не логируется, работа продолжается — просто фильтр, который ты считал поставленным, не поставлен. Два вечера на «плавающий баг», оказавшийся молча проигнорированной командой.

Сверх таблицы есть ещё один механизм: программа считает подряд идущие пустые ответы и, если их больше порога, сама переинициализирует адаптерATZ и вся стартовая последовательность заново. Дешёвые клоны иногда уходят в себя, и единственное лечение — сброс; раньше я делал это, дёргая питание.

Сама последовательность специально короткая: ATZ, ATE0 (выключить эхо), ATSP6 (ISO 15765-4, CAN 11 бит, 500 кбит/с), ATCAF1. Всё. Каждая лишняя команда — риск получить пустоту вместо ответа.

Ещё программа смотрит напряжение бортсети: ниже 11,5 В блоки начнут выдавать ложные ошибки, выше 13,3 В — двигатель запущен, лучше заглушить. Тоже из опыта: половину первого вечера я гонялся за ошибками, которых не было, потому что сел аккумулятор.

Заглушка и тесты: как отлаживать автомобиль на столе

Ездить к машине ради каждой правки — плохая обратная связь. Поэтому я написал mock_elm327.py (313 строк) — заглушку, изображающую автомобиль по сети: отвечает как ELM327 на AT-команды, изображает блок парковочной системы с ошибками, отвечает на стандартные запросы OBD-II, а с ключом --glitch периодически «теряет» датчик, чтобы проверить логику регистратора.

По-настоящему ценной она стала после одной доработки. Заглушка изображает блок кузовной электроники, который отдаёт длинный ответ только после кадра разрешения. Главная особенность настоящих блоков — и главная проблема ELM327 — воспроизводится прямо на столе. С этого момента я отлаживал и детектор обрыва, и SLCAN-сборку, и перебор масок, не выходя из дома.

Поверх заглушки живёт tests/test_smoke.py — 151 строка, 27 проверок, все проходят: разбор кадров ISO-TP, обнаружение обрыва длинного ответа, чтение заявленной длины из заголовка, сборка многокадрового ответа, расшифровка кодов и типов отказа, полный обмен с заглушкой от инициализации до чтения списка.

GitHub Actions гоняет compileall и эти тесты на Python 3.8, 3.11 и 3.12 при каждом push и pull request. Матрица не для галочки: 3.8 в ней потому, что программа должна запускаться на старом гаражном ноутбуке.

Как я читаю код ошибки: это три вещи, а не одна

Возвращаюсь к тому, ради чего всё затевалось. Большинство приложений показывает код ошибки одной строкой — «B1234». Это треть информации: в ответе 19 02 каждая запись содержит три вещи — номер кода, байт типа отказа и байт состояния. И два последних для поиска дефекта важнее первого.

Байт типа отказа говорит, что блок увидел электрически. Это прямое указание, где искать:

Тип отказа

Где искать

Обрыв цепи

Прозванивать жилу от разъёма блока до разъёма датчика

Замыкание на массу

Проверять изоляцию по всей длине участка

Замыкание на плюс

Искать протёртую изоляцию рядом с силовой цепью

Нет сообщений от узла

Проверять питание и массу самого узла, а не сигнальную линию

Сигнал нестабилен

Искать шевелением жгута — статические замеры ничего не покажут

Байт состояния говорит, живой дефект или исторический, и определяет метод поиска:

Состояние

Как искать

Активна сейчас

Дефект присутствует в момент опроса — искать замерами, мультиметр покажет

Была ранее

Плавающий дефект, сейчас цепь в норме — искать только шевелением жгута под наблюдением

Повторю, потому что это главный практический вывод: состояние важнее номера кода. Номер говорит, какая цепь; состояние — каким инструментом её искать. Гоняться мультиметром за ошибкой в состоянии «была ранее» — гарантированно потерянный вечер.

Как отделить живой дефект от исторического

Когда я впервые прочитал блок парктроника, там лежало восемь ошибок. Половина — следы старого удара в бампер, которые никто не стирал годами: к моей проблеме отношения не имели, только путали.

Методика отделения простая:

  1. Прочитать список и сохранить снимок.

  2. Стереть все ошибки.

  3. Цикл зажигания: выключить, подождать, включить.

  4. Включить заднюю передачу и подержать 30 секунд.

  5. Прочитать список снова.

Вернувшиеся ошибки актуальны. Остальные были историей. У меня из восьми вернулась одна, и задача сузилась с «непонятно что» до «конкретный канал конкретного датчика».

Пункт про заднюю передачу не формальность: без неё блок парковочной системы датчики не опрашивает вообще.

Режим «Монитор» и поиск неисправного датчика за десять минут

Оставался вопрос: какой из восьми датчиков. Штатный ответ — снимать бампер и проверять каждый. Я нашёл способ не снимать.

В программе есть режим «Монитор»: живой счётчик активных ошибок, обновляющийся в цикле. Он работает на заявленной длине из заголовка, то есть точен даже там, где список не дочитывается.

  1. Включить заднюю передачу (без неё опроса нет).

  2. Отключить все датчики от жгута.

  3. Запустить монитор — счётчик показывает восемь.

  4. Подключать датчики по одному, ожидая 10–15 секунд после каждого.

Счётчик уменьшился — канал исправен. Не сдвинулся — вот дефектный канал. Десять минут, бампер на месте, обе руки свободны.

Чем всё кончилось с проводкой

Монитор указал на конкретный датчик, а тип отказа показывал «сигнал нестабилен» — значит, статический замер бесполезен, надо шевелить.

Я запустил регистратор с писком, взял жгут в руки и пошёл вдоль него. Пищать начало возле кузовного проёма, где жгут был передавлен. Разобрал гофру — жила с надломленной медью: изоляция целая, проводник переломлен почти полностью и контактирует только в определённом положении.

Перепаял участок, термоусадка, нормальная фиксация. Стёр ошибки, цикл зажигания, задняя передача, чтение. Код не вернулся — и не вернулся через месяц.

От «начал искать» до «нашёл» — минут сорок, из которых тридцать пять ушло на снятие обшивки. Само обнаружение заняло минуты три: руки были на жгуте, а не на экране.

Что ещё появилось в программе

Пока я гонялся за одним проводом, программа обросла тем, что понадобилось по дороге:

  • Веб-интерфейс на локальном сервере (127.0.0.1:8765): браузер как оболочка, GUI-фреймворк не нужен, работает и с телефона в том же Wi-Fi.

  • Универсальный раздел OBD-II — на любом автомобиле с 2001 года: коды, готовность систем самодиагностики, VIN, стоп-кадр.

  • Живые параметры с автоопределением поддерживаемых и записью в CSV.

  • Мастер поиска датчика — формализация протокола «отключил — записал»: строит карту «код ↔ датчик» экспериментально, без справочников с распиновкой, которых для многих блоков в открытом доступе нет.

  • Сравнение снимков до и после ремонта: что ушло, что осталось, что появилось.

  • Тест аккумулятора и генератора по графику напряжения при пуске и офлайн-словарь стандартных кодов.

  • Отчёт REPORT_TO_SEND.txt с полным журналом обмена — один файл, который прикладывается к вопросу на форуме, чтобы отвечающему не пришлось выпытывать подробности по одной.

Рамки специально жёсткие: ноль внешних зависимостей, только стандартная библиотека Python. Архив должен работать сразу после распаковки, вместе с переносимым интерпретатором внутри: в гараже нет ни интернета, ни желания разбираться с pip. Лицензия MIT.

По объёму: в репозитории около 7450 строк, из них код и разметка в app/ и tests/ — около 6380. pdc_diag.py — 1660, webui.py — 1025, ui.html — 985, pdc_core.py — 975, menu.py — 438, mock_elm327.py — 313, pdc_slcan.py — 231, pdc_obd.py — 218, pdc_serial.py — 216, pdc_codes.py — 168, test_smoke.py — 151.

Про LLM-ассистента, коротко и без рекламы

Обвязку писал ассистент: разбор аргументов, работа с CSV, HTML-страница интерфейса, значительная часть тестов. Это ускорило рутину и позволило не тратить внимание на то, в чём нет инженерного содержания.

Стандарт читал человек. ISO 15765-2 и описание сервисов UDS я разбирал сам — иначе было не понять, что вообще происходит с многокадровым обменом. А ограничение ELM327 нашлось только измерением на живой машине. Ассистент уверенно предлагал комбинации AT-команд, которые «должны работать», включая всю четвёрку ATCRA / ATFCSH / ATFCSD / ATFCSM из таблицы выше. Они не работали. Не потому, что ассистент врал: он честно пересказывал документацию, а документация описывает, как задумано, а не как сделано в конкретном клоне. Разница между этими двумя вещами и есть содержание статьи.

Что бы я сделал иначе

Самокритика по пунктам, потому что ошибок было достаточно.

Купил бы SLCAN-адаптер сразу. Он стоит как ELM327. Я потратил месяц вечеров, заставляя ELM327 делать то, чего он не умеет, вместо того чтобы за неделю проверить гипотезу «дело в железе». Стоимость эксперимента — двести рублей, стоимость упрямства — месяц.

Написал бы заглушку первой, а не пятой. День работы, окупившийся двадцатикратно, я откладывал до момента, когда ездить к машине стало совсем невыносимо.

Не делал бы веб-интерфейс так рано. webui.py на 1025 строк плюс ui.html на 985 — треть проекта. Консольная версия закрывала мою задачу полностью, а веб-интерфейс нужен, чтобы программой мог пользоваться кто-то ещё; делать его до появления такого человека было рано.

Логировал бы сырой обмен с первого дня. Полный журнал я добавил, только когда упёрся в загадку с 0x21. Будь он с начала, ограничение обнаружилось бы недели на две раньше: там всё видно, надо было только смотреть.

Чего бы не менял: отказа от внешних зависимостей и приёма с подменой интерфейса — оба окупились полностью.

Выводы

Три вещи, которые я хотел бы прочитать до начала.

Первое. Дешёвый ELM327 — не универсальный интерфейс к CAN, а интерфейс к блоку двигателя с ограниченным режимом просмотра сырых кадров. Многокадровый обмен ISO-TP он ведёт только с парой 0x7E0 / 0x7E8, и никакой комбинацией AT-команд это не расширяется: это не настройка, а прошивка.

Второе. Упёрся в стену — проверь, не врёт ли инструмент измерения. Я месяц искал ошибку в своём коде, в машине и в блоке, прежде чем посмотрел, что реально уходит на шину. Байт длины, принятый блоком за номер сервиса, был виден с самого начала — я просто не смотрел.

Третье. Отрицательный результат стоит публиковать. Таблица из четырёх неработающих обходов — самое полезное здесь: она экономит следующему тот же месяц.

Программа лежит на GitHub под лицензией MIT: https://github.com/gdm0991/vagdiag. Ставить ничего не нужно — переносимый Python внутри архива.

Что действительно пригодилось бы — отчёты REPORT_TO_SEND.txt с других автомобилей: полный журнал обмена, какие адреса откликнулись, как назвались блоки, что ответили. Из этого складывается справочник «модель — адреса блоков», которого в открытом виде не существует. Каждый такой файл — одна машина, которую следующему не придётся перебирать вслепую.

И вопрос, ради которого я, честно говоря, и пишу: встречался ли кому-нибудь ELM327, который умеет дочитывать длинные ответы от блоков кузова — то есть сам шлёт кадр разрешения на адрес, не связанный с запросом правилом «плюс восемь»? Назовите модель и версию прошивки. Я перебрал всё, что было под рукой, и не нашёл ни одного. Буду рад ошибиться.

Комментарии (0)