Z900RSのCANデータをAWS IoT FleetWiseで収集する

バイクも電子制御が高度化しており、しばらく前からバイクにも診断用にOBD2のコネクタが装備されている車種が増えてきています。([新型は2020年12月から、継続生産車は2022年4月から義務化らしい](https://www.mlit.go.jp/report/press/jidosha10_hh_000223.html))

そんなわけで、[先日乗り換えたZ900RS](https://sparkgene.hatenablog.com/entry/2024/12/07/185556)にも搭載されており、これを試してみたいなと思いました。

<font color="red">注意：この記事で紹介している内容は、メーカーが保証している作業ではありません。場合によっては故障の原因となります。参考にする場合はすべて自己責任で作業してください。</font>

シートを外すと赤いカプラーがフレームに固定されています。
![IMG_8747.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36184/a032ec45-2fe1-4859-8f19-91bab3ae9e59.jpeg)


車のOBD2コネクタとは形状が違い、6ピンのカプラーということもあり、汎用的なOBD2ケーブルは直接繋げられないので、[キタコが出している変換コネクタ](https://amzn.asia/d/jceTQI0)を使います。(ホンダ車用と書かれてますが、Z900RSはこのタイプでした)

OBDのデータを読み出すなら、Pythonなどのライブラリもあるので、簡単に試せるのですが、せっかくなのでコネクテッドバイクとしての構成も考え、AWS IoT FleetWiseを使ってデータの収集をしてみたいと思います。

AWS IoT FleetWiseを実行するデバイスはLinuxベースである必要があるため、家に転がってたRaspberry Pi 3を使うことにしました。OBD2のコネクタからCAN Busに接続できるので、[Raspberry Pi用のCAN BUS Hat](https://amzn.asia/d/3HRrxoq)を使い、[OBD2のコネクタ](https://amzn.asia/d/iCHiRII)に繋いで接続します。

ネットワークも必要なので、これまた家に転がっていた[SoracomのSIMとドングル](https://www.switch-science.com/products/2606)を使って直接通信できるようにしました。
今回使ったのは3G向けのSIMですが、[今後停波が予定されている](https://support.soracom.io/hc/ja/articles/4410273267737-%E6%97%A5%E6%9C%AC%E5%9B%BD%E5%86%85%E3%81%A7-3G-%E9%80%9A%E4%BF%A1%E5%B0%82%E7%94%A8%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99-FOMA-%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E7%B5%82%E4%BA%86%E3%81%AE%E5%BD%B1%E9%9F%BF%E3%81%AF%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%81%8B)ので、この辺のデバイスも新しく揃えないと。。。

最終的にこんな感じのデバイスとなりました。

![IMG_9016.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36184/ea9fc77a-9981-4b67-9c3b-58c1262115e9.jpeg)

電源はOBD2コネクター経由でも取れるのですが、12vであるため降圧する必要があり、めんどくさかったのでモバイルバッテリーを繋いで試しました。

実際に走行してみて収集できたデータデータをGrafanaで可視化してみるとこのようなグラフが見られました。

![grafana.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36184/11f2b4f4-eccd-44a8-ae2d-3fd8e3581adf.png)

取れるかなと思ったシグナルの内、エンジン回転数、速度、水温、スロットルポジションだけが取れてました。OBDのシグナルのIDは、各社独自の値を使ってたりするので、AWS IoT FleetWiseが用意している標準の設定だと合わないことがあります。メーカーから公開されていれば、その情報をAWS IoT FleetWIseに登録すれば、正しく取れるようになるはず。


実走で発生した通信量はSoracomのコンソールから確認するとこんな感じ。

![soracom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36184/a3a4affc-df28-4317-a316-de51fd8c5c17.png)

17時〜19時の間はかなり下りの通信が発生していますが、これはリモートからSSHしたくてAWS IoT GreengrassのSecure TunnelingのデプロイとかをSIM通信を有効にした後、実行してしまったから。

AWS IoT FleetWiseの通信部分は、19時の時間だけ見ると、上りで 594 KiB、下りで 129 KiB でした。

今回は、4種類のシグナルを1秒おきに収集したシグナルを10秒間隔で送信するキャンペーンをAWS IoT FleetWiseで設定したので、そこそこ高頻度なデータ収集となってます。

![grafana-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36184/1402f6a4-c827-4b06-83ee-e1d4b20592a7.png)

こんな感じで、もっと細かくデータを見ることもできます。

最近はバイクのアクティビティーを記録するアプリもあり、私も使っています。ただ、GPSから想定した速度しか記録できないので、このように直接バイクからデータを取れると、より詳細な情報が見られるようになり、面白そうだなと思いました。

以降では、AWS IoT FleetWiseのセットアップや、キャンペーンの設定を行った際の手順を紹介しています。

## AWS IoT FleetWise agentの準備

この手順で用意されているCloudFormationのテンプレートを使ってインスタンスを起動します。
https://github.com/aws/aws-iot-fleetwise-edge/blob/main/docs/rpi-tutorial/raspberry-pi-tutorial.md#step-2-launch-your-development-machine

起動されたインスタンスの情報

```bash
uname -a

Linux ip-172-31-35-230 5.15.0-1075-aws #82~20.04.1-Ubuntu SMP Thu Dec 19 05:23:06 UTC 2024 aarch64 aarch64 aarch64 GNU/Linux
```

### ビルド

```bash
git clone https://github.com/aws/aws-iot-fleetwise-edge.git ~/aws-iot-fleetwise-edge

cd ~/aws-iot-fleetwise-edge \
&& sudo -H ./tools/install-deps-native.sh

./tools/build-fwe-native.sh
```

### 必要な情報をプロビジョニング

実行する前にAWSのクレデンシャルが必要です。`REGION`と`VEHICLE_NAME` 自分の環境に合わせます。

```bash
REGION=us-east-1
VEHICLE_NAME=z900rs

mkdir -p ~/aws-iot-fleetwise-deploy \
&& cd ~/aws-iot-fleetwise-deploy \
&& cp -r ~/aws-iot-fleetwise-edge/tools . \
&& mkdir -p build \
&& cp ~/aws-iot-fleetwise-edge/build/aws-iot-fleetwise-edge build \
&& mkdir -p config \
&& cd config \
&& ../tools/provision.sh \
   --region $REGION \
   --vehicle-name $VEHICLE_NAME \
   --certificate-pem-outfile certificate.pem \
   --private-key-outfile private-key.key \
   --endpoint-url-outfile endpoint.txt \
   --vehicle-name-outfile vehicle-name.txt \
&& ../tools/configure-fwe.sh \
   --input-config-file ~/aws-iot-fleetwise-edge/configuration/static-config.json \
   --output-config-file config-0.json \
   --log-color Yes \
   --vehicle-name `cat vehicle-name.txt` \
   --endpoint-url `cat endpoint.txt` \
   --can-bus0 can0 \
&& cd .. \
&& zip -r aws-iot-fleetwise-deploy.zip .
```

SCPでローカルに `~/aws-iot-fleetwise-deploy/aws-iot-fleetwise-deploy.zip` をダウンロードします。

## Raspberry Piのセットアップ

使ったのは、Raspberry Pi 3、Ubuntu Server 20.04.5 LTS (64bit)のOS

```bash
uname -a
Linux ubuntu 5.4.0-1122-raspi #134-Ubuntu SMP PREEMPT Fri Jan 17 12:48:32 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
```

アップデート

```bash
sudo apt update \
&& sudo apt upgrade -y \
&& sudo apt install -y unzip
```

### CAN Hatの設定

利用している Can Hatはこちらのものですので、サイトの手順を参考に必要な設定を行います。

https://www.waveshare.com/wiki/RS485_CAN_HAT

```bash
sudo vi /boot/config.txt
```

以下を追記

```
dtparam=spi=on
dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25,spimaxfrequency=2000000
```

`sudo reboot` で再起動

### FleetWise agentをインストール

SCPでダウンロードした `aws-iot-fleetwise-deploy.zip` をラズパイにSCPでコピーします。

```bash
mkdir -p ~/aws-iot-fleetwise-deploy && cd ~/aws-iot-fleetwise-deploy \
&& unzip -o ~/aws-iot-fleetwise-deploy.zip \
&& sudo mkdir -p /etc/aws-iot-fleetwise \
&& sudo cp config/* /etc/aws-iot-fleetwise \
&& sudo ./tools/install-fwe.sh

sudo -H ~/aws-iot-fleetwise-deploy/tools/install-socketcan.sh
```

CANを有効にするため `/usr/local/bin/setup-socketcan.sh` へ以下を追記

```
ip link set up can0 txqueuelen 1000 type can bitrate 500000 restart-ms 100
```

サービスを再起動

```bash
sudo systemctl restart setup-socketcan
sudo systemctl restart fwe@0
```

FleetWise agentのログを確認

```bash
sudo journalctl -fu fwe@0 --output=cat

[Thread: 8288] [2025-03-22T03:46:27.984Z] [WARN ] [AwsBootstrap.cpp:52] [LogStream()]: [FileSystemUtils: Home dir not stored in environment, trying to fetch manually from the OS.]
[Thread: 8288] [2025-03-22T03:46:28.033Z] [INFO ] [IoTFleetWiseEngine.cpp:390] [connect()]: [Maximum AWS SDK Heap Memory Bytes has been configured:10000000]
[Thread: 8288] [2025-03-22T03:46:28.033Z] [INFO ] [IoTFleetWiseEngine.cpp:420] [connect()]: [ConnectionType is iotCore]
[Thread: 8288] [2025-03-22T03:46:28.035Z] [INFO ] [AwsIotConnectivityModule.cpp:184] [createMqttClient()]: [Creating MQTT client]
[Thread: 8289] [2025-03-22T03:46:28.096Z] [INFO ] [AwsIotConnectivityModule.cpp:299] [operator()()]: [Attempting MQTT connection]
[Thread: 8288] [2025-03-22T03:46:28.096Z] [INFO ] [CollectionSchemeManager.cpp:72] [start()]: [Thread started]
[Thread: 8288] [2025-03-22T03:46:28.097Z] [INFO ] [IoTFleetWiseEngine.cpp:1689] [connect()]: [Engine Connected]
[Thread: 8296] [2025-03-22T03:46:28.097Z] [INFO ] [Persistency.cpp:61] [retrieve()]: [Retrieved a CollectionSchemeList of size zero]
[Thread: 8296] [2025-03-22T03:46:28.097Z] [INFO ] [Persistency.cpp:61] [retrieve()]: [Retrieved a DecoderManifest of size zero]
Started successfully
```

以上で、デバイス側の準備ができました。

## クラウドのリソースを作成

クラウドのリソースを作成するので、FleetWise agentをビルドした時に使ったEC2インスタンスから実行します。githubのリポジトリに含まれていたサンプル値をいくつか利用しています。

```bash
cd ~/aws-iot-fleetwise-edge/tools/cloud
```

### Timestream DBの作成

```bash
aws timestream-write create-database --database-name BikeDatabase
aws timestream-write create-table --database-name BikeDatabase --table-name BikeTable
```

### Signal catalogの作成

```bash
SIGNAL_CATALOG_NAME=obd-signals

aws iotfleetwise create-signal-catalog \
--name $SIGNAL_CATALOG_NAME \
--nodes file://vehicle-node.json

aws iotfleetwise update-signal-catalog \
--name $SIGNAL_CATALOG_NAME \
--nodes-to-update file://obd-nodes.json
```

### Model Manifestの作成

```bash
SIGNAL_CATALOG_ARN=`aws iotfleetwise get-signal-catalog --name $SIGNAL_CATALOG_NAME --query "arn" --output text`
MODEL_NAME=z900rs-2025
MODEL_MANIFEST=$MODEL_NAME-manifest
```

このモデルが持つSignalの一覧を作成。(Signal catalogからめぼしいものを指定。が、実際にはこの中から4種類しか取れなかった)

```bash
cat << EOF > ./obd-model-nodes.json
[
    "Vehicle.OBD.CoolantTemperature",
    "Vehicle.OBD.EngineCoolantTemperature",
    "Vehicle.OBD.OilTemperature",
    "Vehicle.OBD.EngineSpeed",
    "Vehicle.OBD.Speed",
    "Vehicle.OBD.ThrottlePosition",
    "Vehicle.OBD.FuelLevel",
    "Vehicle.OBD.DemandEngineTorque",
    "Vehicle.OBD.Odometer",
    "Vehicle.OBD.ControlModuleVoltage"
]
EOF
```

Model manifestを作成して有効化

```bash
aws iotfleetwise create-model-manifest \
--name $MODEL_MANIFEST \
--signal-catalog-arn $SIGNAL_CATALOG_ARN \
--nodes file://obd-model-nodes.json

aws iotfleetwise update-model-manifest \
--name $MODEL_MANIFEST \
--status ACTIVE
```

### Decoder Manifestの作成

```bash
DECODER_MANIFEST_NAME=$MODEL_NAME-decoder-manifest
MODEL_MANIFEST_ARN=`aws iotfleetwise get-model-manifest --name $MODEL_MANIFEST --query "arn" --output text`
```

Model Manifestに登録したSignalのdecode情報を `obd-decoders.json` から抜粋して作成

```bash
cat << EOF > ./$MODEL_NAME-obd-decoders.json
[
  {
    "fullyQualifiedName": "Vehicle.OBD.CoolantTemperature",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 1,
      "offset": -40,
      "pid": 5,
      "pidResponseLength": 1,
      "scaling": 1,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.EngineCoolantTemperature",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 1,
      "offset": 0,
      "pid": 103,
      "pidResponseLength": 3,
      "scaling": 1,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.OilTemperature",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 1,
      "offset": -40,
      "pid": 92,
      "pidResponseLength": 1,
      "scaling": 1,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.EngineSpeed",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 2,
      "offset": 0,
      "pid": 12,
      "pidResponseLength": 2,
      "scaling": 0.25,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.Speed",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 1,
      "offset": 0,
      "pid": 13,
      "pidResponseLength": 1,
      "scaling": 1,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.ThrottlePosition",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 1,
      "offset": 0,
      "pid": 17,
      "pidResponseLength": 1,
      "scaling": 0.39215686274509803922,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.FuelLevel",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 1,
      "offset": 0,
      "pid": 47,
      "pidResponseLength": 1,
      "scaling": 0.39215686274509803922,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.DemandEngineTorque",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 1,
      "offset": -125,
      "pid": 97,
      "pidResponseLength": 1,
      "scaling": 1,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.Odometer",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 4,
      "offset": 0,
      "pid": 166,
      "pidResponseLength": 4,
      "scaling": 0.1,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  },
  {
    "fullyQualifiedName": "Vehicle.OBD.ControlModuleVoltage",
    "interfaceId": "0",
    "type": "OBD_SIGNAL",
    "obdSignal": {
      "byteLength": 2,
      "offset": 0,
      "pid": 66,
      "pidResponseLength": 2,
      "scaling": 0.001,
      "serviceMode": 1,
      "startByte": 0,
      "bitMaskLength": 8,
      "bitRightShift": 0
    }
  }
]
EOF
```

decoder manifestを作成

```bash

aws iotfleetwise create-decoder-manifest \
--name $DECODER_MANIFEST_NAME \
--model-manifest-arn $MODEL_MANIFEST_ARN \
--network-interfaces file://network-interface-obd.json \
--signal-decoders file://$MODEL_NAME-obd-decoders.json

aws iotfleetwise update-decoder-manifest \
--name $DECODER_MANIFEST_NAME \
--status ACTIVE
```

### Vehicleの作成

```bash
VEHICLE_NAME=z900rs
DECODER_MANIFEST_ARN=`aws iotfleetwise get-decoder-manifest --name $DECODER_MANIFEST_NAME --query "arn" --output text`

aws iotfleetwise create-vehicle \
--vehicle-name $VEHICLE_NAME \
--model-manifest-arn $MODEL_MANIFEST_ARN \
--decoder-manifest-arn $DECODER_MANIFEST_ARN
```

### キャンペーンを作成

キャンペーン用のRoleを作成

```bash
cat << EOF > ./trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "iotfleetwise.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
```

Roleの作成

```bash
CAMPAIGNE_ROLE=BikeCampaigneRole

aws iam create-role --role-name $CAMPAIGNE_ROLE \
--assume-role-policy-document file://trust-policy.json
```

Timestream DBに書き込む権限を持つPolicyを作成

```bash
TS_TABLE_ARN=`aws timestream-write describe-table --database-name BikeDatabase --table-name BikeTable --query "Table.Arn" --output text`

cat << EOF > ./permissions-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "timestreamIngestion",
      "Effect": "Allow",
      "Action": [
        "timestream:WriteRecords",
        "timestream:Select",
        "timestream:DescribeTable"
      ],
      "Resource": "$TS_TABLE_ARN"
    },
    {
      "Sid": "timestreamDescribeEndpoint",
      "Effect": "Allow",
      "Action": [
        "timestream:DescribeEndpoints"
      ],
      "Resource": "*"
    }
  ]
}
EOF

aws iam create-policy --policy-name ${CAMPAIGNE_ROLE}_TSPolicy \
--policy-document file://permissions-policy.json
```

PolicyをRoleにアタッチ

```bash
POLICY_ARN=`aws iam list-policies --query "Policies[?PolicyName=='${CAMPAIGNE_ROLE}_TSPolicy'].Arn" --output text`

aws iam attach-role-policy \
--policy-arn $POLICY_ARN \
--role-name $CAMPAIGNE_ROLE
```

キャンペーンに必要なARNを確認

```bash
TARGET_ARN=`aws iotfleetwise get-vehicle --vehicle-name $VEHICLE_NAME --query "arn" --output text`
ROLE_ARN=`aws iam get-role --role-name $CAMPAIGNE_ROLE --query "Role.Arn" --output text`
```

キャンペーンで収集するシグナルを定義

```bash
cat << EOF > ./campaigne-nodes.json
[
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.CoolantTemperature"
    },
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.EngineCoolantTemperature"
    },
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.OilTemperature"
    },
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.EngineSpeed"
    },
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.Speed"
    },
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.ThrottlePosition"
    },
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.FuelLevel"
    },
    {
        "maxSampleCount": 10,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.DemandEngineTorque"
    },
    {
        "maxSampleCount": 1,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.Odometer"
    },
    {
        "maxSampleCount": 1,
        "minimumSamplingIntervalMs": 1000,
        "name": "Vehicle.OBD.ControlModuleVoltage"
    }
]
EOF
```

キャンペーンを作成

```bash
aws iotfleetwise create-campaign \
--name bike-timebase-10sec \
--signal-catalog-arn $SIGNAL_CATALOG_ARN \
--target-arn $TARGET_ARN \
--collection-scheme "timeBasedCollectionScheme={periodMs=10000}" \
--spooling-mode TO_DISK \
--data-destination-configs "timestreamConfig={timestreamTableArn=$TS_TABLE_ARN,executionRoleArn=$ROLE_ARN}" \
--signals-to-collect file://campaigne-nodes.json
```

キャンペーンを有効化

```bash
aws iotfleetwise update-campaign \
--name bike-timebase-10sec \
--action APPROVE
```

キャンペーンを有効化すると、targetに登録されているFleetWise agentに設定情報が配信され、自動的に収集が開始されます。