Меня зовут Груздев Дмитрий Михайлович, я инженер АСУТП. Промышленная автоматизация — это измерительные каналы, полевые шины и возня с тем, что «должно работать по стандарту», а по факту работает как получилось у производителя железки. Эта статья — про то, как привычка проверять всё измерением помогла найти конкретное ограничение в самом массовом диагностическом адаптере.
Завязка: плавающий контакт, который не ловится сканером
У меня 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, а на самом деле это интерфейс к блоку двигателя с боковым режимом «покажу сырые кадры, дальше сам».
Что я пробовал и почему это не сработало
Дальше был месяц перебора — каждый вариант на живой машине, потому что теоретически ни один не проверяется. Результат отрицательный, и именно поэтому его стоит опубликовать.
|
Способ |
Что произошло |
|---|---|
|
|
Все четыре команды принимаются с |
|
|
Адаптер приписывает свой служебный байт поверх нашего — на шину уходит не то, что мы отправили |
|
|
Заставляет ждать ответ строго по правилу «адрес запроса плюс восемь». Ответ с |
|
|
Дешёвые клоны отвечают |
Доказательство подмены кадра
Самое убедительное получилось случайно, когда я пытался отправить кадр разрешения вручную в режиме 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=0def 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-команд подряд начинает возвращать пустоту вместо ответов |
|
Снятие фильтра через |
|
|
Повтор при ответе «занят» ( |
Блок остаётся занят примерно секунду после незавершённой длинной передачи |
|
Пауза 2 секунды между выборками ошибок |
По той же причине — иначе вся серия масок вернёт |
|
Заявленная длина берётся из заголовка первого кадра |
Позволяет знать точное число ошибок, даже когда список не дочитан |
|
Расширенная сессия перед стиранием |
Без переключения сессии сервисом |
|
Отслеживание ответа |
Непонятая адаптером команда молча не выполняется, и дальше программа работает на неверных допущениях |
Дороже всех обошлась последняя строка. Символ ? означает «команда не поддержана»: ничего не падает, ничего не логируется, работа продолжается — просто фильтр, который ты считал поставленным, не поставлен. Два вечера на «плавающий баг», оказавшийся молча проигнорированной командой.
Сверх таблицы есть ещё один механизм: программа считает подряд идущие пустые ответы и, если их больше порога, сама переинициализирует адаптер — 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 каждая запись содержит три вещи — номер кода, байт типа отказа и байт состояния. И два последних для поиска дефекта важнее первого.
Байт типа отказа говорит, что блок увидел электрически. Это прямое указание, где искать:
|
Тип отказа |
Где искать |
|---|---|
|
Обрыв цепи |
Прозванивать жилу от разъёма блока до разъёма датчика |
|
Замыкание на массу |
Проверять изоляцию по всей длине участка |
|
Замыкание на плюс |
Искать протёртую изоляцию рядом с силовой цепью |
|
Нет сообщений от узла |
Проверять питание и массу самого узла, а не сигнальную линию |
|
Сигнал нестабилен |
Искать шевелением жгута — статические замеры ничего не покажут |
Байт состояния говорит, живой дефект или исторический, и определяет метод поиска:
|
Состояние |
Как искать |
|---|---|
|
Активна сейчас |
Дефект присутствует в момент опроса — искать замерами, мультиметр покажет |
|
Была ранее |
Плавающий дефект, сейчас цепь в норме — искать только шевелением жгута под наблюдением |
Повторю, потому что это главный практический вывод: состояние важнее номера кода. Номер говорит, какая цепь; состояние — каким инструментом её искать. Гоняться мультиметром за ошибкой в состоянии «была ранее» — гарантированно потерянный вечер.
Как отделить живой дефект от исторического
Когда я впервые прочитал блок парктроника, там лежало восемь ошибок. Половина — следы старого удара в бампер, которые никто не стирал годами: к моей проблеме отношения не имели, только путали.
Методика отделения простая:
-
Прочитать список и сохранить снимок.
-
Стереть все ошибки.
-
Цикл зажигания: выключить, подождать, включить.
-
Включить заднюю передачу и подержать 30 секунд.
-
Прочитать список снова.
Вернувшиеся ошибки актуальны. Остальные были историей. У меня из восьми вернулась одна, и задача сузилась с «непонятно что» до «конкретный канал конкретного датчика».
Пункт про заднюю передачу не формальность: без неё блок парковочной системы датчики не опрашивает вообще.
Режим «Монитор» и поиск неисправного датчика за десять минут
Оставался вопрос: какой из восьми датчиков. Штатный ответ — снимать бампер и проверять каждый. Я нашёл способ не снимать.
В программе есть режим «Монитор»: живой счётчик активных ошибок, обновляющийся в цикле. Он работает на заявленной длине из заголовка, то есть точен даже там, где список не дочитывается.
-
Включить заднюю передачу (без неё опроса нет).
-
Отключить все датчики от жгута.
-
Запустить монитор — счётчик показывает восемь.
-
Подключать датчики по одному, ожидая 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, который умеет дочитывать длинные ответы от блоков кузова — то есть сам шлёт кадр разрешения на адрес, не связанный с запросом правилом «плюс восемь»? Назовите модель и версию прошивки. Я перебрал всё, что было под рукой, и не нашёл ни одного. Буду рад ошибиться.
ссылка на оригинал статьи https://habr.com/ru/articles/1072434/