※この記事は人間の手によって作成されています。
導入
その昔、OSI参照モデルというものを習いました。レイヤーを分離することで、その上に自由にプロトコルを乗っけたりあるいは中間レイヤを差し替えられるということです。なかなかに面白い。。。俺俺プロトコルを作りたいね、、、となりました。
どうせやるなら物理層!それも通信を感じたいから音でやろうとなりました。
思い浮かんだのは歌って電話を呼び出すコナンの映画。二つの音を組み合わせることで16種類の符号を定義しているDTMFです。
方針
プロトコルを定義して使うOSに差し込む必要がありそうです。初めはFPGAとかRasberry Piとかそう言ったハードウェアがあって、デバイスドライバを書いて、、、とかしないといけないと思っていました。
実現可能性を含めてチャッピーさんに聞いてみたところ、Linuxの場合、TAPと呼ばれる機能があり、仮想的なNICを自由に生やせるみたいです。ありがてぇ。。。
実現可能性がうんと高まったところで以下の方に方針を立ててPoCを行います。
前提
- マイクとスピーカーのお家の在庫からLinuxとMacBookマシン間で通信する
- Linuxは物理レイヤーを実装し、TAPでOSに差し込む
- MacBookはTAPが使えないため、OSに差し込まずシミュレーションとして実装する
方針
- 音を使ってバイナリデータを一方方向に送信可能か?
- 双方向にバイナリ、テキストデータを送信可能か?
- TAPを使用してパケットを送信可能か?
音を使ってバイナリデータを一方方向に送信可能か?
DTMFは高い音と低い音の二種類の組み合わせで符号が決まります。
| 高群(Hz) | |||||
|---|---|---|---|---|---|
| 1209 | 1336 | 1477 | 1633 | ||
| 低群 (Hz) |
697 | 1 | 2 | 3 | A |
| 770 | 4 | 5 | 6 | B | |
| 852 | 7 | 8 | 9 | C | |
| 941 | * | 0 | # | D | |
以下のファイルで実装をしました。
デコードは以下のファイルです。
チャッピーが大体書いてくれました。
キーボードに入力すると即時音が出ます。デコーダでその音が意味する符号を判定してくれます。楽しいw
この辺りで認識し、対処した課題は以下です。
- 時系列的な音を意味ある符号として認識方法
- 境界判定
- 無音区間の閾値設定(機器のボリュームや位置にも依存)
- 周波数判定
- 意味のない周波数だった場合切り捨てる
- 境界判定
- スピーカーからの不意な大きなノイズ
- 音量を下げる、窓関数で立ち上がりなどをなめらかにするなどしてみたが、最後まで行ってもたまに暴発
双方向にバイナリ、テキストデータを送信可能か?
この辺りからの実装はapp配下にあります。全体のアーキテクチャやインターフェースの設計は自分で考えました。
- util.py
- DTMFの設定などを記載
- encode.py
- 情報のエンコードと送信
- decode.py
- 情報の受信とデコード
- dtmf_nic.py
- 指定した通信内容(Binary,Text,TAPなど)で仮想NICを作成する
- main.py
- 通信内容を選択する
まずはDEBUG_IOを用いて、バイナリデータを送信できるかどうかを確認します。
この時は双方向を前提にしています。つまり、全二重か半二重かという話になりますが、マスタースレイブを決めることなどもかなり面倒なので、半二重にします。ということなので、端末が二つしかない環境でも衝突回避が必要です。
今回は簡単のため、パケットの連続送信する場合は間隔を空けるようにしました。こうすると、交互に送信が可能となります。双方が同時に送信を始めた場合は諦めるしかありません...w
そんな感じの方針を決めてでチャッピーにお願いをし、機能を書いてもらいました。
そのあたりのコードは以下をご覧下さい
せっかくなのでテキストもASCIIで送信したいと思って作成したのが、DEBUG_TEXT_IOクラスです。
この辺りで認識し、対処した課題は以下です。
- 衝突検知が正しく動くか?
TAPを使用してパケットを送信可能か?
いよいよ通信の実験を行う段階にきました。
今回はテスト用に192.168.111.0/24というネットワークを作成し、その上で通信するものとします。Linuxが192.168.111.15、MACBookが192.168.111.13という設定にします。
TAP操作の実装
TAPデバイスは、Ethernetのレイヤーのインターフェースとして公開されており、Ethernetパケットの形式で書き込み、あるいは読み込みができます。
以下のようにしてLinuxに生やします。
sudo ip tuntap add dev mynic mode tap user $USER
sudo ip link set mynic up
sudo ip addr add 192.168.111.15/24 dev mynic
これにより、このLinuxマシンに、192.168.111.15というIPアドレスを持ち、192.168.111.0/24に属するmynicという名前のネットワークインターフェースを生やすことができました。
Pythonからは以下のように操作できます。
つまり、毎回のwrite、readが1パケットに対応するわけです。
シミュレーション端末の実装
MACBookはシミュレーションです。と言っても、全ての音での通信に対して正しいレスポンスを定義することは厳しいため、今回LinuxからMACBookへpingが通れば良いという設定にします。
pingは、指定したIPをARPキャッシュに持っていない場合、ARPリクエストを送信し、その後でICMP requestを返すことになっています。従って、以下の実装をします。
- ARP requestに対して正しく返す
- ICMP requestに対して正しく返す
これらを実装したものが以下です。
実験
以上で用意が整いました。続いて実験です。
実験を始めると、思わぬトラブルが発生します。以下に例を挙げます
- pingを何も考えないで実行すると返答が遅すぎてタイムアウトになる
- 送信オプションでタイムアウトを長くする
- ARPキャッシュがせっかく更新されてもキレてしまう
- ARPキャッシュのタイムアウトを長くする
- 通信の効率が悪い(遅い、間違える)
- 間違えないギリギリの通信レートのチューニング
- 符号間隔や休止時間
- 情報密度を上げるためにサンプリング周波数を上げる
- 機材の配置のチューニング
- マイクとスピーカーの位置
- 誤り訂正などの検討
- 本場のEthernetのように最後にFCSを入れることを検討したが、入れると通信効率は下がるのでサボっています。
- 間違えないギリギリの通信レートのチューニング
- pingの回数を制限しても通信が終わらない
- 何が起こっているかを解析するためにデバッグログやARPキャッシュ監視、WireSharkの用意
- おそらくChrome由来と思われるパケットがちょっかいをかけている
- ARPが解決する前に大量の通信がTAP上にスタックされるため、解決後にICMP Requestをなかなか飛ばせない(Wiresharkでは送信済みになっているが、NICからは出ていない)
- LinuxではデフォルトでARPキャッシュがない場合、ARPを送信する場合3回送信することになっているので、pingの送信間隔を十分に空ける
以上のようなトラブルを四苦八苦しながら取り組んでなんとか成功させたのが以下の動画です。
この通信を見るとなかなか面白いです。ARPキャッシュ更新やWireSharkでの表示、音の発生のタイミングから、低レイヤーの中での処理の分担を推しはかることができます。OSの素の部分をみている感じがして嬉しい気持ちになることができます。
最後に
実は、今回の実装の前に、仮想環境の中でネットワークを構築してpingをWiresharkでみたりLinuxカーネルのコードを見るなどして理解を深めてきました。
おそらくそう言った、「リアル」な体験がないと、概念上うまく行っても適切なデバッグを行い成功させることは難しかったかと思います。
また、そう言った経験があったとしても、TAPの存在など知らない概念を教えてくれたりコード実装をちゃっちゃとやってくれるLLMがなくても難しかったと思います。
経験とLLMがあったおかげか、1日もかけないで検証ができたのは自分にとって驚きでもありました。
いい時代になりましたね〜。