Node.js で車とシリアル通信する

先日車が壊れたのを機に車について調べていたら OBD (On-board diagnostics) という診断機能を知りました。大抵の車には OBD のコネクタが搭載されていて、これに ELM327というマイコンを使った製品をつなぐと Bluetooth のシリアル通信などで簡単に情報をやりとりできるそうです。おもしろそうなので試してみました。

**重要** このポストと同じ方法で車の診断ログをリセットしたりすることもできます。診断ログが正しくない状態になると、修理の適切なタイミングなどがわからなくなるはずです。コマンドを車に送る前によく考えた上で自己責任で試してください。

---

## ELM327 のドングルを手に入れる

これらの製品は [Amazon](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_4?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&dchild=1&keywords=ELM327&qid=1610854994&s=automotive&sr=1-4) 等で廉価に入手できるようです。全製品で固定っぽい Bluetooth の接続パスワードが書かれていたので念のため黒塗りにしました。

![connector1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125062/4237f8bf-c8b0-0d6c-e7fc-7732f0b47e03.jpeg)

## 車のOBDポートを探す

運転席のどこかにある場合が多いようです。私の車はここにありました。

![connector2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125062/6b6829a6-6764-405b-dd91-499ba2109ef8.jpeg)

ここに購入した上記のドングル型デバイスを接続します。

## 通信方法を確認

### 送信

[AT コマンド](https://www.sparkfun.com/datasheets/Widgets/ELM327_AT_Commands.pdf)というものを送るとマイコンを操作することができるようです。今回は以下の2つのコマンドを使いました。

- `AT Z`: 状態をリセットする
- `AT SP 0`: OBDの通信プロトコルを自動で検出

通信プロトコルは様々なものがあるようですが、私の車で試した限りでは `AT SP 0` の自動検出でうまく通信できました。

AT コマンドの他に OBD コマンドというものがあり、これで様々な OBD の情報を取得できました。Wikipedia に[コマンドのリスト](https://en.wikipedia.org/wiki/OBD-II_PIDs)があったため、これを参照しました。

OBD コマンドは `Mode` と　`PID` から構成されています。現在のデータを読み取る `01` というモードを今回は使いました。取得できるデータごとに `PID` が振られており、例えば `PID` が `0C` のエンジン回転数の情報を読み取る OBD コマンドは `010C` になります。

### 受信

データは、`４1 0C AA BB ...` のようにスペース区切りの16進数の文字列で帰ってきます。1つめが `Mode` の値に40を足したもの、2つめが `PID` で、それ以降が実際のデータになります。例えばエンジン回転数の場合は値が4倍されているなど、それぞれ決まりがあるようです。


## Node.js でシリアル通信をする

[serialport](https://www.npmjs.com/package/serialport) というライブラリが著名そうだったので利用しました。

Node.js のランタイムでシリアルポートをオープンする TypeScript のコードはこんな感じ。ELM327 のデバイスに Bluetooth で接続した後に `connect` の関数を実行すると、非同期で `SerialPort` のインスタンスが得られます。


```typescript
import SerialPort from 'serialport'

const DEVICE_PATH = '/dev/tty.OBDII-SPPslave'
export const connect = () => new Promise<SerialPort>((resolve, reject) => {
    const port = new SerialPort(DEVICE_PATH, { baudRate: 9600, autoOpen: false })
    console.log(`[SP] opening on ${DEVICE_PATH}..`)
    port.open((error) => {
        if(error) {
            console.log('[SP] port opening error.')
            reject(error)
        } else {
            console.log('[SP] port opened.')
            resolve(port)
        }
    })
})
```

ポートに書き込むには `write` メソッドを使います。コマンドのデリミタはキャリッジリターンなので注意してください。

```typescript
// `AT Z` の AT コマンドを送出
port.write('AT Z\r')
```

データはイベントリスナーを使って取得できます。
文字列を処理して、目的の値を抽出します。

```typescript
port.on('data', (data: string) => {
    const [mode, pid, ...values] = data.split(' ')
    
    let modeNum
    try {
        modeNum = parseInt(mode, 16) - 40  
    } catch {
        return
    }

    if(modeNum === 1) {
        const hexValue = parseInt(values.filter(value => value !== '').join(''), 16)
        if(pid === '0C') {
            console.log('EngineRPM: ' + hexValue / 4)
        }
    }
})
```

## WebSocket でデータをブラウザに送信できるようにする

上記でデータが取得できる準備が整ったので、あとは WebSocket を使ってブラウザにデータを送ってみます。[ws](https://www.npmjs.com/package/ws) というライブラリを使います。

WebSocket サーバーを立ち上げるコードはこんな感じ。 `createSocket` の関数をコールすると非同期で `WebSocket` のインスタンスが得られます。

```typescript
import WebSocket from 'ws'

export const createSocket = () => new Promise<WebSocket>((resolve, reject) => {
    console.log('[WS] connecting...')
    const wss = new WebSocket.Server({ port: 8081 })
    wss.on('connection', (socket) => {
        console.log('[WS] connected.')
        resolve(socket)
    })
})
```

`WebSocket` のインスタンスを使って、シリアル通信で得られたデータを送ります。上記のシリアル通信のイベントリスナーに `socket.send(data)` を差し込めば OK です。



クライアント側は、こんな感じ。

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #display {
            width: 500px;
            height: 100px;
            font-size: 2em;
        }
    </style>
</head>
<body>
    <textarea id="display"></textarea>
    <script>
        const ws = new WebSocket('ws://localhost:8081')
        const display = document.getElementById('display')

        let currentData = {}

        ws.onmessage = (event) => {
           const data = JSON.parse(event.data)
            display.value = "エンジン回転数: " + data.engineRPM + "rpm"
        }
    </script>
</body>
</html>
```

## 動作させる

以上で全ての材料が揃ったので、これを組み込んでいきます。今回は Mac 上で以下の全てのプロセスを立ち上げて試してみました。

- 車とシリアル通信をする WebSocket サーバー
- クライアントを表示する ウェブサーバー

また、エンジン回転数以外にも現在の運転速度と冷却水温度も取得してみました。

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125062/f1f791b2-2908-0da0-032c-02ee31f7152d.png)](https://youtu.be/jtPIO3J6cEs)
[https://youtu.be/jtPIO3J6cEs](https://youtu.be/jtPIO3J6cEs)

動画を撮りながら走ってみると、いい感じにデータが取れているようです✌️

---

上記のコードは以下のリポジトリにあります。
https://github.com/kamataryo/parse-obd-serial