コトハコビ

https://github.com/henohenon/hackz-allo-cup

GitHub

TypeScript

React

Electron

Node.js

コトバでつながるハコべるハコビたい

へのへのん

くらず / kurazu

推しアイデア

インターネットもペアリングも使わない、ブロードキャストで1文字ずつ流す独自文字コードのガラパゴス的秘密通信! とそれに乗る世界観を伝えるゲーム的表現!

作った背景

ローカルで特殊仕様な通信が、したい。 ワクワクする技術の魅せ方、したい。

推し技術

BLE通信の本来のBody領域が封じられてしまったため、ID部分の狭い領域を工夫してやりくりすることで通信を実現しています。 また、新たなるアーキテクチャに対して適切な設計を行なったほか、音と同期する絵作りを頑張りました。

プロジェクト詳細


TITLE


📦 概要 📦

  • インターネット接続を必要としない、すれちがいコミュニケーション
  • ヒトとヒトをコトバでつなげる
  • アソビ心は大切に

📦️ 使い方 📦️

送る

  1. 送るボタンをクリック
  2. 送りたい文章を入れよう
  3. 1文字ずつ丁寧に梱包して発送します
  4. 45秒以内、最大50文字は送れます

受け取る

  1. 受け取るボタンをクリック
  2. (Bluetooth権限の許可)
  3. 配達を待つ
  4. 戻るボタンで終了

デモ動画 (Youtube)

https://youtu.be/quu81XBAp0c

タイトル

image

送る

image

受け取る

image

荷物一覧

image


📦 技術 📦

技術スタック

アプリ

採用理由

Electronなぜ?

通常のBluetooth通信であれば、Webブラウザ上でも利用可能なAPIが存在するそうなので、例としてJoyconのようなHIDデバイスを接続して制御することは可能なようです。

しかし、今回使ってみたいBLE (Bluetooth Low Energy)はある程度低レベルのAPIでないとアクセスすることが出来ず、ネイティブアプリを作ることがほぼ必然となります。

ネイティブアプリをこの短期間で開発するのはコストが高いので、Web系で開発ができる + Node.jsでnoble/bleno等のBLE関連のライブラリを使用できることが判明したためElectronを採用しています。

PixiJSなぜ?

ElectronということでメジャーなのはReact + Node.jsだと思います。 しかし、当初の予定では3DS, Wiiのような低解像度感を出すためにThree.js, Phaser等のライブラリを使用しようと考えていましたが、リサーチ途中で見つかったWebGLレンダラーの1つであるPixiJSの公式サイトにて、公式のAgent Skillsの導入案内があったのが非常に好感で採用しました。

BLEなぜ?

後述します。

通信手段について

まず、Bluetoothにはモードが存在します。 コトハコビで使用しているのは「BLE (Bluetooth Low Energy)」と呼ばれるものです。

以下のような特徴を持ちます。

  • 低消費電力で通信が可能
  • Bluetoothのバージョンによって性能が大きく変化
  • 2つの通信方式を持つ

BLEは比較的新しめの規格であり、普及はかなり進んでいるものの、性質故にデータ通信の帯域幅の狭さが目立つため、メインの通信方法として採用される事例は多くはないと考えられます。

そして、今回はこのBLEのブロードキャストモードを使用しています。

ちなみに、BLEはバージョンによりますが数十メートルまで通信することも可能なようで、実際に2階のハッカソン会場から1階エントランスでもパケットの受け取りが可能なことは確認しました。


コネクション・ブロードキャストの違い

  • コネクション:双方向通信
  • ブロードキャスト:一方向通信

コネクションは所謂ペアリングのように、相手を認識した上で相互に情報伝達を行いますが、ブロードキャストでは一方的に電波を発信する、ラジオに近い動きをします。

Airtag等のビーコン的な動作をするものもこういった通信を採用していると思います。


ブロードキャストの中身

image

上記の画像のような、自身の識別情報をたくさん含んだパケットを発信しているのがブロードキャストです。

本来は、ペアリング前のBluetoothデバイスが、自分を見つけてもらうためのビーコン発信をするために使用されることが多く、基本的には任意のデータ通信を行うものではありません。

ですが、LocalName, Adv.service UUIDsと記載されている項目はある程度自由にデータを変更することが可能です。(今回の開発環境の場合、デバイス依存度合いが高すぎるため要検証)

この自由にデータを格納させられるService UUIDsの部分にコトハコビで発信する文章のうち、1文字を詰め込む構成にしています。

Bluetoothの通信規格上、ば


アプリデザイン

そう、Topazを見ているそこのあなた、ちょっと思ったでしょう?

懐かしいなぁって。

それを狙ってデザイン設計をしています。

1. ワイヤーフレーム"風"デザイン

"昔"あった、PlayStation向けゲームソフト「ビブリボン」やNHKのピタゴラスイッチに登場していた「フレーミー」などがモチーフになっています。

  • なんか懐かしい
  • ちょっとワクワクする
  • 気持ちが良い

色々なポジティブな気持ちが湧き出てくるUIを目指して作りました。

1. 細かいアソビ心

  • タイトルロゴのゆらゆら
  • タイトルで転がってるひらがな
  • ホバーで開くハコ
  • 荷物一覧の荷物についてるタグ

ちょっとしたこだわりが詰まったUIって良いですよね。

1. 音でもアソベる

アプリ起動中は常に一定のリズムを刻み続けています。リズムに合わせ、機械類はノリノリで動いてくれます。ヒトも機械もハコもハッピー。

メンバーひとこと

@henohenon

どうも、インフラエンジニアです。今回インフラないです。 nova

今回私は通信規格、データ永続化、CIの整備を行いました。

通信規格

type BleStatus = "IDLE" | "ADVERTISE" | "SCANNING" type BleResult = { ok: boolean, error?: string } // ステータスを排他更新する。遷移ごとに今やってる動作を止めてから移る。 // ADVERTISE にしても、最初の advertise() を呼ぶまで電波は何も撒かない。 setStatus(status: BleStatus): Promise<BleResult> // 撒く生データ(128bit UUID hex の配列)を差し替える。ADVERTISE 中だけ有効。 advertise(serviceUuids: string[]): Promise<BleResult> // HAKO パケットを拾ったら呼ばれる。生の serviceUuids をそのまま渡す。 // 戻り値の関数を呼ぶと購読解除。 onPacket(callback: (serviceUuids: string[]) => void): () => void

超シンプル・ジェネリック。ライブラリのライフサイクル管理と、BLE送受信のLocalNameの管理だけを行い、UUID以降の処理はレンダラに一任する。という内容ですね。

image image

このようなシンプルなMock画面とターミナルのログの形式で提供しました。シンプルで扱いやすく、なかなか良い形でデバック環境を提供できた気がします。

データ永続化

今回はIndexedDBに真剣に向き合い、最大限のパフォーマンスを持ってデータを扱いました。アプリ側が使用しているAPIは次です。

// created_at の新しい順に最大 limit 件。 getRecent(limit = 50): Promise<SessionRecord[]> // 文字が来るたび呼ぶ。初回は draft を作って created_at を打ち、以降は text を追記して育てる。 push(session_id: string, text: string): void // 全 draft を即確定フラッシュ(画面遷移用)。書き込み完了まで await できる。 flushAll(): Promise<void>

デバック用にClearや50件追加などの関数とMockUIを整備していたり実際はもう少し複雑ですが、複雑なQueryはないため、素のIndexedDBに関数を被せるだ気で十分なパフォーマンスと必要十分なAPI提供ができてるかと思います。

ただしこのアプリ、1文字ずつ文字が送られてくるため最悪100のセッションが2秒ごとに書き込みを要求してくるかもしれません。

ということでIndexDBのTransactionを活用し、値が最後に更新されてからコミット(flush)されるまでの時間を8秒、最後にコミットされてからのコミットされるまでの時間を30秒と定義し、負荷を軽減しました。

ビルド

折角のElectronということで配布を見据え、ビルドを試してみました。

手でビルドして共有もだるいし、Releaseで配る形のBuild CIをAIくんにお願いしました。まるでインフラエンジニアだ〜

Windowsでも動いて草

なんか気づいたらWindowsビルドも組んであって、いくつかバグフィックスしたら普通にWindowsビルドに成功していました。ウケますね。

image

Windowsビルドも試してみたところ、送信はダメでしたが受信だけは動くことが発覚しました。 ライブラリ自体を信用できてなかったので、見る側だけでも結構行幸だな。という感じです。

真面目にCI

ということでひとまず目的は達していたのですが、せっかくだし真面目にやってみたら?と相方(本業)から言われます。しょうがないにゃ〜

自分はActionsやCIは2回ぐらい真面目に触ったことがあるのですが、それ以外はコピペ・AI・GUIだけでやってきました。

image

久しぶりにやるかーということで色々AIくんに相談しつつ、pnpm/electronのキャッシュから引っ張ってくる、pythonのインストールを破棄するという偉業に成功しました。やったね! その後、ビルドだけではなくチェックのCIも実装!俺の勝ち!

動かん

動かんのだな〜そもそもビルドが思うように通らなかったり、Vite+とpnpm(とElectron)というなかなか新しい構成で、pnpmのバージョンをどこから引っ張ってくるのか論争などで四苦八苦してました。

image

ここずっとCIのコミットです。

それでも自分なりに最適な形に仕上げれたかと思います。docsとdiscordで仕様が分岐するということで、docsは全て破棄するという選択を取ったのですが、prのコメントなどに書き込むという脱法を行なっているので、興味がある方はそちらをご覧ください。

https://github.com/henohenon/hackz-allo-cup/pull/21 https://github.com/henohenon/hackz-allo-cup/pull/25 https://github.com/henohenon/hackz-allo-cup/pull/26

その他

技術の逆転というのも裏テーマとして開発を行いました。普段Unityなどフロントエンドをする自分、へのへのんはユーティリティ/CIなどバックエンド的なことを行っていました。

へのへのんさんの感想としては、手取り足取りだったけど成し遂げられてよかった。そこそこ楽しかったですが、進んでやりたくはない、全てのインフラエンジニアに感謝とのことです。

また今回初対面ということもあってか、共通認識が1日目の段階ではうまくできておらず、半日はそこのすり合わせと再構築をしていました。最初普通に気まずかったです。ありがとう。ごめん。


@kurazuuuuuu

普段はインフラ・バックエンドを触っていますが、今回は珍しくレンダラー側(フロントエンド)の見た目の部分をメインに触りました。

ユーティリティ側のバックエンド的なロジック部分はある程度設計を担当しましたが、いつもと違う技術スタックで工程の組み立てミス、リサーチ不足等まだまだ力不足だなと感じる場面が多かったです。

最終的には動く形かつアプリとしてまとまりがある状態になったので、色々な学びがありつつ、アプリがちゃんと完成してよかったなと感じています。

今後Electronを採用して開発する機会があれば、完全なマルチプラットフォーム開発をしてみたいなと感じました。


詰まったポイント

BLEパケット欠落問題

環境依存度合いが大きすぎることが原因でした。

BLEのブロードキャストは通常のBluetooth接続とは違い、本来はアプリケーションではなくハードウェアそのものが制御する領域であり、イヤホン・スマートウォッチ等のペアリングを行うビーコンとして使われることが多いはずです。

よって、BLEのアドバタイズ等の要求を出したとしても、それをどういう順番で動かすのかを決めるのはOS、ドライバの役割であり、アプリケーションソフトウェア側ではこれ以上抵抗することはできそうになかったです。

また、ライブラリの対応状況も良くはなく、今回使用したライブラリもオリジナルからフォークされたものでした。ライブラリ自体も整備は進んでいないと考えられ、どのような環境・デバイスであれば想定している機能を正常に利用できるかどうかの検証までは時間を割くことができませんでした。

macOSでは上手く動くけど、Windowsでは動かないみたいな、やはりマルチプラットフォーム開発は難しいんだなと感じました。

へのへのん

@henohenon