Hackear autobus CAN. Tablero virtual



En el primer artículo, "Hackear un bus CAN para un auto para control de voz", me conecté directamente al bus CAN Comfort en la puerta de mi auto y examiné el tráfico que pasaba, esto me permitió determinar los comandos para controlar ventanas, cierre centralizado, etc.

En este artículo, le diré cómo armar mi propio tablero virtual o digital único y obtener datos de cualquier sensor en los automóviles VAG (Volkswagen, Audi, Seat, Skoda).

Ensamblé un nuevo sniffer CAN y un escudo CAN para la Raspberry Pi basado en el módulo MCP2515 TJA1050 Niren, los datos que obtuve con ellos los utilicé en el desarrollo de un panel de instrumentos digital usando una pantalla de 7 ″ para la Raspberry Pi. Además de simplemente mostrar información, el panel digital responde a los botones de control de la columna de dirección y otros eventos en el automóvil.

Como marco para dibujar instrumentos, Kivy para Python fue genial. Funciona sin X y usa GL para mostrar gráficos.

  1. Puede rastrear de Arduino Uno
  2. Escuchar solicitudes utilizando el Sistema de diagnóstico VAG-COM (VCDS)
  3. Desarrollo del tablero basado en la Raspberry Pi y pantalla de 7 ″
  4. Software de tablero en Python y Kivy (marco de la interfaz de usuario)
  5. Video del panel de instrumentos digital basado en Raspberry Pi

Debajo del corte, ¡la implementación completa del proyecto será interesante!


La puerta del conductor está abierta.

Puede rastrear de Arduino Uno


Para escuchar lo que envía VCDS al bus CAN, ensamblé un sniffer en una placa de prueba de Arduino y el módulo MCP2515 TJA1050 Niren.



El diagrama de conexión es el siguiente:


Para escuchar el tráfico, utilicé el analizador CanHackerV2 y el firmware arduino-canhacker para Arduino, que implementa una API compatible con este programa. Firmware en el gith https://github.com/autowp/arduino-canhacker .

CanHackerV2 le permite ver el tráfico que pasa, grabar y reproducir comandos en un intervalo específico, lo que ayuda mucho en el análisis de datos.



Escuchar solicitudes utilizando el Sistema de diagnóstico VAG-COM (VCDS)


Descripción de VCDS del sitio web oficial ru.ross-tech.com :

El escáner de hardware y software VCDS está diseñado para diagnosticar sistemas de control electrónico instalados en vehículos VAG. Acceso a todos los sistemas: motor, ACP, ABS, control de clima, electrónica del cuerpo, etc., lectura y borrado de códigos de falla, visualización de parámetros actuales, activación, configuraciones básicas, adaptación, codificación, etc.



Al conectar el sniffer a las líneas CAN_L y CAN_H en el cable de diagnóstico, pude ver qué solicitudes hace el VCDS y qué responde el automóvil.



La peculiaridad del grupo automático VAG es que el conector OBD2 está conectado al bus CAN a través de la puerta de enlace y la puerta de enlace no pasa todo el tráfico que atraviesa la red, es decir. Si se conecta al conector OBD2 con un sniffer, no verá nada. Para recibir datos en el conector OBD2, debe enviar solicitudes especiales a la puerta de enlace. Estas solicitudes y respuestas son visibles cuando se escucha el tráfico de VCDS. Por ejemplo, así es como puede obtener kilometraje.


En VCDS, puede obtener información de casi cualquier sensor en el automóvil. En primer lugar, estaba interesado en información que generalmente no está en mi orden, esto:

  • temperatura del aceite
  • cual puerta esta abierta

También recibí velocidad, revoluciones, temperatura del refrigerante, kilometraje, flujo, espacio en el tanque y otras solicitudes, lo publicaré como referencia.

// 
714 03 22 22 0D 55 55 55 55
77E 05 62 22 0D 55 65 AA AA -  
77E 05 62 22 0D 00 65 AA AA -  
77E 05 62 22 0D 54 65 AA AA -  
77E 05 62 22 0D 51 65 AA AA -  
77E 05 62 22 0D 50 65 AA AA -    
77E 05 62 22 0D 45 65 AA AA -   
77E 05 62 22 0D 15 65 AA AA -   
77E 05 62 22 0D 44 65 AA AA -     
77E 05 62 22 0D 40 65 AA AA - , ,   
01010101 = 0x55 ( )
0  - 
2  - 
4  -  
6  -  

// 
714 03 22 22 05 55 55 55 55
77E 05 62 22 05 21 AA AA AA - 
77E 05 62 22 05 20 AA AA AA -  

//  
714 03 22 22 0 55 55 55 55
77E 04 62 22 0C 55 AA AA AA - -7.5°
77E 04 62 22 0C 65 AA AA AA - 0.5 101°
77E 04 62 22 0C 66 AA AA AA - 1 = 102°
77E 04 62 22 0C 68 AA AA AA - 2 = 104°

//   
714 03 22 10 14 55 55 55 55
77E 04 62 10 14 84 AA AA AA - 16°

//  
714 03 22 22 94 55 55 55 55
77E 05 62 22 94 00 8E AA AA - 142

//  
714 03 22 22 06 55 55 55 55
77E 04 62 22 06 2C AA AA AA - 44
77E 04 62 22 06 16 AA AA AA - 22

//     
714 03 22 22 96 55 55 55 55
77E 05 62 22 96 01 9A AA AA - 41.0°

//  
714 03 22 F4 05 55 55 55 55
77E 04 62 F4 05 85 AA AA AA - 52.5°
77E 04 62 F4 05 87 AA AA AA - 54°
77E 04 62 F4 05 5 AA AA AA - 100.5°

//  
714 03 22 F4 0C 55 55 55 55
77E 05 62 F4 0C 0B C6 AA AA - 753.5 /; 0BC6 = 3014/4 = 753
77E 05 62 F4 0C 0B DC AA AA - 759 /; 0BDC = 3036
77E 05 62 F4 0C 0B E8 AA AA - 762 /; 0BE8 = 3048
77E 05 62 F4 0C 1C 32 AA AA - 1804.5 /

//  
714 03 22 20 2F 55 55 55 55
77E 04 62 20 2F 36 AA AA AA - -4°
77E 04 62 20 2F 67 AA AA AA - 45°
77E 04 62 20 2F 68 AA AA AA - 46°

//    
746 03 22 26 13 55 55 55 55
7B0 05 62 26 13 00 5B AA AA - 9.1, 91 == 0x5B
7B0 05 62 26 13 00 5C AA AA - 9.2°
7B0 05 62 26 13 00 5D AA AA - 9.3°

// 
714 03 22 22 16 55 55 55 55
77E 05 62 22 16 11 1E AA AA - 17:30

//   
714 03 22 22 1B 55 55 55 55
77E 05 62 22 1B 80 AA AA AA -  
77E 05 62 22 1B 81 AA AA AA -  
77E 05 62 22 1B 84 AA AA AA -  

//   2
714 03 22 22 99 55 55 55 55
77E 03 62 22 99 00 91 AA AA - 14.5/100

//  
714 03 22 22 98 55 55 55 55
77E 05 62 22 98 00 00 AA AA - 0.0/100

// 
714 03 22 22 03 55 55 55 55
77E 05 62 22 03 24 0 AA AA - 94080  0x240 * 10

Raspberry Pi 7″


Raspberry Pi. Android , , Raspberry Pi . 7″ , CAN TJA1050 Niren.



OBD2 ELM327 .



: CAN_L, CAN_H, +12, GND.



. , Raspberry Pi , , .



, . .



, . , 3D .



Python Kivy (UI framework)


. .




. , , - 80-.




- .




, Linux . , : , , nodejs, . Qt PySide2 , , .. . Kivy — Python, .

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')

python
# -*- 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 :

  1. (, , , )
  2. 5
  3. CAN ,

, . iOS, .

. !

https://github.com/aivs/blackCockpit

Raspberry Pi




24.06.2019




— , ELM327 Wi-Fi . OBD2 , CAN Wi-Fi.


VAG Virtual Cockpit AppStore. , iPhone/iPad, Android . .
, , !
VAG Virtual Cockpit


Source: https://habr.com/ru/post/442184/


All Articles