RaspberryPi + OBD2 で取得した車両情報を解析してみる

実体はこちらです

オフィスや自宅を快適にするIoT byゆめみ③ Advent Calendar 2018

オフィスや自宅を快適にするIoT byゆめみ③ Advent Calendar 2018

さて、このAdvent Calendarの趣旨に合っているのか?と思っている皆さん。「タイトル」的にはアウトですが、「テーマ」的にはセーフという曲折を含めつつ、書いていきます:)

Advent Calendar の趣旨は?

このアドカレのタイトルは「オフィスや自宅を快適にするIoT」ですが、テーマは「続々と登場するIoTガジェットやサービス。果たして実際に生活は豊かになるのか?皆さんが試してみた、生活や身の回りを改善するIoTプログラミングを、教えてください。」 とあったので、「自分の好きな車 = 身の回り」ってことにしました(すいません🙇‍♂️)

TL;DR

ODB2 + Raspi で車の走行性能を数値化できる(ようになるための準備ができる) 車種にもよりますが、自分の車では「エンジン回転数」「スピード」を取得してグラフ化しました。下のグラフは回転数のグラフです。

キャプチャ.PNG

ODB2/ODBⅡとは

OBD = On-Board Diagnostics です。ざっくり言えば、元は「自動車の自己診断システム」で、最近では「車両情報」の意味も含まれていると思います。車速、回転数、水温、湯音、CANデータなどなどです。

歴史

1991年、US カリフォルニア州にて、州内で販売される新車に搭載が義務付けられた。 1996年、US 全土でも新車に搭載が義務付け。 2001年、EU でも排出ガス規制の一環で新車に搭載義務付け。 2006年、日本でも新車に搭載義務付け。

2008年時点で、アメリカで販売される全ての自動車にCANを信号のプロトコルとして埋め込む事が義務づけられている

本来の目的のデータ以外にも、車速、エンジン回転数などが取得できる。 規定されているデータの種類(PID)は100種類 + 各自動車メーカ独自拡張。なので、一概にすべての車に適用はできないかもしれません。

ODB2 ≠ CAN

CAN (Controller Area Network) = プロトコル OBD2 = CAN 上でやり取りされるデータ

CANの一例

プロトコル 備考
SAE J1850 PWM フォードが使用
SAE J1850 VPW ドイツの自動車メーカーが使用
ISO 9141-2 クライスラーや、ヨーロッパ、アジアの車で使われる
ISO 14230 KWP2000 Keyword Protocol 2000
ISO 15765 CAN ボッシュによって開発された。他のOBDプロトコルと違って、変種が自動車業界の外でも使用されている。

システム構成

今回OBD2からデータ取得するために、以下のようなシステム構成になりました。

スペック等 備考
検証車 TOYOTA カローラフィールダー エアロツアラー
OBD2コネクタ ELM327 Bluetooth対応版 iPhone未対応
RaspberryPi3 Model B+ Python3, obd2ライブラリ使用
macbook pro

OBD2コネクタ

image.png

結構いろんな種類あるんですが自分はこれを使ってます。はずれが多いらしいんですが、これは問題なく使えてるので大丈夫かと

超小型モデル OBDII 診断 ELM327 Bluetooth ブルートゥース スキャンツール テスター OBD2

新品価格
¥690から
(2019/4/2 20:57時点)

システム説明

主にBluetoothを制御するstart.shと、OBD2からデータを取得しExcelに落とし込むlogging.pyの2つのファイルで構成されています。 実行するときにはstart.shsudoで実行することで、Bluetooth設定が終わったら自動的にlogging.pyが起動します。

start.sh

sudo hciconfig hci0 up
sudo rmmod rfcomm
sudo modprobe rfcomm
sudo rfcomm bind 0 AA:BB:CC:11:22:33
ls /dev | grep rfcomm
sudo rfcomm listen 0 1 &
sudo python logging.py

この各行の意味を追っていきたいと思います。

hciconfig

hciconfigとはBluetooth(BT)をコマンドラインユーティリティから使うためのコマンドです。Bluezデーモンを使うのでインストールされてない場合は以下の通りインストールします

# 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

無事インストールできれば、BTが認識できているか確認してみます。以下のようにBT機器のMACアドレスが表示されてればOKです。

pi@raspberrypi:~ $ sudo hcitool lescan
LE Scan ...
74:DE:1A:E6:4E:4F (unknown)
34:36:3B:C7:FB:E9 (unknown)
34:36:3B:C7:FB:E9 (unknown)
74:DE:1A:E6:4E:4F (unknown)

pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ sudo hcitool scan                                                                                                                                                                                                          
Scanning ...
        AA:BB:CC:DD:EE:FF       xxxxxxxxxxxxxxxxxxx
pi@raspberrypi:~ $ 

確認ができたらhcitool -aでBTモジュールを確認します

hcitool -a

hci0:   Type: USB
        BD Address: 00:1B:DC:XX:XX:XX ACL MTU: 310:10 SCO MTU: 64:8
        UP RUNNING PSCAN
        RX bytes:384 acl:2323 sco:0 events:255 errors:0
        TX bytes:512 acl:4949 sco:0 commands:512 errors:0
        Features: 0xff 0xff 0x8f 0xfe 0x9b 0xff 0x59 0x83
        Packet type: DM1 DM3 DM5 DH1 DH3 DH5 HV1 HV2 HV3
        Link policy: RSWITCH HOLD SNIFF PARK
        Link mode: SLAVE ACCEPT
        Name: 'BlueZ at localhost.localdomain-0'
        Class: 0x120104
        Service Classes: Networking, Object Transfer
        Device Class: Computer, Desktop workstation
        HCI Ver:  (0x4) HCI Rev: 0x12e7 LMP Ver:  (0x4) LMP Subver: 0x12e7
        Manufacturer: Cambridge Silicon Radio (10)

RaspiにあるBTモジュールが認識されていれば、このような感じの情報が出力されます。このhci0というのがデバイスごとに割り振られるデバイスIDです。このデバイスIDを使って通信するBTモジュールを指定します。なので、メモしておきましょう。

sudo hciconfig hci0 up

このコマンドでデバイスIDがhci0のデバイスの電源をUPにします。このコマンドを実行してsudo hciconfigを確認したときにUP RUNNINGという表記がされていればOKです。

$ sudo hciconfig
hci0:   Type: BR/EDR  Bus: UART
        BD Address: B8:27:EB:2E:E0:10  ACL MTU: 1021:8  SCO MTU: 64:1
        UP RUNNING
        RX bytes:717 acl:0 sco:0 events:42 errors:0
        TX bytes:1532 acl:0 sco:0 commands:42 errors:0

rfcomm

rfcommはBluetoothの通信プロファイルに1つです。このプロファイルでは、通信上はBluetoothで接続されているデバイスと通信を行うのですが、見かけ上、シリアル通信としてプログラムできるという利点があります。

image.png

rmmod

Loadable Kernel Moduleのアンロードを行います。後で説明するmodprodeで読み込んだ情報を削除します。OBD2の接続するとき毎回行う必要はないかもしれないんですが、念のためやってます。rfcomm自体は再起動すると設定が失われます。逆に言えば電源が入っていればずっと保持されるので、rmmodしておいたほうが無難かと...。

modprobe

rmmodの反対でLoadable Kernel Moduleのロードを行います。modprobe rfcommrfcommプロトコルが利用可能になります。

rfcomm bind

rfcommセクションでも説明したようにrfcommプロトコルを利用すれば、シリアル通信としてプログラムできるようになります。しかし、そのままではシリアル通信として利用できないので、シリアルポートにrfcommbindする必要があります。バインドする先は/dev/rfcommXです。Xは0~9などの任意の数値を指定できますが、今回は0を指定しておきます。

sudo rfcomm bind 0 AA:BB:CC:11:22:33

ここのAA:BB:CC:11:22:33とは接続先のBTのMACアドレスです。sudo hcitool lescansudo hcitool scanで見つけたMACアドレスをメモしておきましょう。ただ、今回利用するELM327モジュールは全てAA:BB:CC:11:22:33に統一されているようでした。

ls /dev | grep rfcomm

無事にバインドが成功していれば、ls /dev | grep rfcommを実行すれば先ほど指定した番号のシリアルポートが確認できるかと思います。

rfcomm listen

先ほどバインドした番号のシリアルポートに来るデータ読む(listenする)コマンドを実行します。これを実行することで/dev/rfcomm0に流れてくるデータを他のプログラムでも確認することができます。1はチャンネルを表しています。

sudo rfcomm listen 0 1 &

&とは、このrfcomm listenというコマンドはプロセスとして動作します。このlistenの動作自体はバックグラウンドで動作してもらいたいので&を付けます。つまり、この&を付けて実行したコマンドはバックグラウンドで動作するようになります。

logging.py

使用しているOBD2ライブラリの公式サイトはこちら

import obd
from obd import OBDStatus
import time, csv
import os

f = open("data.csv", "w")
writer = csv.writer(f, lineterminator="\n")

connection = obd.OBD()
print (connection.status())

if connection.status() == OBDStatus.CAR_CONNECTED:
    writer.writerow(["rpm", "speed"])
    while(True):
        try:
            rpm = connection.query(obd.commands.RPM)
            speed = connection.query(obd.commands.SPEED)
            writer.writerow([rpm.value.magnitude, speed.value.magnitude])
            print (rpm.value.magnitude, speed.value.magnitude)
            # time.sleep(.2)
        except KeyboardInterrupt:
            pass
else:
    connection.close()

obd.OBD()で先ほどbindしたポート等と確認して自動的にOBD2スキャンツール(ELM327)にアクセスしてくれます。その接続先(データ取得先)をconnectionという変数で保持し、今後この変数に対して、値の要求などを行います。 接続に使用するポートを変更したいときには、以下のように使用するポートを指定します。

import obd

# 直接シリアルポートを指定
connection = obd.OBD("/dev/ttyUSB0")

# 利用できるシリアルポートから選択する
ports = obd.scan_serial()      # ['/dev/ttyUSB0', '/dev/ttyUSB1']
connection = obd.OBD(ports[0]) 

接続できているかは以下で確認できます。

connection.is_connected()

#or
from obd import OBDStatuus
if connection.status() == OBDStatus.CAR_CONNECTED:

OBDStatusには以下が定義されています

# no connection is made
OBDStatus.NOT_CONNECTED # "Not Connected"

# successful communication with the ELM327 adapter
OBDStatus.ELM_CONNECTED # "ELM Connected"

# successful communication with the ELM327 and the vehicle
OBDStatus.CAR_CONNECTED # "Car Connected"

Basic Usage

import obd

connection = obd.OBD() # auto-connects to USB or RF port

cmd = obd.commands.SPEED # select an OBD command (sensor)

response = connection.query(cmd) # send the command, and parse the response

print(response.value) # returns unit-bearing values thanks to Pint

基本的な使い方としては上記の通りで、connectionに対して値を要求するクエリを実行します。そのクエリの返り値が要求した結果となっています。別にcmdを使わなくてもconnection.query(obd.commands.SPEED)でも動作します。お好みで

Module Layout

このライブラリのレイアウトは以下の通りです。すべてを使いこなせる気はしませんが...。

import obd

obd.OBD            # main OBD connection class
obd.Async          # asynchronous OBD connection class
obd.commands       # command tables
obd.Unit           # unit tables (a Pint UnitRegistry)
obd.OBDStatus      # enum for connection status
obd.scan_serial    # util function for manually scanning for OBD adapters
obd.OBDCommand     # class for making your own OBD Commands
obd.ECU            # enum for marking which ECU a command should listen to
obd.logger         # the OBD module's root logger (for debug)

csv出力

f = open("data.csv", "w")
writer = csv.writer(f, lineterminator="\n")

データ出力する先と改行を指定します。ファイルの出力先はlogging.pyと同じ位置になりますが、パス指定すれば任意の場所に出力することも可能です。データをファイルに書き込むときには以下のように記述します。

# カラム名の書き込み
writer.writerow(["rpm", "speed"])

# 取得データの書き込み
writer.writerow([rpm.value.magnitude, speed.value.magnitude])

取得したデータ分析

この取得したデータをOctaveを使って、解析的なことをしてみたいなと思います。今回利用するのはGNU Octaveです。

GNU Octave は、主に数値解析を目的とした高レベルプログラミング言語である。Octaveは線形ならびに非線形問題を数値的に解くためのコマンドライン·インタフェースを提供する。また、 MATLABとほぼ互換性のある、数値実験を行うためのプログラミング言語として使用することができる。 Octaveは、GNUプロジェクトの一つでGNU General Public Licenseの条件の下のフリーソフトウェアである。 GNU OctaveScilabは、MATLABオープンソース代替品の一つである。 ただし、Octaveは、ScilabよりもMATLABとの互換性維持に重点を置いている Wikipedia - GNU Octave

ダウンロード先はこちら 解析は普通のPCを使います。今回はWindowsを使いますがMacでも同じです。インストール等の開設は省略します。インストーラーで終わってしまうので...。Macportsbrewでインストールになります。

今回は「エンジン回転数」のデータを使って、回転数のピーク部を検出して印付けしたいと思います。100%検出できるわけではないです。以下のような画像のグラフを作成します。 キャプチャ.PNG

解析データ作成

生成したcsvのデータから、解析したデータのみを取り出します。今回はエンジン回転数のファイルを作成します。ファイルの中身は数値のみになります。

636.5
644.5
642
641.25
638.25
639
639
640.5
:

Octaveでファイル読み込み

Octaveでそれぞれのファイルを読み込みます。この読み込まれたデータはOctave上では行列として認識されます。上記2つのデータの場合「n行1列」のデータ配列となります。しかし、rpm[1] speed[10]のようにアクセスしたい場合は「転置」を行う必要があります。

% ファイルからデータ読み込み
rpm   = load('car_rpm.txt'  , ' ') ;
% 行列の転置
rpm   = rpm.' ;

グラフ設定

そして、横軸(時間軸)を決めていきたいのですが、今回は時間を使用した解析は行わないので「1」から連番で振っていきます。

%
% bからcまでのm個を用いた「m X 1」の行列を生成
% a = b : c ;
%
x = 1 : length(rpm) ; 

これでグラフに表示する準備ができました。グラフを表示するには以下のように記述します。

% データをグラフにプロット
plot(x, rpm) ;
hold on ;

% ラベルの表示設定
xlabel('TIME') ;
ylabel('rpm') ;
%
% 走行データ解析プログラム
% コマンドウィンドウで pkg load signal 必須
%

% 走行データの読み込み
rpm   = load('car_rpm.txt', ' ') ;

% 走行データ配列の転置
rpm = rpm.' ;

% 時間軸の生成
x = 1 : length(rpm) ;

% グラフのプロット
plot(x, rpm) ;
hold on ;

% ラベルの表示設定
xlabel('TIME') ;
ylabel('RPM') ;

グラフ結果

car_blog1.jpg

エンジン回転数のピーク値を見つける

Octaveには様々な計算ライブラリが搭載されています。今回単純にピークを検出するfindpeaksを用います。この関数はsignalパッケージに含まれているため、Octaveのコマンドウィンドウで

pkg load signal

と打ってもらう必要があります。

findpeaks

実際に先ほどのプログラムにfindpeaksを追加してグラフに表示させてみます。

%
% 走行データ解析プログラム
% コマンドウィンドウで pkg load signal 必須
%

% 走行データの読み込み
rpm   = load('car_rpm.txt', ' ') ;

% 走行データ配列の転置
rpm = rpm.';

% 時間軸の生成
x = 1 : length(rpm);

[pks1 idx1] = findpeaks(rpm, 'DoubleSided', 'MinPeakDistance', 1);

% グラフのプロット
plot(x, rpm);
hold on;
plot(idx1, pks1, 'o') ;

% ラベルの表示設定
xlabel('TIME');
ylabel('RPM');

car_blog2.jpg

しかし、このfindpeaksを用いただけでは正常にピーク値を導き出すことができません。今求めたいpeakとは「明らかに突出していて、回転数が前回の測定値よりも格段に大きいとき」という意味を持ちますので、以下のようなpeak値は不要なのです。

car_blog3.jpg

なので、プログラムにひと手間加え、次のようなプログラムが完成します。

ピーク値検出

%
% 走行データ解析プログラム
% コマンドウィンドウで pkg load signal 必須
%

% 走行データの読み込み
rpm   = load('car_rpm.txt', ' ') ;

% 走行データ配列の転置
rpm = rpm.';

% 時間軸の生成
x = 1 : length(rpm);

[pks1 idx1] = findpeaks(rpm, 'DoubleSided', 'MinPeakDistance', 1);

new_pks1 = [];
new_idx1 = [];
th_rpm = 20; % 認識する閾値を指定

for i = 3 : length(rpm) - 2

  prev    = rpm(i - 2);  % currentの一つ前のデータ
  current = rpm(i);      % peakかどうか確認する点
  next    = rpm(i + 2);  % currentの一つ後のデータ

  b_slope = current - prev; % 2つのデータの差を計算
  a_slope = current - next; % 2つのデータの差を計算

  %
  % b_slope > 0 と a_slope > 0 より current が前後よりも突出している(つまりpeakの可能性)
  % b_slope, a_slope ともに閾値以上の差(つまりpeakの可能性)
  %
  if (b_slope > 0) && (b_slope > th_rpm)
    if (a_slope > 0) && (a_slope > th_rpm)
      if (rpm(i) > 1500)
        % 可能性のあるものだけ、新規変数に追加
        new_pks1 = [new_pks1, rpm(i)];
        new_idx1 = [new_idx1, i];
      endif
    endif
  endif
endfor

% グラフのプロット
plot(x, rpm);
hold on;
plot(new_idx1, new_pks1, 'o');

% ラベルの表示設定
xlabel('TIME');
ylabel('RPM');

car_blog4.jpg car_blog5.jpg

修正を加える前よりかは、きれいに取得できていることが分かると思います。

まとめ

世の中には「家具、家電、その他いろいろ」なものに対するIoT製品や記事はよく見かけるのですが、「車」という分野ではあまり見かけないような気がしました。最近では「自動運転」が騒がれていますが、こんな感じの簡単で面白い「情報 X 車」コラボをしたくなったので、あえてレベルを下げてみました。 まだまだ、できることはあります。これをうまく応用して作りこむと image.png

こんな感じの「自作の」ヘッドマウントディスプレイすら作れるようになると思います。ってかできます。なので、この記事で興味を持った方は調べてみてください。もっと面白いことたくさんあります!!

ではでは~。

ソースコード

https://gist.github.com/nomunomu0504/dc3dc538bbc7738ecdff2a771a0c6c29