Дружим 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.
Базовый уровень
Список действий для базового уровня:
погасить все незадействованные лампочки (чтобы они не отвлекали во время игры) с помощью CAN-сигналов;
выводить только значения оборотов, скорости и температуры двигателя на панель;
оставить Lua-скрипт BeamNG первозданном виде;
оставить конфигурацию панели в первозданном виде.
Структура CAN-шины автомобиля
Этот шаг не является обязательным, но упрощает поиск сообщений/сигналов и помогает лучше понять устройство автомобиля.
Основными источниками информации являются данные о комплектации и оснащении автомобиля, руководство по эксплуатации, электрические схемы и наглядное изучение проводки при разборке салона.
В данном конкретном случае, в роли подопытного выступает Lada Granta FL 2021 в комплектации "Комфорт", с двигателем ВАЗ-21127 и механической коробкой передач. Оснащение не самое навороченное, поэтому и схема получается небольшой:
Наименования блоков подобраны фривольно и могут не соответствовать официальной документации.
Всего 6 блоков, подключенных к одной-единственной шине:
ECM (Engine Control Module) - блок управления двигателем;
SRS (Supplemental Restraint System) - блок подушек и преднатяжителей ремней безопасности;
ABS (Anti-lock Braking System) - блок управления антиблокировочной системой;
Instrument Cluster - панель приборов;
Head Unit - головное устройство;
ERA - блок системы Экстренного Реагирования при Авариях.
Отдельно можно упомянуть блок центрального замка, но он к шине не подключается и существует сам по себе.
Сбор и анализ логов
Самый простой способ подключения к CAN-шине - через диагностический разъем. Ситуация может быть чуть сложнее, если на автомобиле установлен гейтвей, который фильтрует трафик и не пропускает сообщения. Тогда придется подключаться к шинам за гейтвеем и записывать трафик до фильтрации. При этом важно понимать, какая шина за какой домен сети отвечает.
Процесс сбора логов не представляет особой сложности, достаточно подключиться и проделать те операции, результат которых надо будет найти в логах. Например, прогреть двигатель до определенной температуры или поднять обороты до заранее известных значений.
Следующий шаг - это анализ собранных логов. Существуют разные подходы для упрощения этой задачи, есть даже автоматизированные. В целом это процесс творческий и по-своему интересный, похожий на судоку. Чем больше сигналов вы находите, тем меньше остаются загадками.
Обнаруженные сигналы вносятся в DBC-файл, который будет использоваться далее. Кстати, возможно вам повезло и CAN-шина вашего автомобиля уже описана в проекте opendbc.
Базовые подсказки, которые помогут ускорить поиски:
Начинать анализ желательно с поиска того, какой блок какие сообщения отправляет, чтобы не искать обороты двигателя в сообщениях блока ABS. Можно снять и прослушать блок отдельно на столе или вытащить его предохранитель в автомобиле и посмотреть, какие сообщения пропадут (вариант для экстремалов).
Полезно понимать, какой именно сигнал вы ищете: непрерывный (обороты двигателя), счетный (пробег) или логический (состояние дальнего света: вкл/выкл), так как каждый из них по-своему меняется со временем.
Сигналы могут иметь самые разные коэффициенты для перевода из "попугаев" в корректные единицы измерения, поэтому до перевода не заостряйте внимание на значениях - форма сигнала гораздо важнее.
Приоритет сообщения тесно связан с его ID: меньшее значение соответствует высшему приоритету и наоборот. Сообщение с меньшим ID скорее всего будет принадлежать важному блоку, например ECM.
Тоже самое работает и с периодом отправки сообщений: сообщения с меньшим периодом скорее всего содержат более важную информацию и наоборот.
Для быстрого получения результатов использовалось два подхода: воспроизведение записанных сообщений и сравнение с диагностическими запросами.
Воспроизведение записанных сообщений - наиболее быстрый и комфортный способ. Достаточно просто записать лог короткой поездки по городу, воспроизвести и наблюдать, как шевелятся стрелки у лежащей на столе панели.
панель подключаем отдельно на столе и проигрываем записанный ранее лог, за исключением ее собственных сообщений;
поочередно убираем проигрываемые сообщения, сужая круг подозреваемых, пока интересующий индикатор не поменяет свое поведение;
с помощью фаззинга находим нужные биты внутри подозрительного сообщения.
Не забудьте поставить галку "Use original frame timing from captured frames", чтобы SavvyCAN использовал тайминги оригинальных сообщений.
В процессе воспроизведения могут быть свои хитрости, например, искомый сигнал может быть в одном сообщении, но отображаться только при наличии одного или нескольких других сообщений. Тут важно набраться терпения и искать корректные сочетания и последовательности.
Сравнение с диагностическими запросами - более замороченный способ, требует больше оборудования и подходит только для сигналов, описанных в OBD2, поэтому лучше оставлять его на крайний случай.
записываем лог, одновременно посылая диагностические OBD2-запросы, например, спрашивая значение RPM;
скармливаем SavvyCAN лог, DBC-файл с описанием OBD2-запросов и разрабатываемый DBC-файл подопытного автомобиля;
строим два графика: референсный на базе OBD2 и предполагаемый;
сравниваем оба графика между собой, убеждаемся, что сигналы похожи или нет.
Попробуем на реальных данных, построив два графика 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 умеет симулировать гораздо больше событий, которые можно выводить на панель. Список действий для углубленного уровня:
погасить все незадействованные лампочки (чтобы они не отвлекали во время игры) с помощью CAN-сигналов и конфигурации панели;
выводить наибольшее количество возможной информации на панель;
отредактировать Lua-скрипт BeamNG для передачи новых переменных;
изменить конфигурацию панели для расширенной индикации.
Эта бесконечная гирлянда
Начнем с простого - основных контрольных ламп в центральной части панели, сигналы которых уже известны из прошлого раздела. Раньше они просто выключались, чтобы не маячить, но теперь можно задействовать их по прямому назначению, нужно только найти родственную переменную в 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 минут:
Теперь время всегда на виду.
Конфигурация и ее причуды
Конфигурация блоков - понятие растяжимое и каждый автопроизводитель подходит к ней по-своему, но основной смысл всегда один: изменение настроек/параметров без изменения самой прошивки.
Конфигурация представляет интерес для исследований по двум причинам:
практическая: расширение набора штатных функций, получение информации о возможных комплектациях и опциях автомобиля;
археологическая: конфигурация иногда наглядно иллюстрирует эволюцию, которую прошел автомобиль, переходя от ранних датчиков и актуаторов к более современным и технологичным (особенно если модель задержалась на рынке).
Для выведения большего количества информации на панель, чем в прошлом разделе, необходимо научиться включать дополнительные функции. Например, чтобы моргать лампочкой ESC, нужно сначала найти способ включить эту опцию через UDS, а затем найти сообщение, управляющее лампочкой. Задачу усложняет отсутствие подопытного автомобиля в нужной комплектации.
В простом случае, этот процесс включает четыре этапа:
поиск возможного DID, который может отвечать за состояние функции;
подбор и запись нового значения в DID, например, 1 = ВКЛ, 0 = ВЫКЛ;
анализ поведения панели, например, могли загореться новые лампы;
подбор сообщения и управляющего сигнала с помощью фаззинга.
Как показали эксперименты, из этих правил бывают исключения. Некоторые опции даже не потребовалось включать отдельно, а некоторые включались, но никак не выдавали себя. Иногда панель могла самостоятельно изменять свою конфигурацию, увидев в трафике сообщение от конкретной системы - так было с типом коробки передач. Вероятнее всего, такое самообучение было добавлено для упрощения установки.
Сканирование 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.