【OBD-Ⅱ】ELM327とRaspberryPiを使って自作で車のメーターを作ってみた。

今回はELM327を用いて、以下のようなメーターを作成した。
#1.必要なもの
ハードウェア
ELM327(V1.5)
OBDⅡを介してCAN通信を行うスキャナである。BlueToothでRaspberryPiやスマホなどに、CAN通信で得られたデータを飛ばすことができる。
V2.1ではELM327自体が正常に動いた事例が少なかったので、v1.5を選択した。

https://www.amazon.co.jp/OBD2%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%83%84%E3%83%BC%E3%83%AB-BLUETOOTH-%E8%87%AA%E5%8B%95%E8%BB%8A%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%8A%E3%83%BC-bluetooth%E4%BB%95%E6%A7%98-%EF%BC%8AiPhone%E3%81%AF%E5%AF%BE%E5%BF%9C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%9B%E3%82%93/dp/B07RNHZS44/ref=sr_1_5?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=ELM327+v1.5&qid=1620393359&sr=8-5

OBDⅡの対応車種(今回はインプレッサスポーツを使用。以下サイトにOBDⅡ対応車種が掲載されているサイトを記載)

https://www.yupiteru.co.jp/harness/obd2.pdf


RasperryPi3 ModelB+以上のRaspberryPi


5インチ以上のTFT液晶

https://www.amazon.co.jp/ELECROW-%E3%83%87%E3%82%A3%E3%82%B9%E3%83%97%E3%83%AC%E3%82%A4-800x480%E8%A7%A3%E5%83%8F%E5%BA%A6-%E3%82%BF%E3%83%83%E3%83%81%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3-Raspberry/dp/B013JECYF2/ref=sr_1_10?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=5%E3%82%A4%E3%83%B3%E3%83%81+raspberry+pi&qid=1620472722&sr=8-10


ソフトウェア
GIMP2

https://www.gimp.org/downloads/

#2.実装方法
Bluetoothにより、ELM327からCANデータを飛ばす。その後、pythonのobdモジュールでデータを整形。それをさらに整形し、メーターに表示させる。

#3.実装時注意点
車種次第では一部のデータが取得できない場合があるので、その場合は潔く諦める。

#4.実装方法
4.0 ELM327の動作確認
ELM327が使えないと困るので、Androidでインストールできる「Torque lite」をインストールし、回転数などのデータが取得できるか確認する。
ここで取得できない場合には、ELM327を別のものに取り替える。


4-1.pythonのバージョンアップ
RaspberryPi 3では、デフォルトでインストールされているpythonが2.7と3.7の2種類があり、デフォルトでは、2.7になっている。
今回は、3.7を使うので、以下記事を参考に、pythonのバージョン変更をする。

https://qiita.com/Tommy0221/items/fa516c87fa23b3d8068f

4.2 obdモジュールのインストールとBluetooth接続環境のセット
このモジュールのインストールは公式サイトでは、pip install obdでインストールすると案内されているが、この方法でインストールすると内部で使用されているloggingモジュールのエラーが発生し、使えない。
そのため、今回は、以下サイトのgithubにアクセスし、そのurlを指定してpipでインストールする。

https://github.com/brendan-w/python-OBD


インストールする際は、以下のコードを打つ


```
pip install git+https://github.com/brendan-w/python-OBD
```

次に、以下コードを打ち、Bluetooth接続時に必要なライブラリをインストールする。
(コードはnomunomu0504様記事より引用)

```python:Bluetooth
# bluezを動かすために必要なライブラリ群
$ sudo apt-get install -y libglib2.0-dev libdbus-1-dev libudev-dev libical-dev bluetooth bluez-utils blueman

# bluez本体
$ sudo mkdir car_tmp && cd car_tmp
$ wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.45.tar.xz
$ xz -dv bluez-5.45.tar.xz && tar -xf bluez-5.45.tar 
$ cd bluez-5.45/ && ./configure --enable-experimental
$ make
$ sudo make install
```

make時にエラーが出たが、エラーの詳細を失念してしまったので参考にしたサイトを記載する。

http://www.neko.ne.jp/~freewing/raspberry_pi/raspberry_pi_3_bluetooth_ble_bluez/


4.3 テストコードの実装
obdモジュール・RaspberryPiにおけるBluetoothの接続方法については、nomunomu0504様の記事がわかりやすかったため、こちらを参考にし、説明は割愛する。



以下コード

```Python:test_obd.py
import obd
import os

#Bluetoothの接続準備
os.system('sudo hcitool scan')
os.system('sudo hciconfig hci0 up')
os.system('sudo rfcomm bind 0 ELM327のMACアドレス')
os.system('sudo rfcomm listen 0 1 &')

#OBDⅡコネクションを定義
o = obd.OBD()

#回転数を取得
while True:
    rpm=o.query(obd.commands.RPM)
    print(rpm) 
```

以下コードが実装できたら、実車とELM327を接続、RaspberryPiを起動する。
起動し、先ほど作ったテストコードを実行する。
すると、余計な文字も含まれているが、ターミナル上に回転数が表示され続ける。
これでobdモジュールが使えることが確認された。


4.4メーター画像の準備
GIMPを用い、メーター画像を制作する。
今回は、ホンダのS2000のメーターパネルを参考に制作したが、画像編集技術がないため、回転数のメモリが曲げられなかった。
細かなgimpの使い方は、以下サイトに詳しく記載されているため、説明を割愛する。

https://synclogue-navi.com/category/gimp/

メモリの表現方法として、tkinterで定義したbackgroundの画像を、あらかじ用意した1メモリごとにずらした画像を当てて、一定間隔で画像を更新しメモリがずれているかのように見せる手法を用いた。

![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/702737/5c64babd-60db-1cce-8e26-0b0ec05c7b33.png)



![meter_image.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/702737/28cac124-8d57-a4f2-3d04-102d3a1d773e.png)
完成したメーターの土台画像(メーター内の黄色の文字はtkinterにより当てている)



4.5実装
以下コードのように実装した。


```Python:test_obd.py
# -*- coding: utf-8 -*-
import tkinter as tk
import obd
import os
from time import *
import random
import math
root = tk.Tk()
i=1
o  = 0
cnt=0
x=0
acc=True
speed_v=0
#fuel_amount=0.0
class Display():
    def __init__(self):
        global o
        os.system('sudo hcitool scan')
        os.system('sudo hciconfig hci0 up')
        os.system('sudo rfcomm bind 0 00:1D:A5:08:AC:BE')
        os.system('sudo rfcomm listen 0 1 &')
        o = obd.OBD()

    def window(self):
        global i
        i+=1
        root.geometry("1920x1080")
        canvas = tk.Canvas(bg = "black", width=1920, height=1080)
        canvas.place(x=0, y=0)
        img = tk.PhotoImage(file =r'/home/pi/Python/elm327_obd2/img/bg/bg3.png')
        canvas.create_image(960,540,image=img)
        rpm_file=str(r'/home/pi/Python/elm327_obd2/img/Scales/rpm_')+str(0)+str('.png')
        rpm_img = tk.PhotoImage(file = rpm_file)
        rpm_canvas = tk.Canvas(bg = "black", width=1755, height=224)
        rpm_canvas.place(x=80, y=58)
        rpm_canvas.create_image(878,113,image=rpm_img)
        
        
        self.clock_display()
        self.speed_display()
        self.rpm_display()
        self.intake_display()
        self.voltage_display()
        self.info_display()
        self.trip_display()
        self.fuel_display()
        self.water_t_display()
        root.mainloop()

    def rpm_display(self):
        def set_rpm():
            global rpm_file,rpm_img,rpm_canvas,o,x,cnt,acc
            rpm_str =  ('{:.5}'.format(str(o.query(obd.commands.RPM))))
            #print(rpm_str)
            rpm_str2 = ('{:.3}'.format(str(o.query(obd.commands.RPM))))
            #Idling_stop
            if '0.0' in rpm_str:
                rpm_v=0
                rpm_float=0 
            else:
                rpm_float = float(rpm_str) 
            #データ整形
            if (math.floor(rpm_float)) < 999.999999:
                rpm_v = ('{:.1}'.format(str(rpm_float)))
            else:
                rpm_v = ('{:.2}'.format(str(rpm_float)))
            if rpm_v==0 and acc==True: 
                if x==0 and acc==True: #ACC-On
                    if x<=80 and cnt==0:
                        x+=1 #up memory
                    if x==80:
                        cnt=1 #reverse
                        x-=1
                    if x<80 and cnt==1:
                        x-=1 #down memory
                    if x==0:
                       cnt =2
                       x=0
                    if x==0 and cnt==2:
                       x=0
                       acc=False
                    rpm_file=str(r'/home/pi/Python/elm327_obd2/img/Scales/rpm_')+str(x)+str('.png')
            else: #Ignition-On
                rpm_file=str(r'/home/pi/Python/elm327_obd2/img/Scales/rpm_')+str(rpm_v)+str('.png') #デフォルトrpm
            rpm_img = tk.PhotoImage(file = rpm_file)
            rpm_canvas = tk.Canvas(bg="black",width=1755, height=224)
            rpm_canvas.place(x=80, y=58)
            rpm_canvas.create_image(878,113,image=rpm_img)
            if rpm_v== 0 and acc==True:
                root.after(400, set_rpm)
            else:
                root.after(400, set_rpm)
        set_rpm()

            

    def speed_display(self):
        speed_status = tk.StringVar()
        speed_status.set("888")
        speed_label=tk.Label(root,textvariable=speed_status,font=("DSEG7 Classic",97),background = "#351d18",fg= "orange",anchor="e",width =3)
        speed_label.place(x=778,y=483)
        def set_speed():
            global o,speed_v
            speed_str =  ('{:.3}'.format(str(o.query(obd.commands.SPEED))))
            speed_float = float(speed_str)
            if (math.floor(speed_float)) < 10:
                speed_v = ('{:.1}'.format(str(o.query(obd.commands.SPEED))))
            if (math.floor(speed_float)) >=10:
                speed_v = ('{:.2}'.format(str(o.query(obd.commands.SPEED))))
                if (math.floor(speed_float))>=100:
                    speed_v = ('{:.3}'.format(str(o.query(obd.commands.SPEED))))
            speed_status.set(speed_v)
            root.after(400, set_speed)
        set_speed()

    

    def clock_display(self):
        buff = tk.StringVar()
        buff.set('')
        clock = tk.Label(root,textvariable=buff,font=("DSEG7 Classic",27),background = "#351d18",fg= "orange")
        clock.place(x=878,y=730)
        def show_time():
            buff.set(strftime('%H:%M'))
            root.after(1000, show_time)
        show_time()


    def intake_display(self):
        intake_status = tk.StringVar()
        intake_status.set("0.88")
        intake_label=tk.Label(root,textvariable=intake_status,font=("DSEG7 Classic",67),background = "#351d18",fg= "orange",anchor="e",width =3)
        intake_label.place(x=273,y=438)
        def set_intake():
            global o
            intake_str =  ('{:.3}'.format(str(o.query(obd.commands.INTAKE_PRESSURE))))
            intake_float = float(intake_str)
            intake_status.set(intake_float)
            root.after(400, set_intake)
        set_intake()


    def voltage_display(self):
        voltage_status = tk.StringVar()
        voltage_status.set("888")
        voltage_label=tk.Label(root,textvariable=voltage_status,font=("DSEG7 Classic",65),background = "#351d18",fg= "orange",anchor="e",width =3)
        voltage_label.place(x=283,y=643)
        def set_voltage():
            global o
            voltage_str =  ('{:.3}'.format(str(o.query(obd.commands.CONTROL_MODULE_VOLTAGE))))
            voltage_float = float(voltage_str)
            voltage_status.set(voltage_float)
            root.after(300, set_voltage)
        set_voltage()


    def info_display(self):
        info_status = tk.StringVar()
        info_status.set("888")
        info_label=tk.Label(root,textvariable=info_status,font=("misaki_mincho",42),background = "#351d18",fg= "orange",anchor="e",width =9)
        info_label.place(x=260,y=853)
        def set_info():
            info_status.set("S2000-X!")
            root.after(400, set_info)
        set_info()


    
    def water_t_display(self):
        water_t_status = tk.StringVar()
        water_t_status.set("888")
        water_t_label=tk.Label(root,textvariable=water_t_status,font=("DSEG7 Classic",67),background = "#351d18",fg= "orange",anchor="e",width =3)
        water_t_label.place(x=1406,y=438)
        def set_water_t():
            global o
            water_t_str =  ('{:.3}'.format(str(o.query(obd.commands.COOLANT_TEMP))))
            water_t_float = float(water_t_str)
            water_t_status.set(water_t_float)
            root.after(300, set_water_t)
        set_water_t()


    
    def fuel_display(self):
        fuel_status = tk.StringVar()
        fuel_status.set("888")
        fuel_label=tk.Label(root,textvariable=fuel_status,font=("DSEG7 Classic",67),background = "#351d18",fg= "orange",anchor="e",width =3)
        fuel_label.place(x=1406,y=643)
        def set_fuel():
            global o  #Height_of fuel
            fuel_str =  ('{:.3}'.format(str(o.query(obd.commands.FUEL_LEVEL))))
            fuel_float = float(fuel_str)
            #max=100
            fuel_amount= ((fuel_float)/100.0)*55.0
            fuel_status.set(fuel_amount)
            root.after(400, set_fuel)
        set_fuel()


    
    def trip_display(self):
        trip_status = tk.StringVar()
        trip_status.set("888")
        trip_label=tk.Label(root,textvariable=trip_status,font=("DSEG7 Classic",37),background = "#351d18",fg= "orange",anchor="e",width =10)
        trip_label.place(x=696,y=798)
        def set_trip():
            global o
            trip_str =  (str(o.query(obd.commands.DISTANCE_W_MIL)))
            if '0.0' in trip_str:
                trip_v=0
                trip_float=0 
            if (math.floor(trip_float)) < 10:
                trip_v = ('{:.2}'.format(str(trip_str)))
            if (math.floor(trip_float)) < 100:
                trip_v = ('{:.3}'.format(str(trip_str)))

            trip_status.set("TRIP:     "+str(trip_v))
            root.after(400, set_trip)
        set_trip()


        
    
d = Display()
d.window()

```
データの整形は、取得されたデータをstring型に変換し、必要な文字を切り詰め、それをfloat型に変換した。

実際のメーターの動作風景を以下に載せる。

![メーター.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/702737/b130da8e-542b-2675-a599-330ac8c6778a.jpeg)


#5.制作してみて
OBDモジュールのインストールでてこずった。自分の画像編集技術のなさに失望した。
質問に的確に答えてくださったLINEのPythonのオープンチャットの皆様、大変感謝しております。
次回改良するときがあったら、車検に通るくらいの精度が高く起動が速いメーターを作りたい。

#6.参考サイト

https://qiita.com/nomunomu0504/items/8876f34fb33d8bf24fcb

メーター制作の際に、使用モジュールなどを参考にさせていただいた記事です。丁寧なコメントありがとうございました。


https://note.nkmk.me/python-math-floor-ceil-int/

小数点以下を切り上げる際に参考にさせていただいたサイトです。いつもお世話になっております。