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

# オフィスや自宅を快適にするIoT byゆめみ③ Advent Calendar 2018
さて、このAdvent Calendarの趣旨に合っているのか？と思っている皆さん。「タイトル」的にはアウトですが、「テーマ」的にはセーフという曲折を含めつつ、書いていきます:)

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

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

![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/110131/8394d1b4-4a12-f638-7e02-d4521453ac76.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](https://qiita-image-store.s3.amazonaws.com/0/110131/31ee6121-3502-ca28-4975-7e15ceeb475a.png)

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

[超小型モデル OBDII 診断 ELM327 Bluetooth ブルートゥース スキャンツール テスター OBD2 - Amazon.co.jp](https://www.amazon.co.jp/%E8%B6%85%E5%B0%8F%E5%9E%8B%E3%83%A2%E3%83%87%E3%83%AB-ELM327-Bluetooth-%E3%83%96%E3%83%AB%E3%83%BC%E3%83%88%E3%82%A5%E3%83%BC%E3%82%B9-%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%83%84%E3%83%BC%E3%83%AB/dp/B00AK6ENQ6)

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

## start.sh

```bash: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デーモンを使うのでインストールされてない場合は以下の通りインストールします

```bash
# 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です。

```bash
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モジュールを確認します

```bash
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です。

```bash
$ 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の通信プロファイルに１つです。このプロファイルでは、通信上はBluetoothで接続されているデバイスと通信を行うのですが、見かけ上、シリアル通信としてプログラムできるという利点があります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/110131/d23d152f-3142-27fd-31eb-5af5e4116079.png)


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

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

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

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

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

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

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

```bash
sudo rfcomm listen 0 1 &
```

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

## logging.py

使用しているOBD2ライブラリの公式サイトは[こちら](https://python-obd.readthedocs.io/en/latest/)

```python:logging.py
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`という変数で保持し、今後この変数に対して、値の要求などを行います。
接続に使用するポートを変更したいときには、以下のように使用するポートを指定します。

```python
import obd

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

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

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

```python
connection.is_connected()

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

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

```python
# 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

```python
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
このライブラリのレイアウトは以下の通りです。すべてを使いこなせる気はしませんが...。

```python
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出力

```python
f = open("data.csv", "w")
writer = csv.writer(f, lineterminator="\n")
```
データ出力する先と改行を指定します。ファイルの出力先は`logging.py`と同じ位置になりますが、パス指定すれば任意の場所に出力することも可能です。データをファイルに書き込むときには以下のように記述します。

```python
# カラム名の書き込み
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 OctaveとScilabは、MATLABのオープンソース代替品の一つである。 ただし、Octaveは、ScilabよりもMATLABとの互換性維持に重点を置いている
Wikipedia - GNU Octave

ダウンロード先は[こちら](https://www.gnu.org/software/octave/download.html)
解析は普通のPCを使います。今回はWindowsを使いますがMacでも同じです。インストール等の開設は省略します。インストーラーで終わってしまうので...。Macは`ports`か`brew`でインストールになります。

今回は「エンジン回転数」のデータを使って、回転数のピーク部を検出して印付けしたいと思います。100%検出できるわけではないです。以下のような画像のグラフを作成します。
![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/110131/8394d1b4-4a12-f638-7e02-d4521453ac76.png)

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

```:car_rpm.txt
636.5
644.5
642
641.25
638.25
639
639
640.5
:
```

## Octaveでファイル読み込み


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

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

## グラフ設定
そして、横軸（時間軸）を決めていきたいのですが、今回は時間を使用した解析は行わないので「１」から連番で振っていきます。

```matlab
%
% bからcまでのm個を用いた「m X 1」の行列を生成
% a = b : c ;
%
x = 1 : length(rpm) ; 
```
これでグラフに表示する準備ができました。グラフを表示するには以下のように記述します。

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

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

```matlab::ここまでの全体像
%
% 走行データ解析プログラム
% コマンドウィンドウで 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](https://qiita-image-store.s3.amazonaws.com/0/110131/8b78a879-e99f-efe8-6c0d-7b866000d7d2.jpeg)

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

```
pkg load signal
```

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

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

```matlab
%
% 走行データ解析プログラム
% コマンドウィンドウで 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](https://qiita-image-store.s3.amazonaws.com/0/110131/b81dfa85-9e5c-418f-80a6-6baa50700e7d.jpeg)

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

![car_blog3.jpg](https://qiita-image-store.s3.amazonaws.com/0/110131/cac7a40e-99e4-be4e-f9c7-0568c32d8de0.jpeg)

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

## ピーク値検出

```matlab
%
% 走行データ解析プログラム
% コマンドウィンドウで 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; % ２つのデータの差を計算
  a_slope = current - next; % ２つのデータの差を計算

  %
  % 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](https://qiita-image-store.s3.amazonaws.com/0/110131/057c9c5c-d7ee-c2d5-a0ca-9dd346a0b2f1.jpeg)
![car_blog5.jpg](https://qiita-image-store.s3.amazonaws.com/0/110131/54ec863c-a191-12be-bca6-9eaccb5abb09.jpeg)

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


# まとめ
世の中には「家具、家電、その他いろいろ」なものに対するIoT製品や記事はよく見かけるのですが、「車」という分野ではあまり見かけないような気がしました。最近では「自動運転」が騒がれていますが、こんな感じの簡単で面白い「情報 X 車」コラボをしたくなったので、あえてレベルを下げてみました。
まだまだ、できることはあります。これをうまく応用して作りこむと
![image.png](https://qiita-image-store.s3.amazonaws.com/0/110131/27657381-25f6-d744-de6d-5f6682d16510.png)

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

ではでは～。

# ソースコード
https://gist.github.com/nomunomu0504/dc3dc538bbc7738ecdff2a771a0c6c29