16 KiB
Анализ динамического сбоя ELM327
Дата: 2026-06-14
Контекст
Проверено:
- статическая диагностика работает стабильно;
- чтение VIN работает;
- чтение DTC работает;
- последовательное чтение нескольких PID в статическом режиме работает;
- ELM327 выдерживает не менее 9 PID подряд в статике;
- увеличение пауз до 4000 мс не устраняет проблему;
- автоподбор таймингов не устраняет проблему;
- сбой проявляется только в динамическом сборе данных через ScriptEngine.
Это сильно сужает пространство причин. Проблема почти наверняка не в "скорости ELM вообще", не в "ECU не успевает" и не в банальном "надо ещё увеличить задержку".
Что объясняет факты лучше всего
1. Несовпадение между тем, как ElmChecker и ScriptEngine общаются с ELM
Вероятность: высокая.
Смысл гипотезы: статический путь и динамический путь используют не одинаковую коммуникационную последовательность. Ломается не ELM как таковой, а конкретный сценарий: кто пишет, кто читает, когда читают, что считается окончанием ответа, как очищается буфер, как меняются состояния.
Что подтверждает:
- статический путь работает полностью;
- динамический ломается только в ScriptEngine;
- один и тот же адаптер выдерживает длинную серию PID в статике;
- в проектных заметках уже зафиксировано, что проблема может быть именно в различии между ElmChecker и ScriptEngine, а не в тайминге как таковом.
Что противоречит:
- если в удачном и неудачном сценарии полностью совпадают AT-команды, порядок команд, чтение и ожидание конца ответа.
Быстрый эксперимент:
- снять полный лог команд и сырых ответов для успешного статического пути и для динамического пути;
- сравнить именно последовательность TX/RX, а не распарсенные значения;
- проверить, расходится ли сценарий уже до первого PID.
2. InputStream не дочитывается до символа ">", и следующий запрос попадает в хвост прошлого ответа
Вероятность: высокая.
Смысл гипотезы: динамический читатель завершает чтение раньше, чем ELM реально закончил ответ. В буфере остаётся промпт > или другой хвост, и следующий запрос читает не чистый ответ, а остаток прошлого цикла.
Что подтверждает:
- это прямо совпадает с типовым режимом отказа ELM327;
- в проектных заметках символ
>отдельно выделен как конец ответа; - в обсуждениях проекта уже встречалась версия, что ответы смешиваются и хвост остаётся в буфере;
- статика может это маскировать, потому что между командами там больше естественных пауз.
Что противоречит:
- если сырые логи показывают, что каждый ответ полностью доходит до
>и следующий запрос стартует только после этого; - если после сбоя буфер точно пуст.
Быстрый эксперимент:
- включить сырой дамп RX/TX без парсинга;
- для первого сбойного цикла проверить, присутствует ли
>в сыром потоке полностью; - перед следующим запросом проверить, не остаётся ли в InputStream ничего, кроме уже считанного ответа.
3. Остатки данных в буфере ломают синхронизацию между командами
Вероятность: высокая.
Смысл гипотезы: чтение и запись идут корректно по отдельности, но между ними нет надёжной очистки буфера. В результате следующий запрос потребляет не только свежий ответ, но и мусор: эхо, переносы строк, старые байты, задержавшийся ответ предыдущего PID.
Что подтверждает:
- проектные заметки отдельно говорят, что drainInput раньше был механизмом синхронизации запрос/ответ;
- после отключения drainInput в одной из версий стало хуже;
- описан эффект сдвига: ответ на команду N прочитан как ответ на N+1;
- статический сценарий выдерживает это лучше из-за более редкой частоты обращений.
Что противоречит:
- если перед каждым запросом в динамическом цикле буфер гарантированно очищается и при этом проблема остаётся;
- если в логах нет признаков мусора, эха или сдвига границ ответов.
Быстрый эксперимент:
- один раз до старта динамики и один раз перед вторым запросом вывести количество доступных байт в InputStream и содержимое остатка;
- сравнить результат между успешным статическим и неудачным динамическим прогоном;
- проверить, есть ли хвосты после первого ответа.
4. ScriptEngine выполняет не тот же state machine, что ElmChecker
Вероятность: средняя.
Смысл гипотезы: проблема не в самом Bluetooth и не в самом ELM, а в том, что динамический движок переходит между состояниями раньше или иначе, чем ElmChecker. Например, команда считается завершённой по временному признаку, а не по фактическому окончанию ответа.
Что подтверждает:
- пользователь отдельно выделил риск state machine;
- динамический режим содержит свои шаги, цикл и внутренние переходы;
- статический путь короче и проще, поэтому ошибки state machine там могут не проявляться;
- уже были замечания, что в таких сценариях рассинхрон появляется раньше, чем кажется.
Что противоречит:
- если логически и по времени state transitions происходят только после полного ответа ELM;
- если обе машины выполняют одинаковый сценарий завершения команды.
Быстрый эксперимент:
- на одном прогоне логировать каждое состояние до и после отправки команды;
- отметить момент, когда реально получен
>; - проверить, не уходит ли ScriptEngine в следующий шаг до фактического конца ответа.
5. Доступ к одному сокету или одному InputStream идёт из двух потоков
Вероятность: средняя.
Смысл гипотезы: чтение или запись в динамике пересекаются с другим потоком, который тоже читает или пишет тот же канал. Для ELM это критично: поток байтов становится недетерминированным, и команда может лишиться части ответа.
Что подтверждает:
- пользователь отдельно попросил проверить конкурентный доступ к сокету;
- динамический режим по определению более многопоточен: цикл, сбор данных, UI, возможные фоновые операции;
- статический путь может не задевать гонку из-за более редкой частоты и меньшего числа активных операций.
Что противоречит:
- если трасса покажет строго одного читателя и одного писателя на весь жизненный цикл соединения;
- если динамика воспроизводится даже в полностью однопоточном прогоне.
Быстрый эксперимент:
- вывести thread id для каждого read и write;
- проверить, нет ли второго consumer на InputStream;
- сравнить идентичность владельца сокета в статике и динамике.
6. Неправильная последовательность команд, а не неправильная пауза
Вероятность: средняя-низкая.
Смысл гипотезы: дело не в длительности ожидания как таковой, а в том, что динамический сценарий отправляет команды в другом порядке или с другим набором служебных AT-команд, чем успешный статический сценарий. Тогда ELM оказывается в другом режиме, и дальнейшая обработка ломается.
Что подтверждает:
- в проекте есть отдельные сценарии для статической диагностики, тестового скрипта и динамики;
- серверный build_test_script и build_dynamic_script действительно строят разные последовательности;
- в заметках по ELM отдельно обсуждаются последствия ATWS, ATE0/ATL0/ATS0 и различий в инит-последовательности.
Что противоречит:
- если сравнение трасс покажет полностью одинаковый init и только разный темп;
- если тот же набор команд в статике и динамике повторяет поломку только из-за способа выполнения, а не порядка.
Быстрый эксперимент:
- распечатать полный список команд, которые реально уходят в ELM в обоих режимах;
- сравнить not only PID, но и все AT-команды, входы в state machine и возможные reset-команды;
- проверить, совпадает ли стартовая инициализация побайтно.
Что менее вероятно
Adaptive timing как первопричина
Вероятность: низкая.
Почему низкая:
- уже проверяли увеличение пауз до 4000 мс;
- уже проверяли автоподбор;
- одиночные запросы и статический набор PID работают.
Вывод: adaptive timing может усиливать или маскировать проблему, но не выглядит корнем сбоя.
Просто "мало ждать"
Вероятность: низкая.
Почему низкая:
- паузы уже увеличивали;
- первый запрос проходит, второй ломается;
- для обычного ELM327 это больше похоже на ошибку синхронизации, чем на нехватку миллисекунд.
Итоговая интерпретация
Новое мнение хорошо согласуется с уже собранными фактами. Оно сдвигает фокус с "таймингов вообще" на более узкий класс проблем:
- границы ответа ELM, особенно символ
>; - остатки в InputStream;
- различие между ElmChecker и ScriptEngine;
- state machine, которая может идти вперёд раньше времени;
- возможная конкуренция за сокет или поток чтения.
Главный вывод: если статический путь стабилен, а динамический ломается даже при больших паузах, то первичная причина почти наверняка находится не в задержках, а в чтении потока, границах ответа и разнице в сценарии исполнения.
Порядок расследования
- Снять сырой RX/TX лог без парсинга для статического и динамического режима.
- Проверить, доходит ли каждый ответ до
>и не остаются ли байты в буфере перед следующим запросом. - Сопоставить полную последовательность команд ElmChecker и ScriptEngine.
- Подтвердить или опровергнуть второй consumer на сокете/InputStream.
- Проверить, не идёт ли state machine вперёд до фактического завершения ответа.
Краткий вывод
Наиболее правдоподобно, что проблема не в скорости ELM, а в том, как динамический сценарий читает и синхронизирует поток ответов. Внутри этого класса причин самые сильные кандидаты: неполное дочитывание до >, остатки в InputStream, и различие между ElmChecker и ScriptEngine.