209 lines
16 KiB
Markdown
209 lines
16 KiB
Markdown
# Анализ динамического сбоя 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, которая может идти вперёд раньше времени;
|
||
- возможная конкуренция за сокет или поток чтения.
|
||
|
||
Главный вывод: если статический путь стабилен, а динамический ломается даже при больших паузах, то первичная причина почти наверняка находится не в задержках, а в чтении потока, границах ответа и разнице в сценарии исполнения.
|
||
|
||
## Порядок расследования
|
||
|
||
1. Снять сырой RX/TX лог без парсинга для статического и динамического режима.
|
||
2. Проверить, доходит ли каждый ответ до `>` и не остаются ли байты в буфере перед следующим запросом.
|
||
3. Сопоставить полную последовательность команд ElmChecker и ScriptEngine.
|
||
4. Подтвердить или опровергнуть второй consumer на сокете/InputStream.
|
||
5. Проверить, не идёт ли state machine вперёд до фактического завершения ответа.
|
||
|
||
## Краткий вывод
|
||
|
||
Наиболее правдоподобно, что проблема не в скорости ELM, а в том, как динамический сценарий читает и синхронизирует поток ответов. Внутри этого класса причин самые сильные кандидаты: неполное дочитывание до `>`, остатки в InputStream, и различие между ElmChecker и ScriptEngine. |