AI・開発、電子工作、クルマ。実際にやってみたことを残すブログ

ランクル FJ のナビ画面に車内の CO2 を表示できるアプリを作ってみた

M5Stack の ATOMS3 Lite と SCD41 で車内の CO2 濃度を測り、Bluetooth で車載の Android ボックスのアプリに送って、ランドクルーザー FJ のナビ画面に出すようにしました。マイコンとセンサーで約 8,200 円。SwitchBot の CO2 センサーと比べて精度も確かめました。

ランクル FJ のナビ画面に映した自作アプリ。左に大きく 935ppm と「GOOD」、下がる速さ「↓712 ppm/分」、その下に CO2 の推移の線とその日の最高・平均、12 時間の降水予報。右に時刻 15:11、室温 26.3℃・湿度 61%・露点 18.2℃・外気 23.3℃
ナビ画面に映した自作のアプリ。画面への撮影者の映り込みは画像処理で消しています
目次
  1. 作ったきっかけ
  2. 使ったもの
  3. しくみ
  4. 部品が届く前にソフトを作った
  5. 精度を確かめた
  6. SwitchBot と並べて一晩
  7. 2 台とも屋外に出す
  8. ケースに入れた影響
  9. ナビ画面のアプリ
  10. 車で使ってみて

ランドクルーザー FJ の車内の CO2 濃度を測って、ナビ画面に出すようにしました。M5Stack の ATOMS3 Lite と CO2 センサーの SCD41 で測り、BLE で車載の Android ボックス Ottocast P3 Pro に送ります。BLE は Bluetooth Low Energy の略で、少ない電力で小さなデータをやり取りするための Bluetooth の規格です。Ottocast に入れた自作のアプリが、CO2・温度・湿度をナビ画面に表示します。

部品はマイコンとセンサーで約 8,200 円、ケースは 3D プリンタで作りました。ファームウェアとアプリは Claude Code と一緒に作っています。今は車に載せて使っていて、換気の目安として役に立っています。

作ったきっかけ

運転中の眠気やだるさが気になっていて、換気のタイミングを数値で知りたいと思ったのがきっかけです。CO2 を測る既製品もありますが、既製品ではなくナビ画面に出したかったのと、電子工作と AI を使った開発を試してみたかったので、自分で作ることにしました。

使ったもの

黒い革の上に並べた部品。左に丸い Ottocast の Android ボックス、中央の上に白い ATOMS3 Lite、その下に穴の並んだ SCD41 ユニット、右に 3D プリンタで作った黒いケースの本体とふた
使ったもの。左から Ottocast P3 Pro、ATOMS3 Lite と SCD41 ユニット、自作のケース
部品 役割 値段
M5Stack ATOMS3 Lite マイコン(ESP32-S3)。測った値を BLE で送る 約 1,200 円
M5Stack SCD41 ユニット CO2・温度・湿度のセンサー 約 7,000 円
自作のケース 3D プリンタで作った —
Ottocast P3 Pro 車載の Android ボックス。アプリを入れてナビ画面に表示する —

ATOMS3 Lite と SCD41 ユニットは、どちらも AliExpress で買いました。

SCD41 ユニットは Grove ケーブルでつなぐだけです。

ATOMS3 Lite の裏側と SCD41 ユニットを、赤・黄・白・黒の線の Grove ケーブルでつないだところ
Grove ケーブル 1 本でつなぐだけ

ケースは 3D プリンタで作りました。上面に通気口を開けています。

車では、センターコンソールの USB の差し込み口から給電して置いています。

ランクル FJ のセンターコンソール。USB の差し込み口から編み込みのケーブルが出て、黒いケースに入れたセンサーが AUDIO のつまみの下に置いてある。右下に LAND CRUISER の刻印
車内に置いたセンサー。USB から給電している

しくみ

構成図。車内で、SCD41 ユニットが Grove(I2C)で ATOMS3 Lite につながり、ATOMS3 Lite が BLE で約 5 秒ごとに Ottocast P3 Pro に値を送る。ATOMS3 Lite は本体の LED を緑・黄・赤で点け、Ottocast のアプリは FJ のナビ画面に表示する。アプリは車の外の Open-Meteo から外気温と天気を取る
センサーからナビ画面までの流れ

ATOMS3 Lite は FJ-CO2 という名前で、周りに自分の存在を知らせる電波(BLE のアドバタイズ)を出しています。アプリがこれを見つけてつなぐと、SCD41 の測定周期に合わせて約 5 秒ごとに値を送ります。送る中身は 6 バイトだけで、CO2(ppm)、温度(×100)、湿度(×100)を並べています。

// 測定値を6バイト(リトルエンディアン)に詰めてNotifyする
static void publishMeasurement(uint16_t co2, float temperature, float humidity) {
int16_t temp100 = (int16_t)lroundf(temperature * 100.0f);
uint16_t hum100 = (uint16_t)lroundf(humidity * 100.0f);
uint8_t payload[6];
payload[0] = co2 & 0xFF;
payload[1] = (co2 >> 8) & 0xFF;
payload[2] = temp100 & 0xFF;
payload[3] = (temp100 >> 8) & 0xFF;
payload[4] = hum100 & 0xFF;
payload[5] = (hum100 >> 8) & 0xFF;
measurementChar->setValue(payload, sizeof(payload));
if (deviceConnected) {
measurementChar->notify();
}
}

CO2 のレベルは 3 段階に分けて、本体の LED とアプリで同じしきい値を使っています。

  • 999ppm まで: GOOD(緑)
  • 1000〜1499ppm: CAUTION(黄)
  • 1500ppm から: VENTILATE(赤。換気したほうがいい)

ファームウェアは PlatformIO と Arduino、アプリは Kotlin と Jetpack Compose で作りました。

部品が届く前にソフトを作った

部品が届く前に、BLE で送るデータの形を先に決めて、ファームウェアとアプリを作り、ビルドが通るところまで済ませました。センサーが無くても BLE を試せるように、400〜2000ppm を行ったり来たりする擬似の値を送る「ダミーセンサーモード」もファームウェアに用意しました。

届いてから Grove でつないで書き込むと、ピンの設定を変えないまま、そのまま動きました。最初に出た値は CO2 901ppm、27.15℃、63.34%RH です。

精度を確かめた

精度を確かめるため、手持ちの SwitchBot の CO2 センサー(SwitchBot Meter Pro CO2)と比べました。

SwitchBot と並べて一晩

ファームウェアに、Home Assistant へ 30 秒ごとに値を送るモードを足して、SwitchBot と履歴を比べました。8.5 時間・323 組の値で、CO2 の差は -68.9ppm(標準偏差 6.73)、相関は 0.996 でした。濃度が 300ppm 動く間も、差はほぼ一定です。動き方はほぼ同じで、値が一定の幅だけ低く出ている、という結果でした。

2 台とも屋外に出す

2 台とも屋外に出して比べました。落ち着いたあとは、自作のほうが 407.8ppm、SwitchBot が 409.3ppm で、差は 1.5ppm。実質一致です。SCD4x の規格(±(40ppm+5%))の範囲に入っていたので、強制的な再校正はしないことにしました。

ここでひとつ失敗しかけました。外に出した直後、ケースが冷えていく途中で値が一時的に 385ppm まで下がり、それを「落ち着いた」と見誤って、「自作のほうが 45ppm 低い」と結論を出しかけました。10 分ほど待つと、本当に落ち着いた値は一致していました。温度も落ち着いたかを見ないと判定を誤る、という教訓です。

車でも同じで、炎天下の車に乗り込んだ直後やエアコンを入れた直後は数十 ppm ずれることがあります。5〜10 分は落ち着くのを待つのがよさそうです。

なお、屋内(550〜920ppm)では自作のほうが SwitchBot より 60〜70ppm 低く出ます。屋内の範囲には基準になるものが無いので、どちらが正しいかは分かっていません。

ケースに入れた影響

ケースに入れた状態でも、703 組の値で比べました。CO2 への影響は 10ppm 未満で、急に上がるときはむしろ自作のほうが先に反応していました。温度は無風の室内で約 1.5℃ 高く出ましたが、屋外(風あり)では差が消えたので、空気が動く走行中を考えて、温度の補正は入れていません。

ナビ画面のアプリ

アプリは横向き固定・フルスクリーンで、画面がスリープしないようにしています。寸法はすべて画面の短辺からの比率で決めていて、解像度が変わっても崩れません。

デザインは自動車の計器をイメージしました。暗いチャコールの背景に、彩度を落とした 3 色(セージ・オーカー・煉瓦)と等幅の数字を使っています。最初の版は緑・黄・赤のべた塗りの背景で、チープに見えたのでやめました。

実際にナビ画面に映すと、こんな感じです。

ランクル FJ のナビ画面に映した自作アプリ。左に大きく 935ppm と「GOOD」、下がる速さ「↓712 ppm/分」、その下に CO2 の推移の線とその日の最高・平均、12 時間の降水予報。右に時刻 15:11、室温 26.3℃・湿度 61%・露点 18.2℃・外気 23.3℃
ナビ画面に映した自作のアプリ。画面への撮影者の映り込みは画像処理で消しています

画面の作りは下のとおりです。この画面は、このプロジェクトを一緒に進めた Claude Code に生成してもらったもので、値は例です。

表示しているのは次の項目です。中央に大きく出す項目は、CO2・時計・室温・湿度・外気温から選べます。

  • CO2(メイン)、変化の速さと、しきい値に届くまでの予測
  • 時計、CO2 のトレンドの線
  • 室温・湿度・露点、外気温・天気、12 時間の降水予報(天気は API キーの要らない Open-Meteo から)
  • 窓の曇りの警告(外気温が車内の露点 + 1℃ 以下になったら出す)
  • その日の最高値・平均

CO2 の推移を大きく出す画面もあります。

CO2 の推移を大きく表示した画面。1000ppm と 1500ppm に点線があり、CO2 が 1500ppm を超えるまで上がってから急に下がる線
CO2 の推移を大きく出す画面(生成してもらったもの。値は例)

車で使ってみて

今は車に載せて使っています。内気循環で走っていると CO2 がすぐに上がり、窓を開けたり外気導入にしたりすると下がっていくのが見えます。換気のタイミングの目安として役立っていて、CO2 の値と眠気の関係も体感で感じるようになりました。