これは筆者が2020年より前の学生時代に取り組んだ学習記録を発掘して記事化したものです。
採用している技術はとても古いですが、システムのレイヤーを学ぶ上で良い題材だったと感じたため共有したい次第です。
Summary
- Raspberry Pi(以下、RPi)を使い、LED明滅による可視光通信システムを自作することで、OSI参照モデルの全7層が担う役割を体で理解した
- 送信側:文字列 → RSA暗号化(128bit鍵) → 2進数化 → 変調 → LED明滅、受信側:フォトダイオード受光 → 復調 → 10進数化 → 復号 → 文字列復元、という一連のパイプラインを全て手実装した
- 生成AIが実験の繰り返しをコントロールする時代が近づきつつある今、手を動かして学ぶハードルは逆に下がっている。自分の守備範囲を超えた領域に踏み出しやすい時代でもある
やらないこと
- 具体的な実装の共有(もう、ソースコードが無い)
- OSI参照モデル7層の教科書的な定義の説明
- TCP/IPプロトコルスタックとOSIモデルの詳細な対応関係の解説
- 可視光通信の産業応用や標準規格の解説
- 本システムの実用的な通信速度・距離性能の評価(あくまで学習目的のプロトタイプである)
本記事における課題
学生時代、通信プロトコルをOSI参照モデルで学んだとき、「第1層:物理層」「第7層:アプリケーション層」といった言葉は頭に入っても、それぞれの層が具体的に何をしていて、隣の層と何を境界にして責務を分けているのかが、どうにも腑に落ちなかった。
教科書のパケット図を眺めていても実感は湧かない。「IPヘッダがどこに付く」「MACアドレスはどの層か」——問題は解けるが、本質的な理解には程遠いと感じていた。
そこで考えたのが、「既存のネットワーク機器を一切使わず、光でビットを飛ばす通信システムを自分で組めば、全レイヤーを自分の手で実装することになるはずだ」 というアプローチである。電磁波(電波)を使わない無線通信として可視光を選び、RPiとLED・フォトダイオードという最小構成でシステムを構築することにした。
やったこと
システム全体構成
RPi 1台を送受信機として動作させ、以下のパイプラインで文字列の無線伝送を実現した。
【送信側】
送信したい文字列
└─ [L7: アプリケーション層] ユーザー入力・文字列の生成
└─ [L6: プレゼンテーション層] RSA暗号化(128bit鍵)
└─ [L5: セッション層] 送受信タイミングの同期制御
└─ [L4: トランスポート層] ビット列のフレーム分割・順序管理
└─ [L3: ネットワーク層] 単一ノード間のためスキップ
└─ [L2: データリンク層] 2進数化・変調(OOK: On-Off Keying)
└─ [L1: 物理層] LED明滅による光信号の送出
【受信側】
フォトダイオード受光
└─ [L1: 物理層] 光強度の電圧変換・閾値判定
└─ [L2: データリンク層] 復調・ビット列復元
└─ [L3: ネットワーク層] 単一ノード間のためスキップ
└─ [L4: トランスポート層] フレーム結合・順序復元
└─ [L5: セッション層] セッション終了判定
└─ [L6: プレゼンテーション層] RSA復号(秘密鍵で復元)
└─ [L7: アプリケーション層] 文字列の表示・出力
以降、各層の実装内容を順に説明する。
L1(物理層):光でビットを運ぶ
最初に取り組んだのは物理層、すなわち「どうやってビットを光に変換するか」である。
RPiのGPIOからLEDを点滅させ、点灯を1・消灯を0として1ビットを表現するOn-Off Keying(OOK)方式を採用した。
以下のコードは動作イメージを示すための生成AIによるサンプルであり、実際の動作確認は行っていない。
import RPi.GPIO as GPIO
import time
LED_PIN = 18
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
def send_bit(bit, duration=0.01):
GPIO.output(LED_PIN, GPIO.HIGH if bit == '1' else GPIO.LOW)
time.sleep(duration)
import RPi.GPIO as GPIO
import time
PHOTO_PIN = 24
THRESHOLD = 0.5 # 0.0〜1.0の正規化された閾値(環境に応じて調整)
GPIO.setmode(GPIO.BCM)
GPIO.setup(PHOTO_PIN, GPIO.IN)
def read_light_level():
"""
フォトダイオードの出力を読み取る。
デジタル入力の場合はGPIO.input()で0/1を取得。
アナログ入力が必要な場合はADC(MCP3008等)経由で正規化値を取得する。
"""
return GPIO.input(PHOTO_PIN) # デジタルの場合: 0 or 1
def receive_bit(duration=0.01):
"""
閾値判定を明示的に行い、1ビットを返す。
外光ノイズ等による境界値の不安定さに対処するため、
THRESHOLD以上を '1'、未満を '0' と判定する。
"""
value = read_light_level()
time.sleep(duration)
return '1' if value >= THRESHOLD else '0'
def receive_bits(num_bits, duration=0.01):
bits = ''
for _ in range(num_bits):
bits += receive_bit(duration)
return bits
受光素子の選定で大きな壁にぶつかった。最初はCdSセル(硫化カドミウム光導電素子)を使っていたが、応答速度が遅すぎてビットが化ける問題が頻発した。CdSセルの応答時間はmsオーダーであり、高速な明滅に追従できないのだ。
フォトダイオードに換装することで明滅周期を元の5分の1に短縮できた。これは教科書には書かれていない、素子の物性がプロトコル設計の制約になるという物理層の本質を身をもって学んだ瞬間だった。
L2(データリンク層):ビット列への変換と同期
物理層が「ビットを光に変換する」層であるとすれば、データリンク層は「意味のあるビット列のまとまり(フレーム)を定義し、正しく届けることを保証する」層である。本システムでは文字列をビット列に変換する処理と、送受信の同期制御の一部をここに位置付けた。
以下のコードは動作イメージを示すための生成AIによるサンプルであり、実際の動作確認は行っていない。
def str_to_bits(text):
bits = ''
for c in text:
bits += format(ord(c), '08b')
return bits
def bits_to_str(bits):
chars = []
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
chars.append(chr(int(byte, 2)))
return ''.join(chars)
送信側と受信側でサンプリングタイミングがずれると、ビットの境界を誤認識し全データが化ける。そのためスタートビット/ストップビットを自前で定義してフレームの境界を明示する実装を行った。「データリンク層はフレームの境界を管理する」という教科書の記述が、具体的なコードの問題として降りてきた瞬間である。
L3(ネットワーク層):実装をシンプルにするためスキップした層
ネットワーク層は複数のネットワークをまたぐ経路制御(ルーティング)を担う層である。IPアドレスの付与やルーティングテーブルの参照がその典型だ。
本システムは実装をシンプルにするためRPi 1台・LED 1本・フォトダイオード 1つという最小構成を選んだ。送信元と宛先が常に固定の1対1であるため、ルーティングを考慮する必要がなく、この層の実装はスキップした。
逆に言えば、複数ノード間の通信を実現しようとした瞬間に初めてL3の必要性が具体的な問題として浮上する。その意味で「L3が担う責務とは何か」を理解する上で、スキップという選択は良い対比になった。
L4(トランスポート層):フレームの分割と順序の保証
トランスポート層は、データを適切なサイズのセグメントに分割して送出し、受信側で正しい順序に組み直すことを担う。TCPにおけるシーケンス番号の管理がその典型例である。
本システムでは暗号化後のビット列が長くなるケースに対応するため、一定長のブロックに分割して送出し、受信側でブロック番号に従って結合する処理を実装した。
以下のコードは動作イメージを示すための生成AIによるサンプルであり、実際の動作確認は行っていない。
BLOCK_SIZE = 8 # bits
def split_into_blocks(bits, block_size=BLOCK_SIZE):
return [bits[i:i+block_size] for i in range(0, len(bits), block_size)]
def reassemble_blocks(blocks):
return ''.join(blocks)
L2でフレームの「境界」を管理し、L4でフレームの「順序と完全性」を管理する——この責務の分離を実装しながら初めて実感できた。
L5(セッション層):通信の「始まり」と「終わり」を管理する
セッション層は、通信の開始・維持・終了という一連のやり取りを管理する層である。「いつ通信を始めるか」「いつ終わったと判断するか」の取り決めがここに相当する。
本システムでは、送信開始前にLEDを一定パターンで点滅させるハンドシェイク信号を実装し、受信側がそれを検出することで「これから通信が始まる」と認識できるようにした。また、全ビット送出後に終端パターンを送ることで「通信が完了した」ことを受信側に伝えた。
以下のコードは動作イメージを示すための生成AIによるサンプルであり、実際の動作確認は行っていない。
START_PATTERN = '11110000' # セッション開始を示す固定パターン
END_PATTERN = '00001111' # セッション終了を示す固定パターン
def send_session_start():
for bit in START_PATTERN:
send_bit(bit)
def send_session_end():
for bit in END_PATTERN:
send_bit(bit)
L2のスタートビットが「1フレームの始まり」を示すのに対し、L5のハンドシェイクは「通信セッション全体の始まり」を示す。粒度の異なる「始まりと終わり」の管理が別々の層に存在することが、ここで腑に落ちた。
L6(プレゼンテーション層):RSA暗号化の実装
プレゼンテーション層はデータの表現形式を変換する層である。文字コードの変換・圧縮・暗号化がここに位置付けられる。本システムではRSA暗号化をこの層の責務として実装した。
128bitのRSA鍵ペアを生成し、送信前に文字列を暗号文に変換する。実用のRSA(2048bit以上)と同等のアルゴリズムを最小鍵長で実装することを目標とし、以下の数学的処理を自力でコーディングした。
- 素数生成:ミラー・ラビン法による確率的素数判定
- 鍵ペア生成:モンゴメリ乗算を用いた高速べき乗剰余演算
- メモリ管理:RPiの限られたRAMで大きな整数を扱うための動的メモリ確保(C言語実装)
以下のコードは動作イメージを示すための生成AIによるサンプルであり、実際の動作確認は行っていない。
// モンゴメリ乗算の概略(C言語)
uint64_t montgomery_mul(uint64_t a, uint64_t b, uint64_t n, uint64_t n_inv) {
__uint128_t t = (__uint128_t)a * b;
uint64_t m = (uint64_t)t * n_inv; // n_inv = -n^{-1} mod 2^64
uint64_t u = (t + (__uint128_t)m * n) >> 64;
return (u >= n) ? u - n : u;
}
素数の総当たり探索やべき乗の愚直な繰り返し乗算といったナイーブな実装と比較したとき、処理速度に大きな差が出た。この経験を通じて得られた気づきは、「PythonライブラリやOpenSSLのような実用暗号ライブラリは、こうした数学的アルゴリズムによって効率化が図られている」 という実感である。普段何気なくimportして呼び出しているライブラリの内側に、こうした工夫が積み重なっていることを、手を動かして初めて理解できた。
L7(アプリケーション層):ユーザーと通信システムをつなぐ窓口
アプリケーション層は、ユーザーや上位のアプリケーションが通信システムを利用するためのインターフェースを提供する層である。HTTPやSMTPのような高レベルなプロトコルがここに相当するが、本システムではシンプルなCLIインターフェースとしてこれを実装した。
送信側ではユーザーがターミナルに入力した文字列を受け取り、L6以下に渡す。受信側では復号済みの文字列を受け取り、ターミナルに表示する。
以下のコードは動作イメージを示すための生成AIによるサンプルであり、実際の動作確認は行っていない。
def application_send():
text = input("送信する文字列を入力してください: ")
bits = str_to_bits(encrypt(text)) # L6: 暗号化
send_session_start() # L5: セッション開始
for block in split_into_blocks(bits): # L4: フレーム分割
for bit in block:
send_bit(bit) # L1/L2: 物理送出
send_session_end() # L5: セッション終了
print("送信完了")
def application_receive():
wait_for_session_start() # L5: セッション開始待ち
bits = receive_all_bits() # L1〜L4: 受信・復元
text = decrypt(bits_to_str(bits)) # L6: 復号
print(f"受信した文字列: {text}") # L7: 出力
このコードを書いたとき、「アプリケーション層とは、下位レイヤーの複雑さを隠蔽してユーザーに単純なインターフェースを提供することだ」 という感覚を把握できた。
システム動作の確認
最終的に、以下の動作を確認できた。
| 項目 | 結果 |
|---|---|
| 伝送媒体 | LED(可視光)→ フォトダイオード |
| 変調方式 | OOK(On-Off Keying) |
| 暗号化 | RSA 128bit(自前実装) |
| 電磁波使用 | なし(完全光無線) |
| 通信の一連フロー | 確認済み |
電磁波を一切使わずにビット列を無線伝送し、暗号化・復号まで含めた通信の一連の基礎フローを体得できた。
まとめ・所感
生成AIの進化は目覚ましく、実験の試行錯誤サイクルそのものをAIがコントロールする時代も、そう遠くないかもしれない。パラメータ探索・バグ修正のループを人間の代わりにAIが回す光景は、すでに開発の最前線では現実になりつつある。
しかしその一方で、手を動かして何かを作り始めるハードルは、生成AIの登場によって劇的に下がった。
かつては「ハードウェア制御」「暗号アルゴリズムの自前実装」「信号処理」といった領域は、それぞれ専門知識を持つ人間が個別に学習コストを負担しなければ踏み込めないものだった。今は、分からない概念をAIに問いながら、隣接する専門領域に少しずつ手を伸ばすことができる。
OSI参照モデルは、通信の世界に限らず、「複雑なシステムを責務ごとに分けて考える」 という設計思想の原型である。その各層を自分の手で実装した経験は、クラウド・マイクロサービス・AI基盤といった現代的なアーキテクチャを考えるときにも確かに生きている。
生成AIをうまく使えば、自分の専門領域の外側にも踏み出せる時代だ。ぜひ、普段とは違う層・違う領域で何かを作ってみてほしい。
