Edited at

今年も4輪耐久レースをエンジニア的に更に改善した話。


昨年のお話はこちらから

Firebase Cloud Firestoreを使って、車の草耐久レースを少し改善した話。


4輪耐久レースってなに??

F1やSUPER GT選手権やスーパー耐久などが有名所のレースで知っている人も多いと思いますが、車(4輪)のレースには大きく分けて二種類があります。


  • 1周のLapTimeを競う→ スプリントレース

  • 規定時間内に周回数をどれだけ重ねることができるか競う→耐久レース

今回、参加したのは後者の耐久レースで、朝の9時から夜の9時までの12時間で、約2kmのコースを何周走れるかを競うモータースポーツのタイプです。


昨年の反省点


レーシンググローブをつけたままiPadの操作を行えない

スマホ対応の冬用手袋などは出ていますが、スマホ対応のレーシンググローブなんて代物は世の中に存在しないです。

何より運転中にiPhone、iPadを操作することが公道上ではNGなのでそもそも製品として作るニーズがないですよね...

昨年の運用では、レーシンググローブにアルミテープを巻きつけて画面タップできるようにしていました。

しかし、夏場のエアコンもない耐久カーの中で1時間半もグローブを着けていると汗まみれになりアルミテープもボロボロになってしまい担当シフトの後半では操作できなくなってしまうシーンも見られました。


今年の改善策


Raspberry Piを活用し物理ボタン操作を可能に



昨年の反省点を踏まえ、今年はiPadの操作をせずにピットに対してメッセージを送信できるようにRaspberry Piを用いて従来の画面上に配置していたボタンを物理ボタンに置き換える取り組みを行いました。

ピットとドライバーとの連絡には、Firebase Firestoreを利用しています。なので、アプリ上のボタンを操作した時と同様の書き込み値をRaspberry Piに接続した物理ボタン操作時に書き込むことで昨年までアプリだけで実現していたことに新しいハードを追加することが出来ました。

初年度は、UserDefaultの値をiCloud上で共有してピットとドライバーとの連絡を実現していたことを考えると昨年Firebase Firestoreを利用する方針に切り替えたおかげで、簡単にIoT機器の導入も出来たことが救いでした。


実装する


Raspberry PiからFirebase Firestoreにアクセスする

今回は、Node.jsで実装しています。

まず、ざっくり公式のドキュメントをもとにFirestoreの操作する部分を書きます。

Swiftなどの一部の言語のSDKでは、指定したコレクション、ドキュメントが更新されるとイベントが発火し、最新のドキュメントを取得するようなリアルタイムリスナーもありますが、Node.jsでは使えません。

そのため、シンプルに初期設定とデータセット、データゲットのみとなっています。

firestoreへの書き込みの流れとしては、下記の様になります。


  1. ボタンが押される

  2. firestoreからデータを取得する

  3. 取得データの「Pitへのメッセージ」を収めるKeyにValueを登録する

  4. 登録したデータをfirestoreに書き込む

  5. データが書き込まれ、アプリ側のリアルタイムリスナーでデータが書き換わったことを検知し、アプリ側の表示を切り替える

非常にシンプルなことしかしていません。


firestore.js

const admin = require('firebase-admin');

const serviceAccount = require('【発行されたjsonのパス】');

const collectionName = "Firestoreのアクセスするコレクション名";
const documentName = "Firestoreのアクセスするドキュメント名";

let db;

exports.firebaseInit = async function () {
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "データベースURL"
});
db = admin.firestore();
}

exports.set = async function (data) {
let reference = db.collection(collectionName).doc(documentName);
reference.set(data);
}

exports.get = async function () {
let reference = db.collection(collectionName).doc(documentName);
let result = async function () {
return new Promise(function (resolve, reject) {
reference.get()
.then(doc => {
if (!doc.exists) {
reject(nil);
}else{
resolve(doc.data());
}
})
.catch(err => {
reject(err);
});
})
}
let res = await result();
return res;
}



Node.jsでGPIOを監視する

回路的には、シンプルなプルダウン回路なので説明は省略します。

GPIOの検知には今回「onoff」モジュールを利用しました。

onoff - npm

コードも本当にGPIOのピンを監視し、Value値が1(ONの状態)になった時にfirestoreへの書き込みを行うだけのシンプルなものです。


index.js

const fireStore = require('./fireStore');

const gpio = require('onoff').Gpio;

//Firebase Firestoreの初期化
fireStore.firebaseInit();

const gpio4 = new gpio(4, 'in', 'both');
const gpio17 = new gpio(17, 'in', 'both');
const gpio27 = new gpio(27, 'in', 'both');
const gpio22 = new gpio(22, 'in', 'both');
const gpio10 = new gpio(10, 'in', 'both');
const gpio9 = new gpio(9, 'in', 'both');
const gpio11 = new gpio(11, 'in', 'both');

gpio4.watch(async function (err, value) {
if (err) {
throw err;
}
if (value == 1) {
console.log("GPIO4");
await updateButtonNo(1);
}
});

gpio17.watch(async function (err, value) {
if (err) {
throw err;
}
if (value == 1) {
console.log("GPIO17");
await updateButtonNo(2);
}
});

gpio27.watch(async function (err, value) {
if (err) {
throw err;
}
if (value == 1) {
console.log("GPIO27");
await updateButtonNo(3);
}
});

gpio22.watch(async function (err, value) {
if (err) {
throw err;
}
if (value == 1) {
console.log("GPIO22");
await updateButtonNo(4);
}
});

gpio10.watch(async function (err, value) {
if (err) {
throw err;
}
if (value == 1) {
console.log("GPIO10");
await updateButtonNo(5);
}
});

gpio9.watch(async function (err, value) {
if (err) {
throw err;
}
if (value == 1) {
console.log("GPIO9");
await updateButtonNo(6);
}
});

gpio11.watch(async function (err, value) {
if (err) {
throw err;
}
if (value == 1) {
console.log("GPIO11");
await updateButtonNo(7);
}
});

process.on('SIGINT', function () {
console.log('Finished process');
gpio4.unexport();
gpio17.unexport();
gpio27.unexport();
gpio22.unexport();
gpio10.unexport();
gpio9.unexport();
gpio11.unexport();
});

async function updateButtonNo(no) {
let data = await fireStore.get();

//ここの間で、firestoreから取得した最新のデータをゴニョゴニョする

await fireStore.set(data);
}



プログラムを起動時に自動実行し、永続化するようにする

こちらの記事を参考にさせていただきました。

【Node.js】 RaspberryPiのプログラムを自動起動・永続化・SSH ログアウト後もプロセスを残す

SSHでログインしてnodeを実行してもログアウトした瞬間、止まってしまいます。なので、Alexaを始めNode.jsでサービスを作る際によく利用しているデーモン化ツールの「forever」を利用します。


運用する

今年も2台体制で、レースに参加しました。なので、2ペアの機材を運用しています。

昨年までは、CellularモデルのiPadにSIMを刺して運用していましたが本年はRaspberry PiをWifiに接続する必要があったため別途モバイルルーターを車載しiPadとRaspberry Piを接続して運用に挑みました。





他のチームの運用状況ですが、このようにピットボード + 無線という運用方法が一般的のようでした。


運用結果

結果として、一言で言うと「失敗」に終わっています。

敗因はコレ。



酷暑の影響で、日中の気温が40度近くまで行ってしまいました。レースカーはエアコンなどついていませんので、日中の車内温度はエンジンの熱なども加わり50度近くになっていたと想定できます。

そんな車内温度の影響もありiPadがフリーズ、熱暴走してしまいまともに利用出来なかったという結果に終わりました。


物理ボタンの運用結果

しかし、物理ボタン単体での運用結果は「成功」しました。

温度環境は、同じでしたが物理ボタン側に利用したRaspberry Piは熱暴走せず12時間連続稼働することが出来たため、途中から無線と物理ボタンの組み合わせで運用する方針に切り替え最後まで凌ぐことが出来ました。

ボタンを設置したベースプレートが薄かったため、勢いよくボタン操作をするとベースプレートが反ってしまい操作が出来ないことも発生しましたが走行中にiPadの画面操作を行うよりも確実に操作を行ったフィードバックをドライバーに戻しつつ、確実にPit側にメッセージを送付することが出来たため本項目は成功したと言い切っても良いと感じる結果となりました。


本年の運用から

本年からアプリの他に物理デバイスまで手を出しました。Arduinoは学生時代に触っていたこともありRasberry Piも比較的簡単に扱うことができ、想定したシステムを組むことが出来たと感じています。(コードのレベルは別として)

ただ、ハードやソフト面以外による外的要因に邪魔されていることもあり来年以降は温度などの面まで考慮しなければ戦いの場で使えるツールとして昇華させることは難しいと感じることが出来たのが今年の収穫でした。


来年に向けて

温度環境など作動させる環境まで考慮し、確実に稼働するシステムにすること。

今年からレースカーのモデルが新しくなり車速や水温などをECUから取得できるようになっているので来年はよりピット側で詳細なデータを取得できるようにしてみたいなとか考えてたりします。

拙い文章ではありますが、最後までお読みいただきありがとうございました。