Дружим BeamNG и частичку Гранты

от автора

Видео для наглядности:

Автор 3D-модели: Kivvich BeamNG Workspace

Дисклеймер: взаимодействие с блоками управления автомобиля без должного опыта и осторожности может повлечь за собой печальные последствия.

Как это работает?

Ключевым компонентом системы является протокол OutGauge, специально разработанный для подключения различных приборов и средств индикации к автосимуляторам.

Слева направо: в состав BeamNG входит специальный скрипт на Lua outgauge.lua, который упаковывает определенные переменные в структуру и отправляет их с помощью UDP по адресу 127.0.0.1:4444. Там эти пакеты принимает и распаковывает сделанный на коленке скрипт на Python, преобразовывает в соответствующие CAN-сообщения и отправляет их блоку панели приборов.

Для простоты проекта никакого железа кроме ПК, CAN-интерфейса и самой панели не используется. У такого решения есть свои недостатки, самый главный — это отсутствие возможности управлять указателем уровня топлива, так как он аналоговый и построен на измерении сопротивления. Такой подход практикуется на большинстве автомобилей и выставить уровень топлива с помощью CAN-сообщений не получится.

Естественно, используемая панель приборов должна быть относительно современной, с поддержкой CAN-шины и минимумом аналоговых/импульсных входов. Для безумных экспериментов желательно приобрести ее на разборке, а не выдирать из собственного автомобиля.

Чтобы панель понимала сообщения, надо говорить с ней на языке ее автомобиля. Для упрощения перевода применяют специальные текстовые DBC-файлы, описывающие три основных сущности CAN-шины:

  • узлы, подключенные к шине (блоки управления, диагностические приборы и т.д.);

  • сообщения, которые эти узлы передают;

  • сигналы, которые содержатся в этих сообщениях.

Для полноценной работы скрипта на Python потребуется библиотека cantools для работы с DBC-файлами и библиотека python-can, умеющая взаимодействовать с кучей различных CAN-интерфейсов.

Важным инструментом, облегчающим обратный инжиниринг CAN-шины автомобиля, выступит ПО SavvyCAN. Вот неполный список того, что оно умеет:

  • писать логи;

  • просматривать и редактировать DBC-файлы;

  • интерпретировать поток сообщений в сигналы, основываясь на DBC-файлах;

  • строить графики;

  • работать с UDS-командами;

  • производить фаззинг.

Для просмотра и редактирования DBC-файлов может пригодится бесплатная версия CANdb++ от Vector.

Чтобы описанное в статье было проще воспринять и повторить самому, она была разбита на два уровня: базовый, в котором мы будем учить панель отображать только самую необходимую информацию, и углубленный, в котором постараемся выжать из панели максимум, пустив в ход изменение ее конфигурации и редактирование Lua-скрипта BeamNG.

Базовый уровень

Список действий для базового уровня:

  1. погасить все незадействованные лампочки (чтобы они не отвлекали во время игры) с помощью CAN-сигналов;

  2. выводить только значения оборотов, скорости и температуры двигателя на панель;

  3. оставить Lua-скрипт BeamNG первозданном виде;

  4. оставить конфигурацию панели в первозданном виде.

Структура CAN-шины автомобиля

Этот шаг не является обязательным, но упрощает поиск сообщений/сигналов и помогает лучше понять устройство автомобиля.

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

В данном конкретном случае, в роли подопытного выступает Lada Granta FL 2021 в комплектации «Комфорт», с двигателем ВАЗ-21127 и механической коробкой передач. Оснащение не самое навороченное, поэтому и схема получается небольшой:

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

Всего 6 блоков, подключенных к одной-единственной шине:

  1. ECM (Engine Control Module) — блок управления двигателем;

  2. SRS (Supplemental Restraint System) — блок подушек и преднатяжителей ремней безопасности;

  3. ABS (Anti-lock Braking System) — блок управления антиблокировочной системой;

  4. Instrument Cluster — панель приборов;

  5. Head Unit — головное устройство;

  6. ERA — блок системы Экстренного Реагирования при Авариях.

Отдельно можно упомянуть блок центрального замка, но он к шине не подключается и существует сам по себе.

Сбор и анализ логов

Самый простой способ подключения к CAN-шине — через диагностический разъем. Ситуация может быть чуть сложнее, если на автомобиле установлен гейтвей, который фильтрует трафик и не пропускает сообщения. Тогда придется подключаться к шинам за гейтвеем и записывать трафик до фильтрации. При этом важно понимать, какая шина за какой домен сети отвечает.

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

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

Обнаруженные сигналы вносятся в DBC-файл, который будет использоваться далее. Кстати, возможно вам повезло и CAN-шина вашего автомобиля уже описана в проекте opendbc.

Базовые подсказки, которые помогут ускорить поиски:

  • Начинать анализ желательно с поиска того, какой блок какие сообщения отправляет, чтобы не искать обороты двигателя в сообщениях блока ABS. Можно снять и прослушать блок отдельно на столе или вытащить его предохранитель в автомобиле и посмотреть, какие сообщения пропадут (вариант для экстремалов).

  • Полезно понимать, какой именно сигнал вы ищете: непрерывный (обороты двигателя), счетный (пробег) или логический (состояние дальнего света: вкл/выкл), так как каждый из них по-своему меняется со временем.

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

  • Приоритет сообщения тесно связан с его ID: меньшее значение соответствует высшему приоритету и наоборот. Сообщение с меньшим ID скорее всего будет принадлежать важному блоку, например ECM.

  • Тоже самое работает и с периодом отправки сообщений: сообщения с меньшим периодом скорее всего содержат более важную информацию и наоборот.

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

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

  1. панель подключаем отдельно на столе и проигрываем записанный ранее лог, за исключением ее собственных сообщений;

  2. поочередно убираем проигрываемые сообщения, сужая круг подозреваемых, пока интересующий индикатор не поменяет свое поведение;

  3. с помощью фаззинга находим нужные биты внутри подозрительного сообщения.

Не забудьте поставить галку «Use original frame timing from captured frames», чтобы SavvyCAN использовал тайминги оригинальных сообщений.

В процессе воспроизведения могут быть свои хитрости, например, искомый сигнал может быть в одном сообщении, но отображаться только при наличии одного или нескольких других сообщений. Тут важно набраться терпения и искать корректные сочетания и последовательности.

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

  1. записываем лог, одновременно посылая диагностические OBD2-запросы, например, спрашивая значение RPM;

  2. скармливаем SavvyCAN лог, DBC-файл с описанием OBD2-запросов и разрабатываемый DBC-файл подопытного автомобиля;

  3. строим два графика: референсный на базе OBD2 и предполагаемый;

  4. сравниваем оба графика между собой, убеждаемся, что сигналы похожи или нет.

Попробуем на реальных данных, построив два графика RPM, используя масштаб 1.0 для обоих сигналов, чтобы посмотреть исходные значения в «попугаях»:

Сигналы похожи по форме, но они отличаются масштабом. График RPM для OBD2 не такой гладкий, так как частота диагностических запросов ниже, чем частота собственных сообщений автомобиля. Если вернуть сигналу OBD2 его стандартный масштаб 0.25 и подогнать предполагаемый сигнал с помощью масштаба 0.125, то можно получить финальный результат:

Сигнал RPM найден! Чтобы дополнительно удостовериться в этом, достаточно проиграть несколько обнаруженных сообщений и проверить, двигается ли стрелка тахометра.

Подключение панели и настройка BeamNG

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

Не забывайте про терминирующий резистор CAN-шины, без него не получится наладить обмен сообщениями.

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

Точный уровень топлива в % можно найти в 1-ом байте сообщения 0x280, например, здесь он равен 0x5C = 92%:

0280 8 01 5C FF 10 FF FF 00 00

Ну а чтобы включить поддержку OutGauge в BeamNG, достаточно поставить галку в подразделе Options -> Other. Там же можно настроить IP-адрес и порт:

Применение результатов анализа

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

granta_basic.dbc
VERSION ""   NS_ :     NS_DESC_     CM_     BA_DEF_     BA_     VAL_     CAT_DEF_     CAT_     FILTER     BA_DEF_DEF_     EV_DATA_     ENVVAR_DATA_     SGTYPE_     SGTYPE_VAL_     BA_DEF_SGTYPE_     BA_SGTYPE_     SIG_TYPE_REF_     VAL_TABLE_     SIG_GROUP_     SIG_VALTYPE_     SIGTYPE_VALTYPE_     BO_TX_BU_     BA_DEF_REL_     BA_REL_     BA_DEF_DEF_REL_     BU_SG_REL_     BU_EV_REL_     BU_BO_REL_     SG_MUL_VAL_  BS_:  BU_: ECM ABS SRS  BO_ 384 ECM_180: 8 ECM    SG_ rpm : 7|16@0+ (0.125,0) [0|0] "rpm" Vector__XXX  BO_ 505 ECM_1F9: 8 ECM    SG_ speed_km_h : 23|16@0+ (0.01,0) [0|0] "" Vector__XXX  BO_ 852 ABS_354: 8 ABS    SG_ abs_lamp_on : 55|1@0+ (1,0) [0|1] "" Vector__XXX  BO_ 1176 SRS_498: 1 SRS    SG_ airbag_lamp : 4|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ seatbelt_fastened : 0|1@1+ (1,0) [0|0] "" Vector__XXX  BO_ 1361 ECM_551: 8 ECM    SG_ coolant_temperature : 15|8@0+ (1,-40) [0|0] "" Vector__XXX    SG_ check_engine_lamp_on : 32|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ check_engine_lamp_blinking : 33|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ oil_lamp_on : 34|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ overheat_lamp_with_sound : 35|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ battery_lamp_off : 24|1@1+ (1,0) [0|0] "" Vector__XXX 

Пару слов о скрипте на Python, который возьмет на себя роль переводчика. Внутри него происходит настройка подключения к CAN-шине, подготовка сообщений на основании DBC-файла и создание тасков для их цикличной отправки. Остается только принимать данные, поступающие от BeamNG, и модифицировать содержимое отправляемых сообщений:

translator_basic.py
import socket import struct import can import cantools from time import time  db = cantools.database.load_file('granta_basic.dbc') ecm_180 = db.get_message_by_name('ECM_180') ecm_1f9 = db.get_message_by_name('ECM_1F9') abs_354 = db.get_message_by_name('ABS_354') srs_498 = db.get_message_by_name('SRS_498') ecm_551 = db.get_message_by_name('ECM_551')  bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=500000)  def socket_stuff():     sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)     sock.bind(('127.0.0.1', 4444))      data = ecm_180.encode({'rpm': 0})     ecm_180_msg = can.Message(arbitration_id=ecm_180.frame_id, data=data, is_extended_id=False)     ecm_180_task = bus.send_periodic(ecm_180_msg, 0.100)      data = ecm_1f9.encode({'speed_km_h': 0})     ecm_1f9_msg = can.Message(arbitration_id=ecm_1f9.frame_id, data=data, is_extended_id=False)     ecm_1f9_task = bus.send_periodic(ecm_1f9_msg, 0.100)      data = abs_354.encode({'abs_lamp_on': 0})     abs_354_msg = can.Message(arbitration_id=abs_354.frame_id, data=data, is_extended_id=False)     abs_354_task = bus.send_periodic(abs_354_msg, 0.100)      data = srs_498.encode({'airbag_lamp': 0,                            'seatbelt_fastened': 1})     srs_498_msg = can.Message(arbitration_id=srs_498.frame_id, data=data, is_extended_id=False)     srs_498_task = bus.send_periodic(srs_498_msg, 0.200)      data = ecm_551.encode({'coolant_temperature': 0,                            'check_engine_lamp_on': 0,                            'check_engine_lamp_blinking': 0,                            'oil_lamp_on': 0,                            'overheat_lamp_with_sound': 0,                            'battery_lamp_off': 1})     ecm_551_msg = can.Message(arbitration_id=ecm_551.frame_id, data=data, is_extended_id=False)     ecm_551_task = bus.send_periodic(ecm_551_msg, 0.100)      start_ms = int(time() * 1000)      while True:         data = sock.recv(256)          if not data:             break          elapsed_ms = int(time() * 1000) - start_ms         if elapsed_ms >= 50:             start_ms = int(time() * 1000)             outgauge_pack = struct.unpack('I3sxH2B7f2I3f15sx15sxi', data)             speed = int(outgauge_pack[5])             rpm = int(outgauge_pack[6])             engtemp = int(outgauge_pack[8])              ecm_180_msg.data = ecm_180.encode({'rpm': rpm})             ecm_180_task.modify_data(ecm_180_msg)              ecm_1f9_msg.data = ecm_1f9.encode({'speed_km_h': speed * 3.6})             ecm_1f9_task.modify_data(ecm_1f9_msg)              ecm_551_msg.data = ecm_551.encode({'coolant_temperature': engtemp,                                                 'check_engine_lamp_on': 0,                                                 'check_engine_lamp_blinking': 0,                                                 'oil_lamp_on': 0,                                                 'overheat_lamp_with_sound': 0,                                                 'battery_lamp_off': 1})             ecm_551_task.modify_data(ecm_551_msg)      sock.close()  socket_stuff() 

Передача должна идти непрерывно, даже если значения не меняются, так как отсутствие сообщения в течение определенного промежутка времени панель принимает за неисправность. Теоретически, чтобы отличать одинаковые сообщения друг от друга, используются циклические счетчики. В трафике Гранты они тоже встречаются, но их отсутствие никак не влияет на работу панели, поэтому они были исключены из DBC-файла.

С выключением надоедливых лампочек все просто — достаточно один раз сформировать сообщения, выставить период отправки и забыть про них. Данные, которые непрерывно обновляются на основании информации от BeamNG представлены в таблице:

Indicator

CAN Message

BeamNG Lua Variable

speed

ECM_1F9

electrics.values.wheelspeed OR electrics.values.airspeed

RPM

ECM_180

electrics.values.rpm

coolant temp

ECM_551

electrics.values.watertemp

Если подключение было сделано правильно, то при запуске скрипт погасит все сигнальные лампы и будет ждать данных от BeamNG:

Углубленный уровень

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

  1. погасить все незадействованные лампочки (чтобы они не отвлекали во время игры) с помощью CAN-сигналов и конфигурации панели;

  2. выводить наибольшее количество возможной информации на панель;

  3. отредактировать Lua-скрипт BeamNG для передачи новых переменных;

  4. изменить конфигурацию панели для расширенной индикации.

Эта бесконечная гирлянда

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

Лампа аварийного давления масла. Загорается красным светом при включении зажигания и после запуска двигателя гаснет. При работающем двигателе загорается при недостаточном давлении в системе смазки двигателя. Ближайшее по смыслу, что удалось найти в BeamNG, это переменная electrics.values.oil, которая срабатывает при температуре масла более 130 градусов. Не совсем то, что нужно, но лучше, чем ничего. Впрочем, в скрипте BeamNG была найдена символичная строка o.oilPressure = 0 -- TODO, так что возможно в будущем лампочку получится подключить более правдиво.

Лампа антиблокировочной системы тормозов. Загорается желтым светом при включении зажигания на 2 секунды, во всех других случаях загорается при неисправности системы ABS. В BeamNG переменная electrics.values.abs передает состояние ABS и загорается, если система активна, например, при резком торможении.

Лампа «Отказ тормоза». Загорается красным светом при включении зажигания на 2 секунды. Далее загорается на основании 3-ех условий: мигает, если включен стояночный тормоз; горит постоянно при низком уровне ТЖ; горит постоянно совместно с индикатором ABS при отказе ABS. И здесь есть проблема — отдельно включить индикатор можно только с помощью выводов, подключенных к стояночному тормозу или датчику уровня ТЖ. По CAN-шине нам доступна только третья опция — когда индикатор горит совместно с индикатором ABS. Поэтому придется смириться с тем, что при включении «ручника» (electrics.values.parkingbrake) в BeamNG мы будем лицезреть еще и лампу ABS.

Лампа «Проверь двигатель». Загорается желтым светом при включении зажигания и после запуска двигателя гаснет. Далее загорается только при возникновении неисправностей, связанных с работой двигателя. В BeamNG переменная electrics.values.checkengine не используется при включении зажигания и загорается только в случае критического повреждения двигателя.

Лампа превышения температуры ОЖ. При включении зажигания загорается красным светом на 2 секунды, далее загорается, если температура ОЖ становится выше 115 градусов. В BeamNG нет логической переменной, которая отвечает за такой индикатор, поэтому просто отредактируем скрипт на Lua, чтобы он активировал индикатор при условии electrics.values.watertemp > 115.

Лампа разряда аккумуляторной батареи. Загорается красным светом при включении зажигания и после запуска двигателя гаснет. При работающем двигателе загорается в случае неисправности системы электропитания автомобиля. Поскольку у BeamNG нет задачи симулировать неисправности электрики, то в игре эта лампа горит только при заглушенном двигателе, на основании переменной electrics.values.engineRunning.

Кратко о времени

Еще при первом просмотре логов было обнаружено множество ASCII-символов в некоторых сообщениях. Всего таких сообщений оказалось 6: 0x4A4, 0x4A6, 0x4A8, 0x4AA, 0x4AC, 0x4AE; все по 8 байт размером. Выяснилось, что они содержат два вида посылок в формате NMEA: GPRMC и GPGGA. Внутри GPRMC находится рекомендуемый минимум навигационных данных, внутри GPGGA — данные о последнем зафиксированном местоположении. Найти источник сообщений было легко: единственным блоком, обладающим навигационным приемником был блок ЭРА-Глонасс.

Если прокатиться на автомобиле, собрать логи и извлечь NMEA-сообщения, то можно построить вполне себе точный и плавный трек поездки. Халявный источник координат — это всегда полезно, можно записывать свои перемещения или скармливать данные покупной системе навигации.

Но для чего эта информация в автомобиле с базовой «балалайкой» без системы навигации? При воспроизведении записанных логов, в глаза бросились часы на панели — их значение менялось на то, которое содержалось в NMEA-сообщениях.

Раз так, то почему бы не научиться выставлять точное время на панели? Эксперименты показали, что достаточно только одного сообщения GPRMC. С помощью библиотеки nmeasim составляем строку, расфасовываем ее по CAN-сообщениям и не забываем поставить паузы в 50 миллисекунд между ними.

nmea_set_time.py
from datetime import datetime, timedelta, timezone from nmeasim.models import GpsReceiver import time import can  bus = can.interface.Bus(bustype='pcan',                          channel='PCAN_USBBUS1',                          bitrate=500000)  gps = GpsReceiver(     #date_time=datetime(2020, 1, 1, 1, 20, 1, tzinfo=timezone.utc),     date_time=datetime.now(),     output=('RMC',) )  gprmc_str = gps.get_output() gprmc_bytes = gprmc_str[0].encode()  can_id = 0x4A4 for i in range(0, len(gprmc_bytes), 8):     data = gprmc_bytes[i:i+8]     print(hex(can_id), data)     bus.send(can.Message(arbitration_id=can_id, data=data, is_extended_id=False))     can_id = (can_id + 0x02) if (can_id < 0x4AE) else 0x4A4     time.sleep(0.050)  bus.shutdown()

Можно брать время ПК или выставлять свое, например 04 часа 20 минут:

Теперь время всегда на виду.

Конфигурация и ее причуды

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

Конфигурация представляет интерес для исследований по двум причинам:

  1. практическая: расширение набора штатных функций, получение информации о возможных комплектациях и опциях автомобиля;

  2. археологическая: конфигурация иногда наглядно иллюстрирует эволюцию, которую прошел автомобиль, переходя от ранних датчиков и актуаторов к более современным и технологичным (особенно если модель задержалась на рынке).

Для выведения большего количества информации на панель, чем в прошлом разделе, необходимо научиться включать дополнительные функции. Например, чтобы моргать лампочкой ESC, нужно сначала найти способ включить эту опцию через UDS, а затем найти сообщение, управляющее лампочкой. Задачу усложняет отсутствие подопытного автомобиля в нужной комплектации.

В простом случае, этот процесс включает четыре этапа:

  1. поиск возможного DID, который может отвечать за состояние функции;

  2. подбор и запись нового значения в DID, например, 1 = ВКЛ, 0 = ВЫКЛ;

  3. анализ поведения панели, например, могли загореться новые лампы;

  4. подбор сообщения и управляющего сигнала с помощью фаззинга.

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

Сканирование DID, доступных для чтения, выявило любопытный набор значений в диапазоне 0x0100..0x0110. Запись доступна без каких-либо механизмов защиты, достаточно перейти в расширенную диагностическую сессию 0x03.

Относительно быстро нашлась система электронного контроля устойчивости (ESC). После записи единицы в DID 0x0103 и последующей перезагрузки, на панели сама загорелась искомая индикация:

В оригинальном скрипте BeamNG для управления лампой ESC применялись две переменных: electrics.values.esc и electrics.values.tcs. Если хотя бы одна из них принимает ненулевое значение, то лампа загорается.

Насколько стало понятно по обрывкам документации на Гранту, функции ABS и ESC совмещает в себе один и тот же блок, поэтому схема почти не меняется:

Перейдем к светотехнике: указателям поворота, лампам ближнего и дальнего света. На подопытном автомобиле индикация включается банальной подачей 12В от соответствующих реле, которые управляют световыми приборами. Для этого отдельные выводы панели подключены к этим самым реле, но так как мы строго ограничены воздействиями по CAN-шине, нужно искать обходной путь.

Им станет ЦБКЭ (Центральный Блок Кузовной Электроники), который применяется в более дорогих комплектациях, одна из его основных функций — это управление светотехникой. Отдельно включать его в конфигурации панели не пришлось, достаточно было просто найти сообщение, влияющие на индикацию световых приборов:

BeamNG использует переменные electrics.values.lowbeam/electrics.values.highbeam для ближнего/дальнего света и electrics.values.signal_L/electrics.values.signal_R для левого/правого поворотников.

ЦБКЭ это отдельный блок, поэтому дорисовываем его на схеме под именем BCM (Body Control Module):

Чего еще не хватает автосимулятору? Правильно, индикатора текущей передачи! На подопытном автомобиле установлена МКПП и на панель выводятся подсказки по переключению передач. Все отображаемые комбинации этого индикатора выглядят так:

Такая индикация не подходит сразу по двум причинам:

  • указывается не текущая, а рекомендуемая передача;

  • рядом всегда мигает стрелка повышения/понижения передачи.

Найти с помощью перебора сообщений и фаззинга «чистый» способ отображения текущей передачи МКПП не удалось. Но не будем отчаиваться раньше времени, поскольку на данный автомобиль могли устанавливаться и автоматические коробки, а уж они-то обычно выводят текущий режим/передачу на панель приборов.

После записи единицы в DID 0x0100 панель зажгла шестеренку с восклицательным знаком внутри — сигнализатор неисправности трансмиссии, который применяется для АКПП или «робота»:

Методом перебора был найден вот такой набор индикации:

Это уже значительно лучше, чем в случае с МКПП, такая индикация может отлично подойти для симуляции классических «прындлов». Но что если захочется поиграть на МКПП и видеть номер передачи больше, чем 2?

Предположив, что DID 0x0100 отвечает за тип коробки передач, проявляем чудеса фантазии и записываем в него двойку — панель принимает это значение и моргает один раз лампой неисправности трансмиссии. Снова перебираем сообщения и находим самого настоящего «робота»:

То есть: заднюю, нейтраль и по 5 передач в ручном и автоматическом режимах. Вот это уже достойный улов! Для более современных 8-ми ступенчатых автомобилей этой индикации, конечно, не хватит, но для симуляции 5-ти ступенчатой механики или автомата — вполне. BeamNG предлагает несколько переменных для отображения передачи, нам больше всего подходит electrics.values.gearIndex.

Оба типа трансмисии могут быть полезными для BeamNG, поэтому рисуем на обновленной схеме сразу обе:

Тем не менее, выбрать нужно что-то одно. АМТ лучше подходит из-за большего количества отображаемых передач, поэтому дальше будем использовать именно её.

Поскольку основные типы индикации уже покрыты, можно попробовать найти и задействовать что-нибудь более экзотическое. В самых ранних экспериментах, во все доступные DID записывались единицы и проводился перебор сообщений совместно с фаззингом. Смысл был прост — включить все, что можно включить и задолбать панель всеми возможными сообщениями.

Именно тогда на глаза попались лампы круиз-контроля и ограничителя скорости, что оказалось удачей, так как сами они не загораются при включении зажигания. Был найден нужный DID 0x0101 и даже подходящая переменная BeamNG electrics.values.cruiseControlActive. В оригинальном интерфейсе игры круиза нет, но его можно легко добавить через UI Apps -> Add app -> Cruise Control. Никакого нового блока на схему добавлять не надо, поскольку сигналы состояния круиз-контроля на панель отправляет ECM. Получилось вполне органично, даже иконки почти одинаковые:

А как насчет состояния шин? Запись единицы в DID 0x0104 привела к появлению лампы аварийного снижения давления в шинах, которой должна управлять по CAN система контроля давления в шинах (Tire Pressure Monitoring System). Информации о данной опции для Гранты в интернете не очень много, видимо она была действительно редкой:

Данные о давлении в шинах electrics.values не предоставляет, но ее можно найти вwheels.wheels[0..N].isTireDeflated. Как можно догадаться из названия, это булево значение, отображающее сдулась конкретная шина или нет.

Теперь можно добавить блок TPMS на схему и получить ее финальную форму:

Стоит отметить, что два последних результата, это в некотором смысле «натягивание совы на глобус». Для них требуется подключение дополнений для интерфейса и использование переменных за пределами electrics.values. Тем не менее, они показывают, что при должном упорстве можно подключить практически что угодно.

Обнаруженные DID, которые удалось осмыслить, соберем в таблицу:

DID

Функция

0x0100

Тип КПП (0 = МКПП, 1 = АКПП, 2 = АМТ)

0x0101

Круиз-контроль и ограничитель скорости

0x0103

ESC

0x0104

TPMS

0x0107

Подсказка переключения для МКПП

0x010A

ABS

0x010C

SRS

А вот так выглядит итоговое «меню», со всеми используемыми индикаторами, сообщениями и переменными:

Indicator

CAN Message

BeamNG Lua Variable

speed

ECM_1F9

electrics.values.wheelspeed OR electrics.values.airspeed

RPM

ECM_180

electrics.values.rpm

coolant temp

ECM_551

electrics.values.watertemp

gear

ECM_1F9/AT_421/AMT_3F7

electrics.values.gearIndex

lowbeam

BCM_481

electrics.values.lowbeam

highbeam

BCM_481

electrics.values.highbeam

handbrake

ABS_354

electrics.values.parkingbrake

ESC/TCS

ESC_245

electrics.values.esc OR electrics.values.tcs

turnsignal L

BCM_481

electrics.values.signal_L

turnsignal R

BCM_481

electrics.values.signal_R

oil warning

ECM_551

electrics.values.oil

battery

ECM_551

electrics.values.engineRunning

abs

ABS_354

electrics.values.abs

check engine

ECM_551

electrics.values.checkengine

coolant warning

ECM_551

electrics.values.watertemp

tire pressure

TPMS_3E2

wheels.wheels[0..N].isTireDeflated

cruise

ECM_35D

electrics.values.cruiseControlActive

Доработанные файлы:

granta_advanced.dbc
VERSION ""   NS_ :     NS_DESC_     CM_     BA_DEF_     BA_     VAL_     CAT_DEF_     CAT_     FILTER     BA_DEF_DEF_     EV_DATA_     ENVVAR_DATA_     SGTYPE_     SGTYPE_VAL_     BA_DEF_SGTYPE_     BA_SGTYPE_     SIG_TYPE_REF_     VAL_TABLE_     SIG_GROUP_     SIG_VALTYPE_     SIGTYPE_VALTYPE_     BO_TX_BU_     BA_DEF_REL_     BA_REL_     BA_DEF_DEF_REL_     BU_SG_REL_     BU_EV_REL_     BU_BO_REL_     SG_MUL_VAL_  BS_:  BU_: ECM ABS SRS BCM ESC AMT TPMS AT  BO_ 384 ECM_180: 8 ECM    SG_ rpm : 7|16@0+ (0.125,0) [0|0] "rpm" Vector__XXX  BO_ 505 ECM_1F9: 8 ECM    SG_ speed_km_h : 23|16@0+ (0.01,0) [0|0] "" Vector__XXX    SG_ mt_gear_recommendation : 15|4@0+ (1,0) [0|1] "" Vector__XXX  BO_ 581 ESC_245: 8 ESC    SG_ esc_off_lamp_on : 24|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ esc_lamp_on : 25|1@0+ (1,0) [0|1] "" Vector__XXX  BO_ 852 ABS_354: 8 ABS    SG_ abs_lamp_on : 55|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ abs_and_brake_lamp_on : 53|1@0+ (1,0) [0|1] "" Vector__XXX  BO_ 861 ECM_35D: 8 ECM    SG_ cruise_orange_lamp_on : 38|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ limiter_orange_lamp_on : 39|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ orange_lamp_blinking_flag : 33|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ green_lamp_flag : 32|1@0+ (1,0) [0|1] "" Vector__XXX  BO_ 994 TPMS_3E2: 8 TPMS    SG_ tpms_lamp_on : 5|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ tpms_lamp_blinking : 6|1@0+ (1,0) [0|1] "" Vector__XXX  BO_ 1015 AMT_3F7: 8 AMT    SG_ amt_state : 7|5@0+ (1,0) [0|1] "" Vector__XXX    SG_ transmission_lamp_on : 2|1@0+ (1,0) [0|1] "" Vector__XXX  BO_ 1057 AT_421: 8 AT    SG_ overdrive_off_lamp_on : 0|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ transmission_lamp_on : 2|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ at_state : 6|4@0+ (1,0) [0|1] "" Vector__XXX  BO_ 1153 BCM_481: 8 BCM    SG_ turnsignal_right : 16|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ turnsignal_left : 17|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ low_beam : 6|1@0+ (1,0) [0|1] "" Vector__XXX    SG_ high_beam : 4|1@0+ (1,0) [0|1] "" Vector__XXX  BO_ 1176 SRS_498: 1 SRS    SG_ airbag_lamp : 4|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ seatbelt_fastened : 0|1@1+ (1,0) [0|0] "" Vector__XXX  BO_ 1361 ECM_551: 8 ECM    SG_ coolant_temperature : 15|8@0+ (1,-40) [0|0] "" Vector__XXX    SG_ check_engine_lamp_on : 32|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ check_engine_lamp_blinking : 33|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ oil_lamp_on : 34|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ overheat_lamp_with_sound : 35|1@1+ (1,0) [0|0] "" Vector__XXX    SG_ battery_lamp_off : 24|1@1+ (1,0) [0|0] "" Vector__XXX  VAL_ 505 mt_gear_recommendation 3 "4_down" 4 "3_down" 5 "2_down" 6 "1_down" 9 "2_up" 10 "3_up" 11 "4_up" 12 "5_up"; VAL_ 1015 amt_state 2 "R" 3 "N" 16 "M1" 17 "M2" 18 "M3" 19 "M4" 20 "M5" 24 "A1" 25 "A2" 26 "A3" 27 "A4" 28 "A5"; VAL_ 1057 at_state 1 "P" 2 "R" 3 "N" 4 "D" 8 "1" 9 "2"; 
translator_advanced.py
import socket import struct import can import cantools from time import time  gear_dict = {0: 'R', 1: 'N', 2: 'M1', 3: 'M2', 4: 'M3', 5: 'M4', 6: 'M5'}  db = cantools.database.load_file('granta_advanced.dbc') ecm_180 = db.get_message_by_name('ECM_180') ecm_1f9 = db.get_message_by_name('ECM_1F9') esc_245 = db.get_message_by_name('ESC_245') abs_354 = db.get_message_by_name('ABS_354') ecm_35d = db.get_message_by_name('ECM_35D') tpms_3e2 = db.get_message_by_name('TPMS_3E2') amt_3f7 = db.get_message_by_name('AMT_3F7') bcm_481 = db.get_message_by_name('BCM_481') srs_498 = db.get_message_by_name('SRS_498') ecm_551 = db.get_message_by_name('ECM_551')  bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=500000)  def socket_stuff():     sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)     sock.bind(('127.0.0.1', 4444))      data = ecm_180.encode({'rpm': 0})     ecm_180_msg = can.Message(arbitration_id=ecm_180.frame_id, data=data, is_extended_id=False)     ecm_180_task = bus.send_periodic(ecm_180_msg, 0.100)      data = ecm_1f9.encode({'speed_km_h': 0,                             'mt_gear_recommendation': 0})     ecm_1f9_msg = can.Message(arbitration_id=ecm_1f9.frame_id, data=data, is_extended_id=False)     ecm_1f9_task = bus.send_periodic(ecm_1f9_msg, 0.100)      data = esc_245.encode({'esc_off_lamp_on': 0,                             'esc_lamp_on': 0})     esc_245_msg = can.Message(arbitration_id=esc_245.frame_id, data=data, is_extended_id=False)     esc_245_task = bus.send_periodic(esc_245_msg, 0.100)      data = abs_354.encode({'abs_lamp_on': 0,                             'abs_and_brake_lamp_on': 0})     abs_354_msg = can.Message(arbitration_id=abs_354.frame_id, data=data, is_extended_id=False)     abs_354_task = bus.send_periodic(abs_354_msg, 0.100)      data = ecm_35d.encode({'cruise_orange_lamp_on': 0,                             'limiter_orange_lamp_on': 0,                             'green_lamp_flag': 1,                             'orange_lamp_blinking_flag': 0})     ecm_35d_msg = can.Message(arbitration_id=ecm_35d.frame_id, data=data, is_extended_id=False)     ecm_35d_task = bus.send_periodic(ecm_35d_msg, 0.200)      data = tpms_3e2.encode({'tpms_lamp_blinking': 0,                              'tpms_lamp_on': 0})     tpms_3e2_msg = can.Message(arbitration_id=tpms_3e2.frame_id, data=data, is_extended_id=False)     tpms_3e2_task = bus.send_periodic(tpms_3e2_msg, 0.200)      data = amt_3f7.encode({'amt_state': 'N',                             'transmission_lamp_on':0})     amt_3f7_msg = can.Message(arbitration_id=amt_3f7.frame_id, data=data, is_extended_id=False)     amt_3f7_task = bus.send_periodic(amt_3f7_msg, 0.100)      data = bcm_481.encode({'turnsignal_right': 0,                             'turnsignal_left': 0,                             'low_beam': 0,                             'high_beam': 0})     bcm_481_msg = can.Message(arbitration_id=bcm_481.frame_id, data=data, is_extended_id=False)     bcm_481_task = bus.send_periodic(bcm_481_msg, 0.100)      data = srs_498.encode({'airbag_lamp': 0,                             'seatbelt_fastened': 1})     srs_498_msg = can.Message(arbitration_id=srs_498.frame_id, data=data, is_extended_id=False)     srs_498_task = bus.send_periodic(srs_498_msg, 0.200)      data = ecm_551.encode({'coolant_temperature': 0,                            'check_engine_lamp_on': 0,                            'check_engine_lamp_blinking': 0,                            'oil_lamp_on': 0,                            'overheat_lamp_with_sound': 0,                            'battery_lamp_off': 1})     ecm_551_msg = can.Message(arbitration_id=ecm_551.frame_id, data=data, is_extended_id=False)     ecm_551_task = bus.send_periodic(ecm_551_msg, 0.100)      start_ms = int(time() * 1000)      while True:         data = sock.recv(256)          if not data:             break          elapsed_ms = int(time() * 1000) - start_ms         if elapsed_ms >= 50:             start_ms = int(time() * 1000)             outgauge_pack = struct.unpack('I3sxH2B7f2I3f15sx15sxi', data)             gear = int(outgauge_pack[3])             speed = int(outgauge_pack[5])             rpm = int(outgauge_pack[6])             engtemp = int(outgauge_pack[8])             showlights = int(outgauge_pack[13])              ecm_180_msg.data = ecm_180.encode({'rpm': rpm})             ecm_180_task.modify_data(ecm_180_msg)              ecm_1f9_msg.data = ecm_1f9.encode({'speed_km_h': speed * 3.6,                                                 'mt_gear_recommendation': 0})             ecm_1f9_task.modify_data(ecm_1f9_msg)              esc_245_msg.data = esc_245.encode({'esc_off_lamp_on': 0,                                                 'esc_lamp_on': (showlights >> 4) & 1})             esc_245_task.modify_data(esc_245_msg)              abs_354_msg.data = abs_354.encode({'abs_lamp_on': (showlights >> 10) & 1 ,                                                 'abs_and_brake_lamp_on': (showlights >> 2) & 1})             abs_354_task.modify_data(abs_354_msg)              ecm_35d_msg.data = ecm_35d.encode({'cruise_orange_lamp_on': (showlights >> 14) & 1,                                                 'limiter_orange_lamp_on': 0,                                                 'green_lamp_flag': 1,                                                 'orange_lamp_blinking_flag': 0})             ecm_35d_task.modify_data(ecm_35d_msg)              tpms_3e2_msg.data = tpms_3e2.encode({'tpms_lamp_blinking': 0,                                                   'tpms_lamp_on': (showlights >> 12) & 1})             tpms_3e2_task.modify_data(tpms_3e2_msg)              amt_3f7_msg.data = amt_3f7.encode({'amt_state': gear_dict[gear],                                                 'transmission_lamp_on':0})             amt_3f7_task.modify_data(amt_3f7_msg)              bcm_481_msg.data = bcm_481.encode({'turnsignal_right': (showlights >> 6) & 1,                                                 'turnsignal_left': (showlights >> 5) & 1,                                                 'low_beam': (showlights >> 0) & 1,                                                 'high_beam': (showlights >> 1) & 1})             bcm_481_task.modify_data(bcm_481_msg)              ecm_551_msg.data = ecm_551.encode({'coolant_temperature': engtemp,                                                 'check_engine_lamp_on': (showlights >> 11) & 1,                                                 'check_engine_lamp_blinking': 0,                                                 'oil_lamp_on': (showlights >> 8) & 1,                                                 'overheat_lamp_with_sound': (showlights >> 13) & 1,                                                 'battery_lamp_off': 1 ^ ((showlights >> 9) & 1)})             ecm_551_task.modify_data(ecm_551_msg)      sock.close()  socket_stuff() 

В скрипте outgauge.lua были отредактированы возможные значения переменной dashLights и логика обнаружения системы ESC. Оригинальный скрипт адекватно включал лампу ESC только на определенных моделях, например, Hirochi Sunburst.

outgauge.lua (modified)
-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1. -- If a copy of the bCDDL was not distributed with this -- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt  -- this file serves two purposes: -- A) generic outgauge implementation: used to use for example custom made dashboard hardware and alike -- B) update the user interface for the remote control app directly via the sendPackage function -- please note that use case A and B exclude each other for now.  local M = {}  local ip = "127.0.0.1" local port = 4444  local udpSocket = nil  local ffi = require("ffi")  local function declareOutgaugeStruct()   -- the documentation can be found at LFS/docs/InSim.txt   ffi.cdef [[   typedef struct outgauge_t  {       unsigned       time;            // time in milliseconds (to check order)       char           car[4];          // Car name       unsigned short flags;           // Info (see OG_x below)       char           gear;            // Reverse:0, Neutral:1, First:2...       char           plid;            // Unique ID of viewed player (0 = none)       float          speed;           // M/S       float          rpm;             // RPM       float          turbo;           // BAR       float          engTemp;         // C       float          fuel;            // 0 to 1       float          oilPressure;     // BAR       float          oilTemp;         // C       unsigned       dashLights;      // Dash lights available (see DL_x below)       unsigned       showLights;      // Dash lights currently switched on       float          throttle;        // 0 to 1       float          brake;           // 0 to 1       float          clutch;          // 0 to 1       char           display1[16];    // Usually Fuel       char           display2[16];    // Usually Settings       int            id;              // optional - only if OutGauge ID is specified   } outgauge_t;   ]] end pcall(declareOutgaugeStruct)  local OG_KM = 16384 local OG_BAR = 32768 local OG_TURBO = 8192  local DL_LOWBEAM = 2 ^ 0 local DL_HIGHBEAM = 2 ^ 1 local DL_HANDBRAKE = 2 ^ 2 local DL_TC = 2 ^ 4 local DL_SIGNAL_L = 2 ^ 5 local DL_SIGNAL_R = 2 ^ 6 local DL_OILWARN = 2 ^ 8 local DL_BATTERY = 2 ^ 9 local DL_ABS = 2 ^ 10 local DL_CHECKENGINE = 2 ^ 11 local DL_TPMS = 2 ^ 12 local DL_OVERHEAT = 2 ^ 13 local DL_CRUISE = 2 ^ 14  local hasShiftLights = false  local function sendPackage(ip, port, id)   --log('D', 'outgauge', 'sendPackage: '..tostring(ip) .. ':' .. tostring(port))    if not electrics.values.watertemp then     -- vehicle not completly initialized, skip sending package     return   end    local o = ffi.new("outgauge_t")   -- set the values   o.time = 0 -- not used atm   o.car = "beam"   o.flags = OG_KM + OG_BAR + (electrics.values.turboBoost and OG_TURBO or 0)   o.gear = electrics.values.gearIndex + 1 -- reverse = 0 here   o.plid = 0   o.speed = electrics.values.wheelspeed or electrics.values.airspeed   o.rpm = electrics.values.rpm or 0   o.turbo = (electrics.values.turboBoost or 0) / 14.504    o.engTemp = electrics.values.watertemp or 0   o.fuel = electrics.values.fuel or 0   o.oilPressure = 0 -- TODO   o.oilTemp = electrics.values.oiltemp or 0    -- the lights   o.dashLights = bit.bor(o.dashLights, DL_LOWBEAM)   if electrics.values.lowbeam ~= 0 then     o.showLights = bit.bor(o.showLights, DL_LOWBEAM)   end    o.dashLights = bit.bor(o.dashLights, DL_HIGHBEAM)   if electrics.values.highbeam ~= 0 then     o.showLights = bit.bor(o.showLights, DL_HIGHBEAM)   end    o.dashLights = bit.bor(o.dashLights, DL_HANDBRAKE)   if electrics.values.parkingbrake ~= 0 then     o.showLights = bit.bor(o.showLights, DL_HANDBRAKE)   end    o.dashLights = bit.bor(o.dashLights, DL_SIGNAL_L)   if electrics.values.signal_L ~= 0 then     o.showLights = bit.bor(o.showLights, DL_SIGNAL_L)   end    o.dashLights = bit.bor(o.dashLights, DL_SIGNAL_R)   if electrics.values.signal_R ~= 0 then     o.showLights = bit.bor(o.showLights, DL_SIGNAL_R)   end    local hasABS = electrics.values.hasABS or false   if hasABS then     o.dashLights = bit.bor(o.dashLights, DL_ABS)     if electrics.values.abs ~= 0 then       o.showLights = bit.bor(o.showLights, DL_ABS)     end   end    o.dashLights = bit.bor(o.dashLights, DL_OILWARN)   if electrics.values.oil ~= 0 then     o.showLights = bit.bor(o.showLights, DL_OILWARN)   end    o.dashLights = bit.bor(o.dashLights, DL_BATTERY)   if electrics.values.engineRunning == 0 then     o.showLights = bit.bor(o.showLights, DL_BATTERY)   end    local hasESC = (electrics.values.esc ~= nil) or (electrics.values.tcs ~= nil)   if hasESC then     o.dashLights = bit.bor(o.dashLights, DL_TC)     if electrics.values.esc ~= 0 or electrics.values.tcs ~= 0 then       o.showLights = bit.bor(o.showLights, DL_TC)     end   end    o.dashLights = bit.bor(o.dashLights, DL_CHECKENGINE)   if electrics.values.checkengine == true then     o.showLights = bit.bor(o.showLights, DL_CHECKENGINE)   end    o.dashLights = bit.bor(o.dashLights, DL_TPMS)   if wheels.wheels[0].isTireDeflated or       wheels.wheels[1].isTireDeflated or       wheels.wheels[2].isTireDeflated or       wheels.wheels[3].isTireDeflated then     o.showLights = bit.bor(o.showLights, DL_TPMS)   end    o.dashLights = bit.bor(o.dashLights, DL_OVERHEAT)   if electrics.values.watertemp > 115 then     o.showLights = bit.bor(o.showLights, DL_OVERHEAT)   end    local hasCC = (electrics.values.cruiseControlActive ~= nil)   if hasCC then     o.dashLights = bit.bor(o.dashLights, DL_CRUISE)     if electrics.values.cruiseControlActive ~= 0 then       o.showLights = bit.bor(o.showLights, DL_CRUISE)     end   end    o.throttle = electrics.values.throttle   o.brake = electrics.values.brake   o.clutch = electrics.values.clutch   o.display1 = "" -- TODO   o.display2 = "" -- TODO   o.id = id    local packet = ffi.string(o, ffi.sizeof(o)) --convert the struct into a string   udpSocket:sendto(packet, ip, port)   --log("I", "", "SendPackage for ID '"..dumps(id).."': "..dumps(electrics.values.rpm)) end  local function updateGFX(dt)   if not playerInfo.firstPlayerSeated then     return   end   sendPackage(ip, port, 0) end  local function onExtensionLoaded()   if not ffi then     log("E", "outgauge", "Unable to load outgauge module: Lua FFI required")     return false   end    if not udpSocket then     udpSocket = socket.udp()   end    ip = settings.getValue("outgaugeIP")   port = tonumber(settings.getValue("outgaugePort"))    log("I", "", "Outgauge initialized for: " .. tostring(ip) .. ":" .. tostring(port))  --  local shiftLightControllers = controller.getControllersByType("shiftLights") --  hasShiftLights = shiftLightControllers and #shiftLightControllers > 0   return true end  local function onExtensionUnloaded()   if udpSocket then     udpSocket:close()   end   udpSocket = nil end  -- public interface M.onExtensionLoaded = onExtensionLoaded M.onExtensionUnloaded = onExtensionUnloaded M.updateGFX = updateGFX  M.sendPackage = sendPackage  return M 

Заключение

Вот так изначальная идея поиграться c CAN-шиной разрослась до аппаратного дополнения к BeamNG. Лень мотивировала использовать минимальное количество железа, а любопытство — найти побольше полезных сообщений.

Из недостатков стоит упомянуть проблемы с таймингом сообщений (особенно это бывает заметно для ритма поворотников) и невозможность полноценного подключения некоторых сигналов (уровень топлива и ручник). Впрочем, это не сильно сказывается на впечатлениях от геймплея.

Заодно в процессе получилось чуть лучше понять, как устроен подопытный автомобиль, и некоторые особенности BeamNG.


ссылка на оригинал статьи https://habr.com/ru/post/653615/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *