Архитектура приложения python для взаимодействия с obd-ii через can
Чтобы создать наше простое приложение на Python, мы будем использовать библиотеку Python CAN для управления сетью CAN. Вы также можете использовать API сокетов в Python для связи CAN, поскольку Python поддерживает CAN с версии 3.3, но на данный момент это более низкоуровневый подход.
Чтобы проиллюстрировать запрос OBD-II для PID 0x0C, как определено в стандарте для частоты вращения двигателя (RPM), мы представляем код ниже. Это будет:
- Создайте интерфейс CAN-шины
- Создайте ссылку на сообщение CAN для запроса
- Запрос сообщения CAN — это кадр CAN с DLC размером 8 байтов.
- Сообщение будет построено в следующем формате для стандарта SAE:
- Байт 0 — количество дополнительных байтов: 2
- Байт 1 — 1, чтобы показать текущие данные
- Байт 2 — запрашиваемый PID-код
- Байты с 3 по 7: они не используются, но ISO 15765-2 предлагает установить для них CCh
- Отправьте запрос в главный ЭБУ с идентификатором 0x7DF
- Получите сообщение и сравните его с ожидаемым идентификатором ответа 0x7E8
- Если мы получим сообщение от ожидаемого идентификатора ответа, он напечатает результат в шестнадцатеричном формате.
Чтобы выполнить наше приложение CAN, мы должны сначала настроить и включить сеть CAN в модуле. Интерфейс CAN1, физический, уже включен в его дереве устройств и обозначен как can0 на стороне Linux. Процесс настройки и включения может быть выполнен с помощью вызовов os.system () в Python, в которых мы настраиваем сеть CAN с битрейтом 500k.
# In the extension we shall bring up the interfaces directly within our Python app
os.system(“ip link set can0 type can bitrate 500000”)
time.sleep(1)
os.system(“ip link set can0 up”)
time.sleep(1)
# set up a CAN bus
bus = can.interface.Bus(channel=”can0″, bustype=’socketcan’)
# using defaults functional address
obd2_tx_arb_id = 0x7DF
obd2_rx_arb_id = 0x7E8
# CAN Frame for a PID Request of 0x0C (RPM) with SAE Standard
obd_req_data = [2, 1, 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC]
# Send our OBD-II query in CAN 11-bit format
msg = can.Message(arbitration_id=obd2_tx_arb_id, data=obd_req_data, is_extended_id=False)
bus.send(msg)
message = bus.recv(1.0) # Timeout in seconds.
if message is None:
print(‘Timeout occurred, no message.’)
sys.exit(1)
# Check if received the expected response message
if message.arbitration_id == obd2_rx_arb_id:
print(“Message received from OBD-II request!”)
hex_data = “”
for c in message.data:
hex_data = “X ” % c
print(“Response ID: X | Response Data: %s” % (message.arbitration_id, hex_data))
import time import os import sys import can # In the extension we shall bring up the interfaces directly within our Python app os.system(“ip link set can0 type can bitrate 500000”) time.sleep(1) os.system(“ip link set can0 up”) time.sleep(1) # set up a CAN bus bus=can.interface.Bus(channel=“can0”,bustype=‘socketcan’) # using defaults functional address obd2_tx_arb_id=0x7DF obd2_rx_arb_id=0x7E8 # CAN Frame for a PID Request of 0x0C (RPM) with SAE Standard obd_req_data=[2, 1, 0x0C, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC] # Send our OBD-II query in CAN 11-bit format msg=can.Message(arbitration_id=obd2_tx_arb_id,data=obd_req_data,is_extended_id=False) bus.send(msg) message=bus.recv(1.0)# Timeout in seconds. ifmessage isNone: print(‘Timeout occurred, no message.’) sys.exit(1) # Check if received the expected response message ifmessage.arbitration_id==obd2_rx_arb_id: print(“Message received from OBD-II request!”) hex_data=“” forcinmessage.data: hex_data =“X “%c print(“Response ID: X | Response Data: %s”%(message.arbitration_id,hex_data)) |
Вы можете загрузить этот код в свою цель, скопировав и вставив его с помощью редактора nano, который мы установили в наш образ контейнера, как показано в его Dockerfile. Другой способ — привязать этот контейнер к /home/torizon, чтобы упростить отправку кода через scp.
Имея приложение под рукой, давайте попробуем его.
Всего понемногу: автомобиль, облако, raspberry pi и can
В продолжении предыдущей заметки «Автомобиль, Интернет вещей и прочие технологии» хотелось бы рассмотреть идеи для воплощения автомобильного компьютера на уровне открытых проектов и немного затронуть современные тенденции в этом ключе.
Image: Bringing its A game: new 2021 Mercedes A-class hatch revealed – CAR Magazine
Интернет вещей – это не призрак далекого будущего, а вполне реализуемая концепция настоящего. Начиная от лампочек и завершая домашними голосовыми ассистентами – все эти устройства уже имеют доступ в Интернет и могут взаимодействовать между собой. Да, сейчас для этого нужно немного настроить, например, сервис IFTTT и т.п., но прогресс уже не остановить. Как следствие – компьютер начинает проникать в автомобиль, как базовый компонент развлекательного центра. Для автопрома есть вполне обоснованные ограничения, которые прямо влияют на скорость развития IVI – это соблюдение всех мер безопасности. В любом случае, нельзя пренебрегать безопасностью как своей, так и других участников дорожного движения. Автомобиль и дорога – не место для экспериментов. Поэтому проекты энтузиастов, которые хочется рассмотреть в этой заметке – лишь идеи, которые возможно перерастут в будущие прототипы, но уже для коммерческих предложений. Конечно, если речь заходит о безопасности, то доверять следует сертифицированным изделиям, за которыми стоят часы разработок и тестирования.
Поскольку речь пошла о Connected Car и IVI, то стает очевидным использование ресурсов облака с функциями IoT, для реализации на «верхнем уровне» соответствующей программно-аппаратной архитектуры. Например, большинство универсальных облачных платформ, таких как Amazon AWS, Microsoft Azure, IBM Watson или др., содержат практически универсальные сервисы для построения Интернета вещей. По сравнению с сними, будут интересны и открытые решения в этой сфере. Проект Kaa является одним из таких примеров.
Picture: Веб-интерфейс платформы Kaa
Разработчики позиционируют Kaa, как многоцелевую платформу промежуточного уровня для решений IoT, нацеленную на создание приложений для подключенных устройств и интеллектуальных систем. Платформа Kaa представляет собой открытый, многофункциональный инструментарий для разработки встраиваемых систем и решений для облака. Платформа выпущена под лицензией Apache Software License 2.0. Для быстрого ознакомления с платформой можно скачать образ для VirtualBox или развернуть систему в облаке AWS.
Идея Kaa заключается в том, что разработчикам предоставляется SDK для создания приложений с использованием API рассматриваемой платформы, и соответствующие сервисы в облаке, включая компиляцию низкоуровневых программ. В текущей версии поддерживаются языки: C, C , Java, Objective-C и операционные системы Linux, Windows, Android и iOS, а также заявлена поддержка множества встраиваемых аппаратных платформ, например, Raspberry Pi, Intel Edison.
Тут хочется отвлечься и заметить, что, примерно, как когда-то давно в начале 2000-х годов Intel ушла с рынка микроконтроллеров со своим интереснейшим решением – MCS-51, а сейчас, фактически, мы являемся свидетелями подобного – завершение разработки систем Galileo, Joule и Edison, это не осталось не замеченным у сообщества разработчиков и энтузиастов. Многие идеи из предыдущих систем станут началом для новых устройств и решений. Конечно, если для кристалла i8051 (MCS-51), компания Intel продолжила лицензировать его архитектуру, даже после своего ухода с рынка микроконтроллеров, то сейчас, пожалуй, значительно укрепятся позиции Raspberry Pi и Arduino (на классической платформе микроконтроллеров Atmel), как основных инструментов прототипирования встраиваемых устройств. Но не стоит забывать, что для IoT также интересны разработки на основе устройств ESP8266/ ESP32 Espressif и подобные системы с интегрированным интерфейсом Wi-Fi на кристалле.
Возвращаясь к проекту Kaa, следует отметить, что существует ряд архитектурных особенностей, которые делают разработку IoT на этой платформе исключительно быстрой и легкой. Решения на Kaa не должны зависеть от конкретной аппаратной платформы и, таким образом, проект является совместим практически с любыми подключенными устройствами, датчиками и шлюзами. Конечно, это достигается за счет использования универсальных протоколов связи и стандартизированных интерфейсов между компонентами системы. Также Kaa обеспечивает четкую структуру функций и расширений IoT для различных типов приложений. Они могут использоваться, почти по аналогии технологии plug-and-play, с минимальным кодом, который все-таки должен доработать разработчик для своих систем и устройств. Эти возможности, в сочетании с аналитикой в облаке, делают Kaa достаточно привлекательной платформой для Connected Car и др. решений.
Проект Kaa успешно развивается и совершенствуется, но есть и другая, можно сказать, обратная задача для построения решений Connected Car. Суть ее лежит в выполнении задач тестирования программной части облачной системы и непосредственной эмуляции поведения множества подключенных к облаку автомобилей. Для этой задачи интересны проекты с открытым кодом, например, AutoSIM компании Automatski, который разрабатывается с поддержкой двух лицензий: GPLv3 и коммерческой. В данной системе используются основные протоколы передачи данных, включая MQTT, AMQP, CoAP и др. Пользователь при помощи Rest/Json API управляет работой сервера для симуляции требуемых процессов. AutoSIM теоретически позволяет имитировать поток данных от более чем 100 миллионов устройств.
Kaa и AutoSIM – замечательные примеры открытых систем и, возможно, решений, которые станут очень востребованными для автопрома. Но уже сейчас, на нижнем уровне встраиваемых систем для автомобиля, есть множество интересных открытых решений, которые успешно взаимодействуют с облачными сервисами. Например, платформа Android Auto компании Google. Это решение базируется на Android Automotive и нацелено на объединение смартфона и автомобильного медиа-центра в единую платформу, расширяющую функционал IVI-систем.
Поскольку в современном автомобиле присутствуют множество блоков, которые непосредственно взаимодействуют с медиа-центром, например, с использованием сетей Controller Area Network (CAN), Local Interconnect Network (LIN), Media Oriented Systems Transport (MOST), а также более привычные Ethernet и протоколы TCP/IP, то Android Automotive предлагает некоторую абстракцию – hardware abstraction layer (HAL) между физическими сетями автомобиля и функционалом операционной системы Android, доступного разработчикам.
Figure: Vehicle HAL and Android automotive architecture
Но, если Android вполне прогнозируемое решение от Google, то использование Linux, в качестве платформы для автомобильного развлекательного центра, пожалуй, уже стала классикой жанра. И, как говорится, кто-нибудь определенно должен был предложить проект, например, Automotive Grade Linux. Это открытая разработка, объединяющая автопроизводителей, поставщиков и технологические компании под эгидой The Linux Foundation. Конечно, AGL доступен в виде исходного кода, а для первоначального знакомства можно скачать образ виртуальной машины, например, для запуска в VirtualBox или вполне можно запустить дистрибутив систему на Raspberry Pi. Для работы с AGL SDK потребуется Docker.
Picture: Automotive Grade Linux – Getting Started
Разработка приложений для AGL предусмотрена на HTML5, Qt OpenGL. Сама платформа автомобильного Linux сосредоточена на безопасности. Например, архитектура решения предполагает гипервизор и, конечно, изоляцию критически-важных приложений, систем мультимедиа и развлечений. Приложения и компоненты AGL строятся по модульному принципу с интерфейсами в виде сигналов/событий, что позволяет скрыть сложную программную реализацию низкоуровневых решений, например, реализацию интерфейса CAN-шины, и предоставить простой API для приложений.
Figure: Upgraded Security Blueprint 2021. AGL security overview — Automotive Grade Linux Documentation
Интересно, что разработчики Automotive Grade Linux присматриваются к реализациям уровня архитектуры из других проектов, впрочем, что характерно для всего движения открытых систем. Например, интересным решением в проекте OpenXC является принцип описания в формате JSON всех сигналов CAN, которые нужно обрабатывать. Программный генератор преобразовывает такое описание в файл на языке C , которая может быть скомпилирована и т.п.
Figure: OpenXC is an open source, data-focused API for your car
Если говорить о проекте OpenXC, то в нем используется комбинация аппаратного и программного обеспечения с открытым исходным кодом, которое позволяет расширить функционал автомобиля с помощью специальных приложений и подключаемых модулей. Проект сконцентрирован на получении данных об автомобиле и использует стандартный интерфейс диагностического разъема OBD-II. Фактически проект позволяет разработать свое мобильное приложение под Android, iOS или использовать для разработки Python, а в качестве основной автомобильной платформы выбрана марка Ford.
Возвращаясь к проекту Automotive Grade Linux стоит заметить, что в нем Raspberry Pi используется скорее для целей демонстрации и прототипирования, а вот для промышленной платформы выбраны специализированные решения, например, системы на базе ARM SoCs (система на чипе) компании Renesas Electronics серии R-Car Generation 3 и некоторые др.
Figure: R-Car H3. System Block Diagram – Renesas Electronics
Поскольку можно и дальше рассматривать различные платформы для построения IVI-систем, то хочется сделать небольшой вывод об уже приведенных примерах. Все говорит о том, что текущий, 2021 год, будет годом доставляемых пользователю и разработчику приложений. Ведь, как в проекте облака Kaa, так и решениях для операционной системы AGL, используется принцип предоставления практически готового приложения в виде SDK для разработчика. Также широко используется подход на основе описания критериев разрабатываемой системы, например, в формате JSON, и генерации на их основе каркаса приложения. Это чем-то напоминает идеи раскрытые ранее в публикации: «Быстрое прототипирование устройств Интернета вещей», где были рассмотрены решения, в которых разработчики микросхем предоставляют стартовые наборы и т.п. для быстрого ознакомления с их компонентами. Действительно, для программиста, разрабатывающего проект для IoT, всегда будет удобно начать разработку не с детального ознакомления с «неким» API, а с уже готового каркаса приложения.
Давайте обратим внимание на немного другой уровень IVI, где не используются такие многофункциональные системы, как Renesas или др. Интересно рассмотреть подходы на основе уже традиционного решения Raspberry Pi. Понятно, что все это скорее прототипы, т.к. Raspberry Pi вряд ли можно сравнить с промышленной платой, специально разработанной для автомобильных применений, но всегда интересны идеи и подходы энтузиастов. Так, одним из любопытных решений для медиа-центра стоит отметить проект CarPC, который фактически является разработкой и совершенствованием графического интерфейса и дополнительного функционала для известного кроссплатформенного медиапроигрывателя Kodi (ранее проект XBMC).
Picture: CarPC-touch for Kodi
Но Kodi или другой медиа-центр не позволят получить и отобразить данные из бортовой сети автомобиля. Для этого проще всего подключиться к диагностическому разъему автомобиля, как было рассмотрено в нашей предыдущей публикации: «Разъем диагностики OBD-II, как интерфейс для IoT». И тут интересен проект Carberry, представляющий из себя плату расширения для Raspberry Pi.
Picture: Carberry for RPi 3 – Paser
Плата Carberry итальянской компании Paser стоит 123.83 € (евро). Она подключается к стандартному порту расширения Raspberry Pi 3 и поддерживает: CANBUS, GMLAN, инфракрасный приемник и другой функционал. Плата расширения служит отличным дополнением к программным решениям, например, уже упомянутому CarPC. Следует отметить, что программно-аппаратный проект постоянно развивается в сторону расширения функционала и т.д.
Picture: What is AutoPi and What Does It Do? – AutoPi.io
Так же активно развивается немецкий стартап AutoPi, представляющий собой проект автомобильной IoT-платформы. Интересной в AutoPi является аппаратная часть на базе Raspberry Pi Zero и наличием интерфейса OBD-II, а также: 3G/4G, Bluetooth, WiFi, GPS-приемника и других компонентов, умещающихся в достаточно небольшом устройстве.
На фоне всех рассмотренных систем прямо напрашивается вопрос: как подключить к Raspberry Pi интерфейс OBD-II? Очевидным ответом будет использование готовой платы, например, PiCAN2 CAN-Bus Board for Raspberry Pi 2/3 по цене от £26.90 (в фунтах без европейского НДС) или что-то похожее. Но очень заманчиво использовать широко распространённый модуль CAN Bus на базе MCP2515/TJA1050, который доступен на AliExpress от USD $1.36 и чуть дороже. Например, такой модуль значительно дешевле плат расширения на базе MCP2515 для Arduino.
Стоит заметить, что низкая цена модуля MCP2515/TJA1050 обусловлена его скромным размером и количеством элементов на плате. Она отлично подходит для работы с платами Arduino, но в таком случае при программировании следует не забывать, что в основном на модуль MCP2515/TJA1050 разработчики устанавливают кристалл кварца 8 MHz, а не 16 MHz, который обычно присутствует на большинстве плат Arduino CAN-BUS Shield. Таким образом, само-собой напрашивается использование модуля MCP2515/TJA1050 для подключения к Raspberry Pi и работа с шиной CAN, например, с эмулятором OBD-II, рассмотренным все в той-же статье «Разъем диагностики OBD-II, как интерфейс для IoT» или напрямую с модулем ELM327 и т.п.
Picture: Модуль CAN Bus на базе MCP2515/TJA1050
Но работая с электронными модулями всегда нужно внимательно относиться к напряжению питания и ограничениям по линиям передачи данных. При использовании стандартного интерфейса расширения GPIO платы Raspberry Pi это достаточно актуально. Дело в том, что на разъеме Raspberry Pi присутствуют напряжения 5В и 3.3В, которыми можно запитать внешние схемы, однако, линии ввода-вывода платы работают только с уровнями до 3.3В. Соответственно, плату модуля MCP2515/TJA1050, рассчитанную только на 5В не следует сразу подключать к Raspberry Pi.
Schematic: CAN bus on raspberry pi with MCP2515 – Raspberry Pi Foundation Forum
Решением может стать простой двухсторонний преобразователь уровней 3.3В/5.5В. Таких готовых модулей для шины SPI предостаточно, но MCP2515 имеет дополнительный вывод прерывания, свидетельствующий о готовности данных. Такой вывод позволит процессору Raspberry Pi выполнять свою работу и реагировать на данные от MCP2515 только в требуемые моменты времени, не отвлекаясь постоянно на мониторинг SPI-интерфейса. Следовательно, готовый модуль сопряжения не будет иметь дополнительного контакта и нужно будет просто сделать свою схему.
Picture: Небольшая доработка модуля CAN Bus на базе MCP2515/TJA1050 для работы с Raspberry Pi
Есть и совершенно простое решение, но конечно, не лучшее. Оно основано на том, что микросхема MCP2515 вполне работоспособна с уровнем напряжений от 2.7В до 5.5В, значит ее можно запитать 3.3В с Raspberry Pi, а линии интерфейса SPI и прерывания INT – подключить к GPIO. Для TJA1050 потребуется питание 5В, которое можно подключить все с того же разъема GPIO, но на модуле MCP2515/TJA1050 потребуется аккуратно перерезать дорожку общего питания и подпаяться к ней.
В качестве правильного усовершенствования такой схемы очень разумно использовать делитель напряжения для согласования уровней 5В микросхемы трансивера TJA1050, кстати, можно использовать аналог MCP2551 от компании Microchip или др., и CAN-контроллера MCP2515 с питанием 3.3В (см. рис.). Также интересен проект CAN Simulator на основе Raspberry Pi и, конечно, MCP2515. В качестве трансивера в симуляторе используется микросхема TJA1049, которая хотя и требует питание 5В, но работает без дополнительных компонентов согласования уровней с 3-х вольтовыми системами. Если выбирать самое интересное решение, то, пожалуй, проект CAN-Bus Interface with galvanic isolation самый интересный, так как предлагает использовать трансивер MCP2562, позволяющий без дополнительных элементов подключить контроллер с напряжениями в диапазоне от 1.8В до 5В.
Schematic: CAN Raspberry Pi
Определившись с аппаратной частью можно переходить к работе с Raspberry Pi на программном уровне. В качестве операционной системы выберем достаточно традиционное решение Raspbian Stretch. А для работы с CAN поддержку драйвера MCP2515 уровня ядра Linux.
Picture: Передача данных между Raspberry Pi и Arduino по шине CAN
Для корректной работы Raspberry Pi с платой интерфейса CAN на базе MCP2515 нужно точно знать версию ядра установленной операционной системы. Для этого следует выполнить команду: $ uname -a
В нашем случае – это Linux raspberrypi 4.9.59 . Более детальную информацию о дистрибутиве и самой плате можно узнать, используя команды:$ cat /etc/debian_version
– версия базового дистрибутива Debian; $ cat /etc/os-release
– особенности релиза;$ cat /proc/cpuinfo
– версия аппаратной платформы (подробности можно узнать из статьи: «How to Check the Software and Hardware Version of a Raspberry Pi»).
Для работы SPI с микросхемой MCP2515 нужно добавить параметры в конфигурационный файл /boot/config.txt операционной системы:# Uncomment some or all of these to enable the optional hardware interfaces
dtparam=spi=on
dtoverlay=mcp2515-can0, oscillator=8000000, interrupt=25
Понятно, что если кварц на плате 16 МГц, то нужно указать число «16000000».# dtoverlay=spi-bcm2835-overlay
Драйвер bcm2835 потребуется явно указывать в конфиге только для старых ядер Linux до kernel 4.4.x
Следующий параметр следует раскомментировать для современных систем:# dtoverlay=spi1-1cs
А, вот, после долгих поисков нашлась актуальная строчка, которая позволила заработать нужному модулю на ядре Linux raspberrypi 4.9.59 в текущей системе: dtoverlay=spi0-hw-cs
После перезагрузки можно просто «поднять» сетевой интерфейс:$ sudo ip link set can0 up type can bitrate 500000
Действительно, получается очень удобно, что CAN стает в Raspberry Pi своеобразным сетевым интерфейсом и, например, выполнить посылку или получить дамп принимаемых данных, а также узнать статистику работы:$ cansend can0 127#DEADBEEF
$ candump can0
$ ip -details -statistics link show can0
Чтобы каждый раз при старте системы не «поднимать» интерфейс, можно добавить пару стандартных строк в файл конфигурации /etc/network/interfaces и т.п.
Screenshot: Передача данных между Raspberry Pi и эмулятором OBD-II на базе платы Arduino
Дальше остается писать новый код или использовать готовые библиотеки. Поскольку драйвер MCP2515 фактически дает системе новый сетевой интерфейс, то разработку можно выполнять практически на любом языке. И тут сразу хочется сказать немного о Go, кроссплатформенном, компилируемом, многопоточном языке программирования, разработанным компанией Google. Этот язык однозначно очень интересен в плане разработки встраиваемых систем и, конечно, будет совсем не сложно найти под него уже готовую библиотеку, подходящую для проекта, например, CAN bus in Go. А если зайти дальше, то следует рассмотреть и проект Gobot – фреймворк, направленный на создание роботов, решений для IoT и т.д. Но, пожалуй, для этой публикации это будет уже слишком много.
К вопросам программирования на Go обязательно следует вернуться, но уже в следующих публикациях. Так же нужно будет не забыть об операционной системе Tizen, на которой ранее базировалась Automotive Grade Linux, а также рассмотреть тренд этого года – голосовые ассистенты в салоне автомобиля и многое др. А в завершении авто тематики текущей публикации просто вспомним о платформе цифровой кабины, представленной Samsung и HARMAN на CES 2021 и немного вдохновимся идеями дизайнеров и инженеров, разработавших этот концепт.
Интересные ресурсы и ссылки:
— Mercedes-Benz’s new MBUX in-car assistant and smart UI rocks – TechCrunch
— Интерфейс от Mercedes-Benz объявлен другом водителя – Драйв
— Getting started. Kaa Sandbox – KaaIoT Technologies
— Intel прекращает выпуск Galileo, Joule и Edison — Компьютерное Обозрение
— OK Google, what is Android Auto? Here’s everything you need to know about it – Digital Trends
— Система Android Auto стала доступна в любом автомобиле – 3DNews
— Android на колесах: систему Google «вживят» в автомобили Audi и Volvo – Вести
— Toyota Camry станет первым автомобилем, оснащённым платформой Automotive Grade Linux – OpenNET
— Raspberry PI 2 CarPC – Engineering(DIY)
— Arduino CAN Bus Module Pin Outs and Schematics – Henry’s Bench
— MCP2515: контроллер шины CAN с интерфейсом SPI – microsin.net — заметки радиолюбителя
— Согласование логических уровней 5В и 3.3В устройств – Сообщество EasyElectronics.ru
— Хакаем CAN шину авто для голосового управления – Geektimes
— Pin Numbering — Raspberry Pi Model B (Revision 2.0) – The Pi4J Project
— CAN-Bus with Raspberry Pi: HowTo/Quickstart MCP2515 Kernel 4.4.x – vimtut0r’s blog
— CAN On the Raspberry Pi – Harrison’s Sandbox
— MyPi CAN-BUS Card Configuration – Embedded Micro Technology
— CAN BUS Shield for Raspberry CanBerryDual V2.1 – Industrialberry
— Libre Solar CAN interface for Raspberry Pi (Zero W) – GitHub
— OBD-Pi – Instructables
— SocketCAN userspace utilities and tools – GitHub
— CAN Bus – Embedded Linux Wiki
— Go on Raspberry Pi — @NET
— Need to Create Synergy Korean Connected Car Developers Alleged to Need More Collaboration – Korea’s Premier Business Portal
— CES 2021: кто главный экспонат? – Geektimes
Софт панели приборов на 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, чтобы любой мог попробовать цифровую панель приборов.
Проект цифровой панель приборов открытый. Рад буду предложениям и комментариям!