- Разработка панели приборов на основе raspberry pi и 7″ дисплея
- Can scanner на arduino
- Can sniffer на arduino
- Can sniffer на pc с использованием wxwidgets
- Can сниффер из arduino uno
- Can шина
- Compatible hardware
- Examples
- License
- Using the arduino ide library manager
- Wiring the mcp2515 shield with obd on arduino |
- Видео работы цифровой панели приборов на базе raspberry pi
- Заливаем скетч в arduino с помощью aduino ide 1.0.6 (использовал эту версию).
- Настройка блютуз модуля hc-05 для работы
- Подслушиваем запросы с помощью диагностической системы vag-com (vcds)
- Приложение на телефон виртуальная панель приборов
- Софт панели приборов на python и kivy (ui framework)
- Список требуемых деталей для сборки бк
- Теперь нужно собрать схему arduino блютуз lcd-экран
- Управление
- Шаг 2: изготовить плату и всё спаять
- Шаг 3: внешнее подключение
- Шаг 4: подготовьте и установите пластик илс
- Шаг 5: закачать код
- Шаг 6: демонстрация
Разработка панели приборов на основе raspberry pi и 7″ дисплея
В качестве аппаратной части я выбрал Raspberry Pi. Была идея использовать Android планшет, но показалось, что на Raspberry Pi будет проще и быстрее. В итоге докупил официальный 7″ дисплей, и сделал CAN шилд из модуля TJA1050 Niren.
OBD2 штекер использовал от старого ELM327 адаптера.
Используются контакты: CAN_L, CAN_H, 12, GND.
Тесты в машине прошли успешно и теперь нужно было все собрать. Плату дисплея, Raspberry Pi и блок питания разместил на куске черного пластика, очень удачно подобрал пластмассовые втулки, с ними ничего не болтается и надежно закреплено.
Местом установки выбрал бардачок на торпедо, которым я не пользуюсь. По примеркам в него как раз помещается весь бутерброд.
Напильником довел лист черного пластика до размера крышки бардачка, к нему прикрепил бутерброд и дисплей. Для прототипа сойдет, а 3D модель с крышкой для дисплея и всеми нужными крепежами уже в разработке.
Can scanner на arduino
Первый прибыл шилд для классической Arduino UNO. Да он стоит значительно дороже своих более мелких собратьев, но он имеет на борту всё необходимое и даже две кнопки.
Именно с ним я и начал все эксперименты. Собрал простую схему с этим шилдом и жидкокристаллическим двухстрочным экраном. Цель была — вывести на экран хоть какие-то данные. Перебирал различные библиотеки для работы с CAN шиной на Arduino (сразу скажу, что правильная и рабочая библиотека называется CAN-BUS Shield by Seeed Studio с заголовочным файлом mcp_can.h), поменял кварцевый резонатор на шилде на 16 МГц (изначально стоял 8 МГц) — данных не было.
На шилде установлены две микросхемы: контроллер CAN шины MCP2515 и драйвер CAN шины TJA1050. Почитав документацию и различные форумы, решил поменять TJA1050 на более каноничный драйвер MCP2551 и данные появились. Возможно TJA1050 была изначально неисправна, так как с её подключением двумя проводками ошибиться было очень сложно, к тому же я использовал OBD и DB9 разъёмы для подключения.
За пару часов был написан простой CAN scanner, который выводил на жидкокристаллический дисплей номер захваченного пакета, его ID и до 8 байтов данных этого пакета.
Вот тут и пригодились кнопочки на шилде, которыми я реализовал переключение между номером отображаемого пакета.
Начало было положено, надо переходить к более интересной реализации.
Can sniffer на arduino
Задача стояла достаточно простая:
С первыми двумя задачами я вообще не видел никаких проблем. Библиотека предоставляла прерывание при приёме очередного пакета данных и удобные функции для получения данных. А вот отправку данных в сторону компьютера решил сделать через библиотеку CyberLib, которая устраняет некоторые накладные расходы всей платформы Arduino, за счёт чего можно немного разгрузить процессор для обработки данных. Позже от этой библиотеки пришлось отказаться.
Для того, чтобы отправляемые данные корректно обрабатывались на стороне компьютера, перед каждой очередной порцией данных в поток вставляется префикс из четырёх байтов 0xAA55AA55 (почему-то вспомнились эти байты по последним двум байтам загрузочного сектора DOS, только они там были в другом порядке). Логика такая:
На этом программная часть в Arduino, на тот момент, была завершена. Позже она была значительно переделана, но общая концепция не поменялась.
Так же я написал простой генератор пакетов данных для отладки, чтобы отлаживаться дома — он просто отправляет в последовательный порт пакеты со случайными данными, что позволяет отлаживать приложение на компьютере в комфортных условиях.
Примерно в это же время прибыли более миниатюрные компоненты Arduino Nano и Mini CAN shield.
Я спроектировал небольшой корпус, распечатал его и разместил внутри все компоненты.
Снаружи с одной стороны OBD разъём, с другой — Mini USB. Внутри имеется переключатель для терминирующего резистора.
Can sniffer на pc с использованием wxwidgets
Набросал простую заготовку программы на C#, которая выводит в Grid получаемые данные. И пошёл проверять в автомобиль. Только пошёл не со своим ноутбуком, так как у него батарея давно приказала долго жить и использовался он как стационарный компьютер, а взял нетбук с очень слабым процессором.
То что я увидел… Я ничего не увидел. Оба ядра загружены на 100%, интерфейс приложения не реагирует. Но на моём компьютере, который всё-таки значительно шустрее нетбука, с генератором случайных пакетов приложение нормально работало и отображало данные.
Ранее я в нескольких проектах использовал библиотеку wxWidgets и о ней у меня только приятные впечатления. Она легковесная, нет необходимости тащить с собой различные библиотеки и даже кросс-платформенная, что вселяет надежду, что интерфейсную часть кода можно перенести без значительных переделок на другие платформы. В конце статьи будет ссылка на скомпилированную программу, если возиться со всем этим не будет желания.
Можно скачать и посмотреть
видео
(менее восьми минут), а можно выполнить 6 шагов по описанию ниже.
Установка и компиляция wxWidgets:
1. Скачать и установить wxWidgets если это установщик, либо распаковать, если это архив
2. Создать переменную окружения WXWIN указывающую на папку, куда установили или распаковали (например C:wxWidgets):
Свойства системы -> Дополнительные параметры системы -> Переменные среды -> Создать
WXWIN = C:wxWidgets
3. Из папки C:wxWidgetsbuildmsw открыть файл решения под соответствующую Visual Studio (wx_vc16.sln для Visual Studio 2021)
4. В Solution Expolorer, с помощью клавиши Shift, выделить все проекты, кроме _custom_build и зайти в Properties проектов.
5. В разделе C/C -> Code Generation изменить параметр Runtime Library:
Для конфигурации Debug выбрать /MTd
Для конфигурации Release выбрать /MT
6. Скомпилировать библиотеки wxWidgets по очереди для Debug и Release конфигураций.
Пробное приложение и настройка проекта в Visual Studio (для проверки)
1. В Visual Studio создать Empty Project с указанием типа приложения Desktop Application (.exe)
2. В окне View -> Property Manager для своего проекта правой кнопкой выбрать меню Add existing property sheet… и выбрать файл:
C:wxWidgetswxwidgets.props
3. Создать файл main.cpp и скопировать в него содержимое файла:
C:wxWidgetssamplesminimalminimal.cpp
4. В настройках проекта C/C -> Code Generation изменить (если пункт не появился — сделать пробную сборку):
Runtime Library для конфигурации Debug: /MTd
Runtime Library для конфигурации Release: /MT
5. Дополнительно, если необходимы привилегии UAC, в разделе Linker -> Manifest File:
UAC Execution Level: requireAdministrator
6. Для добавления иконки exe-файлу надо добавить ресурсный файл со следующим содержимым:
#include «wxmswwx.rc»
wxicon icon app_icon.ico
Первый реализованный прототип на C и wxWidgets показал, что даже нетбук справляется с отображением данных в таблице и я приступил к разработке задуманного.
Архитектурно программа состоит из двух потоков: интерфейсный и поток работы с последовательным портом. Никаких невероятно интересных алгоритмов не применялось. Код обильно снабжён комментариями и должен быть довольно понятен. Ссылка на исходники будет в конце статьи.
Первое что было сделано — раскраска ячеек данных в таблице по давности получения этих данных. Уже в первом прототипе, глядя на 17 строк данных меняющихся непрерывно значений, я понял, что надо как-то различать свежие данные и данные, которые не изменяются или меняется редко. Сделал раскраску в два этапа:
Сразу же стало наглядно видно, какие ячейки вообще не используются, какие содержат сигналы счётчиков. Поиск же интересующих изменяющихся значений значительно упрощается. Здесь и далее все изображения анимированные. Если анимация не работает в статье (на некоторых мобильных браузерах) — кликайте по изображению для открытия полной версии анимации.
Далее мне захотелось всё-таки проверить, справляется ли последовательный порт с потоком данных. Для этого я на стороне Arduino добавил счётчики количества принятых пакетов и счетчик байтов в пакете. Эти счётчики отправляются на компьютер в пакете с идентификатором 0x000.
Программа при получении этих данных не выводит их в таблицу, а отображает в отдельных информационных полях сверху. Полученные результаты даже весьма понравились. В среднем принимается до 750 пакетов/с со скоростью до 9,5 кБ/с, а это где в районе до 80 кбит/с, что вполне по силам последовательному порту. Но всё равно, обмен данными настроен по умолчанию на 500 кбит/с, пусть лучше будет запас.
Добавление возможности записи данных в журнал появилось после того, как подключил параллельно к OBD интерфейсу диагностический адаптер ELM327 и связав его с телефоном, попробовал читать различные данные. Данные пробегали настолько быстро, что увидеть их невозможно.
Записав всё это в журнал, можно потом спокойно сесть и посмотреть передаваемые данные. Для этого в журнал могут записываться даже ASCII текстовые данные. Так же можно выбирать тип файла, символ разделитель и настроить фильтр пакетов кликом в таблице по указанному идентификатору пакета и нажатию кнопки «Добавить ID в фильтр» (по умолчанию записываются все данные), если запись всех данных избыточна.
Именно тогда пришло осознание, что все приложения для телефона, которые производят всякую «диагностику» через связку ELM327 и телефон, не общаются напрямую с CAN шиной автомобиля. Они всего лишь используют функционал диагностики OBD через CAN шину посредством обращения к CAN ID 0x7E0.
Обычно это адрес контроллера мотора (ЭБУ), ответ же от него приходит в пакете с идентификатором 0x7E8. А вот все остальные пакеты данных — это так называемый Vendor Specific и ни один производитель так просто их не раскроет (хотя есть пример: Ford выпустил SDK для своих автомобилей).
Продолжая изучать что же передаётся в этих пакетах пришёл к ещё одной идее: при клике на ячейку в таблице, в окне программы справа выводить двоичное и десятичное значение этого байта, а так же брать следующий байт и дополнять до слова. Далее это слово умножать на некий коэффициент и получить десятичный результат.
Звучит не очень понятно, но вот в связи с чем это делалось: обороты мотора приходят в пакете CAN ID 0x180, в первых двух байтах. Эти два байта дают некое слово, которое пропорционально оборотам. Если значение этого слова разделить на 8, то получатся текущие обороты.
Поэтому указывается множитель 0,125, как обратная величина от 8. Далее это слово визуализируется в графике с динамической подстройкой по амплитуде. В принципе, множитель можно искать в обратной последовательности: нашёл ячейки, которые по графику очень похожи на обороты мотора или ещё что-то искомое, после чего подгоняется множитель для получения действительных значений.
Ну а двоичное представление позволяет искать различные битовые индикаторы. Например поиск индикаторов указателей поворота сводится к тому, чтобы включить их и наблюдать какая ячейка начинает изменяться, в примере ниже это CAN ID 0x481 байт 2.
И напоследок мне понадобилось сделать отправку некоторых управляющих данных в CAN шину и посмотреть реакцию на эти команды. В программу на Arduino был добавлен код, который принимает данные со стороны компьютера и передаёт в CAN шину.
Именно на этом этапе пришлось отказаться от CyberLib, так как у неё не было поддержки прерывания поступления данных в буфер последовательного порта. В программе на компьютере добавил несколько текстовых полей, в которые можно ввести различные параметры и таблицу для просмотра ответа исполнительного устройства.
Can сниффер из arduino uno
Чтобы послушать, что отправляет VCDS в CAN шину я собрал сниффер на макетке из Arduino и модуля MCP2515 TJA1050 Niren.
Схема подключения следующая:
Can шина
Описывать технические подробности CAN шины в деталях — удел документации. В данной статье достаточно знать, что она:
Полистав странички одного известного интернет магазина из поднебесной, я заказал несколько различных вариантов шилдов и пошёл изучать особенности электрических сигналов в автомобиле. Подопытным автомобилем выступил LADA Kalina Cross с 127-ым мотором и электронным блоком управления ИТЭЛМА М74.5 CAN.
Подключаюсь в диагностический разъём OBD (контакты 6 и 14) и смотрю осциллографом, что там имеется. После поворота ключа зажигания начинают бегать пакеты с амплитудой до 2,5 В. Ставлю паузу на осциллографе и смотрю на пакет.
Заметны стартовые и стоповые биты, какие-то данные в пакете. На тот момент я уже знал, что скорость передачи данных ожидается 500 кбит/с, как наиболее частая для моторной CAN шины. Длительность пакета получается около 230 мкс и перед пакетом наблюдается довольно большая пауза в передаче данных. Масштабирую время и вижу три пакета и паузы между ними.
Если сложить длительность передачи данных и паузу между пакетам получается, что передача одной порции данных занимает около 1 мс.
К чему я это всё вывожу? А вопрос чисто практический: хватит ли скорости последовательного порта для передачи всех данных? И исходя из увиденного, можно сделать вывод, что скорость 500 кбит/с развивается внутри пакета, который занимает примерно четверть времени на передачу.
Значит средняя скорость передачи будет вчетверо меньшей. На тот момент я ещё не располагал тестами скорости последовательного интерфейса Arduino и забегая вперёд скажу, что даже с самым распространённым преобразователем Serial to USB CH340 стабильно работает скорость в 2 Мбит/с.
Compatible hardware
Please see the CAN library’s compatible hardware.
Examples
See examples folder.
License
This library is licensed under the MIT Licence.
Using the arduino ide library manager
- Choose
Sketch->Include Library->Manage Libraries... - Type
OBD2into the search box. - Click the row to select the library.
- Click the
Installbutton to install the library.
Wiring the mcp2515 shield with obd on arduino |
/*
14CORE | CAN BUS DEMO CODE
===============================================================
CAN-BUS Shield Bridge Demo
Requires two CAN Bus Shields
First shield should be left default = CS is on D9 and INT is on D2
Second shield needs modified so CS is on D10 and INT is on D3
Written by Cory J. Fowler
December 09, 2021
*/
#include <mcp_can.h>
#include <SPI.h>
longunsignedintrxId;
unsignedcharlen=0;
unsignedcharrxBuf[8];
MCP_CAN CAN0(9); // Set first CAN interface CS to pin 9
MCP_CAN CAN1(10); // Set second CAN interface CS to pin 10
voidsetup()
{
Serial.begin(115200);
pinMode(2,INPUT); // Setting pin 2 for first CAN bus /INT input
pinMode(3,INPUT); // Setting pin 3 for second CAN bus /INT input
CAN0.begin(CAN_500KBPS); // init first CAN bus : baudrate = 500k
CAN1.begin(CAN_250KBPS); // init second CAN bus : baudrate = 250k
Serial.println(“CAN Bridge Example.”);
}
voidloop()
{
if(!digitalRead(2)) // If pin 2 is low, read receive buffer of first CAN interface
{
CAN0.readMsgBuf(&len,rxBuf); // Read data: len = data length, buf = data byte(s)
rxId=CAN0.getCanId(); // Get message ID
CAN1.sendMsgBuf(rxId,1,len,rxBuf); // Unfortunately this library does not return if the received
// message was standard or extended. So sending as extended.
Serial.println(“Received on CAN0”);
}
if(!digitalRead(3)) // If pin 3 is low, read receive buffer of second CAN interface
{
CAN1.readMsgBuf(&len,rxBuf); // Read data: len = data length, buf = data byte(s)
rxId=CAN1.getCanId(); // Get message ID
CAN0.sendMsgBuf(rxId,1,len,rxBuf); // Unfortunately this library does not return if the received
// message was standard or extended. So sending as extended.
Serial.println(“Received on CAN1”);
}
}
Видео работы цифровой панели приборов на базе raspberry pi
ОБНОВЛЕНО 24.06.2021
Заливаем скетч в arduino с помощью aduino ide 1.0.6 (использовал эту версию).
Единственное, в скетче присутствуют переменные, которую нужно подправить.
Нужно будет обязательно учесть три переменных:
1) ED=1.998 Например объем двигателя в литрах 1.398;2) VE_correct=1.0; Корректировка объёмного КПД ДВС по таблице: (если расход реально меньше — то уменьшаем значение в процентном соотношении). Если не хотите калибровать добейтесь чтобы при прогретом двигателе мгновенный расход в л/час был в районе половины обьема двигателя;5)speed_korrect_val=1; Корректировка скорости машины, смотреть по GPS/
Настройка блютуз модуля hc-05 для работы
Подпаиваем провода к пинам блютуза:картинку с выходами смотреть в описании требуемых деталей
- 1 — это TX
- 2 — это RX
- 12 — это 3.3V
- 13 — это GND
- 34 — на этот вход тоже кидаем 3,3 V (нужен для перевода модуля в режим настройки с помощью AT команд).
Подключаем блютуз модуль к ардуине для его настройки
- 1 — TX модуля в 6 пин ардуины. (внимание будет TX в TX это не ошибка!)
- 2 — RX модуля в 7 пин ардуины. (аналогично не ошибка!)
- 12 — и 34 пин к 3,3V ардуины.
- 13 — GND ардуины.
Открываем Aduino IDE 1.0.6 (использовал эту версию) и заливаем скетч через USB порт в плату.
#include <SoftwareSerial.h>SoftwareSerial BTSerial(6, 7); // TX | RXvoid setup(){Serial.begin(9600);Serial.println(‘Enter AT commands:’);BTSerial.begin(38400);}
void loop(){if (BTSerial.available())Serial.write(BTSerial.read());if (Serial.available())BTSerial.write(Serial.read());}
После успешной загрузки скетча открываем: Сервис->Монитор порта. Далее снизу ставим скорость 9600 бод и NL CR вместе.
Далее вводим команды по одной и нажимаем [Послать]. После каждого ввода должен быть ответ ok.
AT // (возможно 1 раз вылетит Error, не пугайтесь… это нормально, повторите опять)AT NAME=Car //Присваиваем имя модулю CarAT ROLE=1 // Переводим модуль в режим МастерAT PSWD=1234 // Ставим пароль 1234 как на OBD ELM327AT BIND=AABB,CC,112233 //Прописываем Mac адрес OBD ELM327.AT CMODE=1 // Подключение модуля с фиксированным адресомAT UART=9600,0,0 // Скорость работы по UART
Заметьте, что mac-адрес вида: «AA:BB:CC:11:22:33» вводится как «AABB,CC,112233». MAC- адрес своего модуля ELM327 можете посмотреть, подключившись для начала на него со своего мобильника. (Стандартные пароли обычно: 1234, 6789, 0000).
Всё, настройка модуля Bluetooth закончена.
Подслушиваем запросы с помощью диагностической системы vag-com (vcds)
Описание VCDS с официального сайта
Приложение на телефон виртуальная панель приборов


Если есть желание поддержать проект, то вот ссылка на приложение, принимаю любые замечания и предложения!
VAG Virtual Cockpit
Софт панели приборов на python и kivy (ui framework)
Параллельно со сборкой самой панели приборов я вел разработку приложения для отображения информации с датчиков. В самом начале я не планировал какой либо дизайн.

Первая версия панели приборов
По мере разработки решил визуализировать данные более наглядно. Хотел гоночный дизайн, а получилось, что-то в стиле 80-х.

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

Третья версия панели приборов
Ранее, я никогда не разрабатывал графические приложения под Linux поэтому не знал с чего начать. Вариант на вебе простой в разработке, но слишком много лишних компонентов: иксы, браузер, nodejs, хотелось быстрой загрузки. Попробовав Qt PySide2 я понял, что это займет у меня много времени, т.к. мало опыта.
Kivy позволяет запускать приложение без Иксов, прямо из консоли, в качестве рендера используется OpenGL. Благодаря этому полная загрузка системы может происходить за 10 секунд.
import can
import os
import sys
from threading import Thread
import time
os.environ['KIVY_GL_BACKEND'] = 'gl'
os.environ['KIVY_WINDOW'] = 'egl_rpi'
from kivy.app import App
from kivy.properties import NumericProperty
from kivy.properties import BoundedNumericProperty
from kivy.properties import StringProperty
from kivy.uix.label import Label
from kivy.uix.image import Image
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget
from kivy.uix.scatter import Scatter
from kivy.animation import Animation
messageCommands = { 'GET_DOORS_COMMAND': 0x220D, 'GET_OIL_TEMPERATURE' : 0x202F, 'GET_OUTDOOR_TEMPERATURE' : 0x220C, 'GET_INDOOR_TEMPERATURE' : 0x2613, 'GET_COOLANT_TEMPERATURE' : 0xF405, 'GET_SPEED' : 0xF40D, 'GET_RPM' : 0xF40C, 'GET_KM_LEFT': 0x2294, 'GET_FUEL_LEFT': 0x2206, 'GET_TIME': 0x2216
}
bus = can.interface.Bus(channel='can0', bustype='socketcan')# -*- coding: utf-8 -*-
import can
import os
import sys
from threading import Thread
import time
os.environ['KIVY_GL_BACKEND'] = 'gl'
os.environ['KIVY_WINDOW'] = 'egl_rpi'
from kivy.app import App
from kivy.properties import NumericProperty
from kivy.properties import BoundedNumericProperty
from kivy.properties import StringProperty
from kivy.uix.label import Label
from kivy.uix.image import Image
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget
from kivy.uix.scatter import Scatter
from kivy.animation import Animation
messageCommands = { 'GET_DOORS_COMMAND': 0x220D, 'GET_OIL_TEMPERATURE' : 0x202F, 'GET_OUTDOOR_TEMPERATURE' : 0x220C, 'GET_INDOOR_TEMPERATURE' : 0x2613, 'GET_COOLANT_TEMPERATURE' : 0xF405, 'GET_SPEED' : 0xF40D, 'GET_RPM' : 0xF40C, 'GET_KM_LEFT': 0x2294, 'GET_FUEL_LEFT': 0x2206, 'GET_TIME': 0x2216
}
bus = can.interface.Bus(channel='can0', bustype='socketcan')
class PropertyState: def __init__(self, last, current): self.last = last self.current = current def lastIsNotNow(self): return self.last is not self.current
class CanListener(can.Listener): def __init__(self, dashboard): self.dashboard = dashboard self.speedStates = PropertyState(None,None) self.rpmStates = PropertyState(None,None) self.kmLeftStates = PropertyState(None,None) self.coolantTemperatureStates = PropertyState(None,None) self.oilTempratureStates = PropertyState(None,None) self.timeStates = PropertyState(None,None) self.outDoorTemperatureStates = PropertyState(None,None) self.doorsStates = PropertyState(None,None) self.carMinimized = True def on_message_received(self, message): messageCommand = message.data[3] | message.data[2] << 8 if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_SPEED']: self.speedStates.current = message.data[4] if self.speedStates.lastIsNotNow(): self.dashboard.speedometer.text = str(self.speedStates.current) self.speedStates.last = self.speedStates.current if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_RPM']: self.rpmStates.current = message.data[5] | message.data[4] << 8 if self.rpmStates.lastIsNotNow(): self.dashboard.rpm.value = self.rpmStates.current/4 self.rpmStates.last = self.rpmStates.current if message.arbitration_id == 0x35B: self.rpmStates.current = message.data[2] | message.data[1] << 8 if self.rpmStates.lastIsNotNow(): self.dashboard.rpm.value = self.rpmStates.current/4 self.rpmStates.last = self.rpmStates.current if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_KM_LEFT']: self.kmLeftStates.current = message.data[5] | message.data[4] << 8 if self.kmLeftStates.lastIsNotNow(): self.dashboard.kmLeftLabel.text = str(self.kmLeftStates.current) self.kmLeftStates.last = self.kmLeftStates.current if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_COOLANT_TEMPERATURE']: self.coolantTemperatureStates.current = message.data[4] if self.coolantTemperatureStates.lastIsNotNow(): self.dashboard.coolantLabel.text = str(self.coolantTemperatureStates.current-81) self.coolantTemperatureStates.last = self.coolantTemperatureStates.current if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_OIL_TEMPERATURE']: self.oilTempratureStates.current = message.data[4] if self.oilTempratureStates.lastIsNotNow(): self.dashboard.oilLabel.text = str(self.oilTempratureStates.current-58) self.oilTempratureStates.last = self.oilTempratureStates.current if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_TIME']: self.timeStates.current = message.data[5] | message.data[4] << 8 if self.timeStates.lastIsNotNow(): self.dashboard.clock.text = str(message.data[4]) ":" str(message.data[5]) self.timeStates.last = self.timeStates.current if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_OUTDOOR_TEMPERATURE']: self.outDoorTemperatureStates.current = float(message.data[4]) if self.outDoorTemperatureStates.lastIsNotNow(): self.dashboard.outDoorTemperatureLabel.text = str((self.outDoorTemperatureStates.current - 100)/2) self.outDoorTemperatureStates.last = self.outDoorTemperatureStates.current if message.arbitration_id == 0x77E and messageCommand == messageCommands['GET_DOORS_COMMAND']: self.doorsStates.current = message.data[4] if self.doorsStates.lastIsNotNow(): self.doorsStates.last = self.doorsStates.current self.dashboard.car.doorsStates=message.data[4] # all doors closed -> minimize car if self.doorsStates.current == 0x55: self.dashboard.minimizeCar() self.carMinimized = True else: if self.carMinimized: self.dashboard.maximizeCar() self.carMinimized = False
class Dashboard(FloatLayout): def __init__(self,**kwargs): super(Dashboard,self).__init__(**kwargs) # Background self.backgroundImage = Image(source='bg.png') self.add_widget(self.backgroundImage) # RPM self.rpm = Gauge(file_gauge = "gauge512.png", unit = 0.023, value=0, size_gauge=512, pos=(0,0)) self.add_widget(self.rpm) self.rpm.value = -200 # Speedometer self.speedometer = Label(text='0', font_size=80, font_name='hemi_head_bd_it.ttf', pos=(0,-15)) self.add_widget(self.speedometer) # KM LEFT self.kmLeftLabel = Label(text='000', font_name='Avenir.ttc', halign="right", text_size=self.size, font_size=25, pos=(278,233)) self.add_widget(self.kmLeftLabel) # COOLANT TEMPEARATURE self.coolantLabel = Label(text='00', font_name='hemi_head_bd_it.ttf', halign="right", text_size=self.size, font_size=27, pos=(295,-168)) self.add_widget(self.coolantLabel) # OIL TEMPERATURE self.oilLabel = Label(text='00', font_name='hemi_head_bd_it.ttf', halign="right", text_size=self.size, font_size=27, pos=(-385,-168)) self.add_widget(self.oilLabel) # CLOCK self.clock = Label(text='00:00', font_name='Avenir.ttc', font_size=27, pos=(-116,-202)) self.add_widget(self.clock) # OUTDOOR TEMPERATURE self.outDoorTemperatureLabel = Label(text='00.0', font_name='Avenir.ttc', halign="right", text_size=self.size, font_size=27, pos=(76,-169)) self.add_widget(self.outDoorTemperatureLabel) # CAR DOORS self.car = Car(pos=(257,84)) self.add_widget(self.car) def minimizeCar(self, *args): print("min") anim = Animation(scale=0.5, opacity = 0, x = 400, y = 240, t='linear', duration=0.5) anim.start(self.car) animRpm = Animation(scale=1, opacity = 1, x = 80, y = -5, t='linear', duration=0.5) animRpm.start(self.rpm) def maximizeCar(self, *args): print("max") anim = Animation(scale=1, opacity = 1, x=257, y=84, t='linear', duration=0.5) anim.start(self.car) animRpm = Animation(scale=0.5, opacity = 0, x = 80, y = -5, t='linear', duration=0.5) animRpm.start(self.rpm)
class Car(Scatter): carImage = StringProperty("car362/car.png") driverDoorClosedImage = StringProperty("car362/driverClosedDoor.png") driverDoorOpenedImage = StringProperty("car362/driverOpenedDoor.png") passangerDoorClosedImage = StringProperty("car362/passangerClosedDoor.png") passangerDoorOpenedImage = StringProperty("car362/passangerOpenedDoor.png") leftDoorClosedImage = StringProperty("car362/leftClosedDoor.png") leftDoorOpenedImage = StringProperty("car362/leftOpenedDoor.png") rightDoorClosedImage = StringProperty("car362/rightClosedDoor.png") rightDoorOpenedImage = StringProperty("car362/rightOpenedDoor.png") doorsStates = NumericProperty(0) size = (286, 362) def __init__(self, **kwargs): super(Car, self).__init__(**kwargs) _car = Image(source=self.carImage, size=self.size) self.driverDoorOpened = Image(source=self.driverDoorOpenedImage, size=self.size) self.passangerDoorOpened = Image(source=self.passangerDoorOpenedImage, size=self.size) self.leftDoorOpened = Image(source=self.leftDoorOpenedImage, size=self.size) self.rightDoorOpened = Image(source=self.rightDoorOpenedImage, size=self.size) self.driverDoorClosed = Image(source=self.driverDoorClosedImage, size=self.size) self.passangerDoorClosed = Image(source=self.passangerDoorClosedImage, size=self.size) self.leftDoorClosed = Image(source=self.leftDoorClosedImage, size=self.size) self.rightDoorClosed = Image(source=self.rightDoorClosedImage, size=self.size) self.add_widget(_car) self.add_widget(self.driverDoorOpened) self.add_widget(self.passangerDoorOpened) self.add_widget(self.leftDoorOpened) self.add_widget(self.rightDoorOpened) self.bind(doorsStates=self._update) def _update(self, *args): driverDoorStates = self.doorsStates&1 passangerDoorStates = self.doorsStates&4 leftDoorStates = self.doorsStates&16 rightDoorStates = self.doorsStates&64 if driverDoorStates != 0: try: self.remove_widget(self.driverDoorOpened) self.add_widget(self.driverDoorClosed) except: pass else: try: self.remove_widget(self.driverDoorClosed) self.add_widget(self.driverDoorOpened) except: pass if passangerDoorStates != 0: try: self.remove_widget(self.passangerDoorOpened) self.add_widget(self.passangerDoorClosed) except: pass else: try: self.remove_widget(self.passangerDoorClosed) self.add_widget(self.passangerDoorOpened) except: pass if leftDoorStates != 0: try: self.remove_widget(self.leftDoorOpened) self.add_widget(self.leftDoorClosed) except: pass else: try: self.remove_widget(self.leftDoorClosed) self.add_widget(self.leftDoorOpened) except: pass if rightDoorStates != 0: try: self.remove_widget(self.rightDoorOpened) self.add_widget(self.rightDoorClosed) except: pass else: try: self.remove_widget(self.rightDoorClosed) self.add_widget(self.rightDoorOpened) except: pass
class Gauge(Scatter): unit = NumericProperty(1.125) zero = NumericProperty(116) value = NumericProperty(10) #BoundedNumericProperty(0, min=0, max=360, errorvalue=0) size_gauge = BoundedNumericProperty(512, min=128, max=512, errorvalue=128) size_text = NumericProperty(10) file_gauge = StringProperty("") def __init__(self, **kwargs): super(Gauge, self).__init__(**kwargs) self._gauge = Scatter( size=(self.size_gauge, self.size_gauge), do_rotation=False, do_scale=False, do_translation=False ) _img_gauge = Image(source=self.file_gauge, size=(self.size_gauge, self.size_gauge)) self._needle = Scatter( size=(self.size_gauge, self.size_gauge), do_rotation=False, do_scale=False, do_translation=False ) _img_needle = Image(source="arrow512.png", size=(self.size_gauge, self.size_gauge)) self._gauge.add_widget(_img_gauge) self._needle.add_widget(_img_needle) self.add_widget(self._gauge) self.add_widget(self._needle) self.bind(pos=self._update) self.bind(size=self._update) self.bind(value=self._turn) def _update(self, *args): self._gauge.pos = self.pos self._needle.pos = (self.x, self.y) self._needle.center = self._gauge.center def _turn(self, *args): self._needle.center_x = self._gauge.center_x self._needle.center_y = self._gauge.center_y a = Animation(rotation=-self.value*self.unit self.zero, t='in_out_quad',duration=0.05) a.start(self._needle)
class requestsLoop(Thread): def __init__(self): Thread.__init__(self) self.daemon = True self.start() canCommands = [ can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_DOORS_COMMAND'] >> 8, messageCommands['GET_DOORS_COMMAND'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_SPEED'] >> 8, messageCommands['GET_SPEED'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_KM_LEFT'] >> 8, messageCommands['GET_KM_LEFT'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_RPM'] >> 8, messageCommands['GET_RPM'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_OIL_TEMPERATURE'] >> 8, messageCommands['GET_OIL_TEMPERATURE'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_FUEL_LEFT'] >> 8, messageCommands['GET_FUEL_LEFT'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_OUTDOOR_TEMPERATURE'] >> 8, messageCommands['GET_OUTDOOR_TEMPERATURE'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x746, data=[0x03, 0x22, messageCommands['GET_INDOOR_TEMPERATURE'] >> 8, messageCommands['GET_INDOOR_TEMPERATURE'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_COOLANT_TEMPERATURE'] >> 8, messageCommands['GET_COOLANT_TEMPERATURE'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False), can.Message(arbitration_id=0x714, data=[0x03, 0x22, messageCommands['GET_TIME'] >> 8, messageCommands['GET_TIME'] & 0xff, 0x55, 0x55, 0x55, 0x55], extended_id=False) ] def run(self): while True: for command in self.canCommands: bus.send(command) time.sleep(0.005)
class BoxApp(App): def build(self): dashboard = Dashboard(); listener = CanListener(dashboard) can.Notifier(bus, [listener]) return dashboard
if __name__ == "__main__": # Send requests requestsLoop() _old_excepthook = sys.excepthook def myexcepthook(exctype, value, traceback): if exctype == KeyboardInterrupt: print "Handler code goes here" else: _old_excepthook(exctype, value, traceback) sys.excepthook = myexcepthook # Show dashboard BoxApp().run()Алгоритм работы следующий, используется 3 потока:
- В главном потоке работаем с графическими элементы (спидометр, тахометр, часы, температуры и др) на экране
- Во втором потоке каждые 5 мс делаем опрос следующего датчика
- В третьем потоке слушаем CAN шину, получив ответ парсим его и обновляем соответствующий графический элемент
Работает стабильно, самый долгий процесс в разработке был связан с рисованием дизайна. На данный момент обкатываю решение и потихоньку пишу мобильное приложение для iOS, чтобы любой мог попробовать цифровую панель приборов.
Проект цифровой панель приборов открытый. Рад буду предложениям и комментариям!
Список требуемых деталей для сборки бк
1) Arduino Uno R3 — 1 шт. ~ 7 долларов:
2) LCD2004 жк-модуль ~ 6 долларов:
3) Модуль Bluetooth HC-05 ~ 4 доллара:
4) OBD ELM327 Bluetooth сканер ~ 4 доллара:
5) Резистор 10 кОм подстроечный, бипер для звука, 2 кнопки для смены экранов, провода для соединений, корпус ~ 3 доллара.
Теперь нужно собрать схему arduino блютуз lcd-экран
Схема:
1.Начнем с подключения HC-05 Bluetooth модуля.
- 1 — TX модуля засовываем в 7 Pin (Rx) арудины (именно TX в RX, не так как ранее);
- 2 — RX модуля засовываем в 8 Pin (Tx) арудины;
- 12 — Pin (3,3V) модуля в Pin 3,3V ардуины;
- 13 — Pin (Gnd) в Gnd арудуины;
- 34 — Pin мы никуда не подключаем (заизолируйте или отпаяйте).
2. Подключаем монитор LCD.
- VSS экрана к GND ардуины;
- VDD экрана к 5V ардуины;
- V0 экрана к центральному выходу резистора;
- RS экрана к 12 пину ардуины;
- RW экрана к GND ардуины;
- E экрана к 11 пину ардуины;
- DB4 экрана к 5 пину ардуины;
- DB5 экрана к 4 пину ардуины;
- DB6 экрана к 3 пину ардуины;
- DB7 экрана к 2 пину ардуины;
- A — к 5V ардуины;
- K — GND ардуины.
Одну из оставшихся ног потенциометра пустить на GND ардуины.
Переменный резистор на 10кОм нужен, чтобы управлять контрастностью монитора, так что если при первом включении вы включите и ничего не увидите, попробуйте отрегулировать контрастность шрифта поворотом резистора.
3. Подключаем дополнительную кнопку для переключения экранов с данными.
[1 кнопка]: один конец от нормально-открытой кнопки подключаем в GND ардуино, а второй конец в пин 10.[2 кнопка]: GND пин 9.
Бипер для звуковых предупреждений подключить по следующей схеме ” ” к пину 13, а минус к GND ардуино.
Управление
[Кнопка 1], [кнопка 2] — листать экран вперед назад.
При включении при надписи «Connecting»… держать [кнопку 1] вход в режим показывания технологических экранов и параметров отдаваемых ЭБУ в 16-чном формате. Если будете включать БК не в машине то нужно отключить функцию опроса блютуз, надо продолжать держать две кнопки при надписи «Recovery»… до появлении надписи «All off»… а то экран будет все время пустой.
[Кнопка 1] [кнопка 2]: 4 секунды — Сброс журнала общего пробега и потраченного бензина на втором экране, также это сброс ошибок на экране информации об ошибках.
Шаг 2: изготовить плату и всё спаять

Ссылка на файлы платы в формате Gerber
Эти файлы можно использовать для заказа готовой платы в сервисах типа JLCPCB.
Шаг 3: внешнее подключение

Чтобы управлять питанием ELM327 и платы, не вынимая каждый раз кабель OBD2, нужно будет переподключить питание и землю. Для этого нужно будет открыть корпус сканера OBD и добраться до его контактов.
- Используя приведённую схему, найдите и отрежьте провод 12 В в середине.
- Зачистите его концы.
- Отрежьте и зачистите два красных провода, длиной такой же, как от контакта 12 В OBD2 до выключателя ИЛС.
- Отрежьте и зачистите 1 красный и 1 чёрный провода, длиной такой же, как от контактов питания OBD2 до клеммной колодки платы.
- Припаяйте провода 12 В так, чтобы выключатель ИЛС управлял пиатнием и ELM327, и платы.
- Используя приведённую выше схему OBD2, припаяйте чёрный провод к контакту GND OBD2, а другой его конец соедините с клеммной колодкой платы.
Затем подсоедините 3 контакта на плате под названием LED PWR к потенциометру сбоку ИЛС. Наконец, подсоедините разъём JST-мама к OLED дисплею.
Шаг 4: подготовьте и установите пластик илс
- Возьмите плексигласовый диск, отражающую плёнку, маркер и ножницы.
- Используйте плексигласовый диск и маркер, чтобы нарисовать на отражающей плёнке круг.
- Ножницами вырежьте круг.
- Наклейте вырезанный круг с одной стороны плексигласового диска.
- Вставьте его в разъём ИЛС (отражающей плёнкой к водителю).
Шаг 5: закачать код

Код для ESP32 и Teensy 3.5.
Не забудьте установить SD-карту в Teensy. Вы сможете записывать на неё скорость машины и обороты двигателя в формате CSV. Потом можно будет использовать, например, python, для построения графиков; привожу построенные мною графики.
Шаг 6: демонстрация
См. также:





