Raspberry PiでWebSocket|Lチカ、WebSocketの理解
WebSocketでLチカ
ここではHTMLとJavaScriptでボタンを作り、MacのウェブブラウザからRaspberry Piを遠隔操作する方法を説明する。通信方法はWebSocketで行っていく。遠隔操作できるようになるのが目的なので、LEDをオンオフ(Lチカ)するだけの簡単なプログラムとなる。
ここではRaspberry Pi zeroを使ったが、WiFi機能があれば他のRaspberry PiでもOK。
Lチカの動作テスト
まずはRaspberry PiとLEDだけで簡単な動作テストをしておこう。図のように、Raspberry PiのGPIO11に1kΩを通してLEDをつないでおく。
RPi.GPIO を使って、LEDを5回点滅させるだけの簡単なプログラムだ。これでLEDをオンオフできるようになった。# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
from time import sleep
LED = 11
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED, GPIO.OUT)
for t in range(0, 5):
GPIO.output(LED, True)
sleep(1)
GPIO.output(LED, False)
sleep(1)
Raspberry PiにWebSocketサーバーを設定
さきほどのLEDをWebSocketで点灯できるようにしてみよう。Raspberry PiでWebSocketのデータを受信できるようにするため、Pythonで動く websocket-server ライブラリを使った。pipでインストールできるので次のようにしてインストールしてく。
$ sudo pip install websocket-server
そして作成したPythonプログラムがこちら。ただしIPアドレスはRaspberry PiのIPアドレスに置き換える。クライアントからWebSocketのメッセージを受信できるようになっている。メッセージが led_on であればLEDを点灯、led_off であれば消灯するプログラムである。ファイル名をled_switch_sever.py にしてRaspberry Piに保存しておこう。
# -*- coding: utf-8 -*-
import RPi.GPIO as GPIO
from time import sleep
from websocket_server import WebsocketServer
LED = 11
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED, GPIO.OUT)
def receivedMessage(client, server, message):
print(message)
if message == 'led_on':
GPIO.output(LED, True)
elif message == 'led_off':
GPIO.output(LED, False)
else:
print("Unknown Message: {}".format(message))
server = WebsocketServer(5555, host="192.168.100.136")
server.set_fn_message_received(receivedMessage)
server.run_forever()
WebSocketの仕組みのを詳しく知りたい方はこちら
WebSocketクライアントを作る
次に、HTMLとJavaScriptでWebSocketクライアントを作っていく。今回はMacを使ったが、HTMLファイルが実行できればWindowsでもスマホからでも動作するはず。好きな端末をクライアントマシンにしよう。
作ったクライアントプログラムがこちら。ボタンを配置し、それを押すとRaspberry PiへWebSocketを投げるプログラムとなっている。led_switch_client.html として適当な場所に保存しておこう。
<html>
<head>
<script src="http://code.jquery.com/jquery-latest.min.js">
</script>
<script>
$(function () {
var ws = new WebSocket("ws://192.168.100.136:5555/");
$('#btn').on('click', function () {
if($('#btn').text() == "OFF") {
$('#btn').text("ON")
ws.send('led_on');
} else {
$('#btn').text("OFF")
ws.send('led_off');
}
});
})
</script>
<style>
#btn{
width: 500px;
height:200px;
font-size:100px;
}
</style>
</head>
<body>
<button id="btn">OFF</button>
</body>
</html>
WebSocketでLチカ実践
それでは最後に、プログラムを実行してRaspberry PiのLEDを点滅させてみよう。まずRaspberry Piで $ python led_switch_sever.py を実行して、WebSocketサーバーを立ち上げておく。次に led_switch_client.html をウェブブラウザ開いてみよう。下の動画のようにボタンを押すごとに、Raspberry Piに接続されたLEDが点灯消灯を繰り返すはずだ。
WebSocketの仕組みを理解する
ここらはHTMLとJavaScrpitを使ってクライントを作り、できるだけ簡単にWebSocketの通信を実現し、WebSocketの全体像をつかみやすくした。
Raspberry PiからWebSocketでブラウザにメッセージを送る
websocket-sever のインストールができたところで、Raspberry PiからWebSocketでブラウザにメッセージを送ってみよう。次の簡単なWebSocketサーバーをPythonで作った。Raspberry PiからWebSocketでブラウザへ「Hello world! This is Raspberry Pi!」を送信するプログラムである。
from websocket_server import WebsocketServer
def sendMessage(client, server):
server.send_message_to_all("Hello world! This is Raspberry Pi!")
server = WebsocketServer(5555, host="192.168.100.136")
server.set_fn_new_client(sendMessage)
server.run_forever()
hostにはRaspberry PiのIPアドレスを設定する。IPアドレスを調べるには、$ ifconfig を実行して wlan0 の inet に書かれている。
JavaScriptでWebSocketクライアントの作成
次に、Raspberry PiでWebSocketサーバーへ通信するために、WebSocketクライアントをJavaScriptで作ってみた。このコードはMacで動かすものとする。
<html>
<head>
<script src="http://code.jquery.com/jquery-latest.min.js">
</script>
<script>
$(function () {
var ws = new WebSocket("ws://192.168.100.136:5555/");
ws.onmessage = function (message) {
document.getElementById("recived").innerHTML = message.data;
}
})
</script>
</head>
<body>
<div id="recived"></div>
</body>
</html>
このコードをたとえば websocket-client.html として保存し、それをSafariで表示させてみよう。
Raspberry Piで設定したメッセージ「Hello world! This is Raspberry Pi!」が、ブラウザに表示されればWebSocketの通信成功である。
JavaScriptでブラウザからRaspberry PiへWebSocketメッセージを送る
今度は逆に、クライアントからサーバーへWebSocketでメッセージを送れるようにしてみよう。
Raspberry Piのサーバープログラム websocket-sever.py を少し改造する。
from websocket_server import WebsocketServer
def sendMessage(client, server):
server.send_message_to_all("Hello world! This is Raspberry Pi!")
def receivedMessage(client, server, message):
print message
server.send_message(client, "recived message: " + message)
server = WebsocketServer(5555, host="192.168.100.136")
server.set_fn_new_client(sendMessage)
server.set_fn_message_received(receivedMessage)
server.run_forever()
クライアント側の websocket-client.html からRaspberry PiへWebSocketでメッセージを送信できるように修正した。
<html>
<head>
<script src="http://code.jquery.com/jquery-latest.min.js">
</script>
<script>
$(function () {
var ws = new WebSocket("ws://192.168.100.136:5555/");
ws.onmessage = function (message) {
document.getElementById("recived").innerHTML = message.data;
}
$('#btn').on('click', function () {
ws.send($('#btn').text());
});
})
</script>
</head>
<body>
<div id="recived"></div>
<button id="btn">Hi, I'm from Mac.</button>
</body>
</html>
ブラウザで表示し、ボタンを押すと次のようにメッセージを送れたと思う。
WebSocketのHTTPリクエストを手作りしてみる
今度はよりWebSocket通信の理解を深めるため、JavaScriptではなくPythonを使ったWebSocketクライアントを作ってみよう。Pythonのscoketライブラリを使って、HTTPヘッダーを手作りしてみる。
WegSocketのヘッダーの構成
ところでWebSocketは次のようにHTTPリクエストを使ってWebSocketのハンドシェイクする形になっている。
GET / HTTP/1.1
Host: 192.168.100.136
Connection: Upgrade
Sec-WebSocket-Key: vTu2gqiamRGo9gXd9dKbXg==
Sec-WebSocket-Version: 13
Upgrade: websocket
上記のHTTPリクエストをWebSocketサーバーへ投げると、次のようにレスポンスが返ってくる。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Od7wpaRgvxXMSnWrv2qkQqREX7I=
これは、これからWebSocket通信に切り替わるメッセージ。このハンドシェイク後、WebSocketでのデータ通信が行えるようになる。
WebSocketのデータフレーム
さらに、WebSocketのデータフレームがどうなっているのか見てみよう。
RFC6455より RFC6455-The WebSocket Protocol
流石にこのデータフレームを1から作り上げていくのはシンドイ。今回は「へぇ、こんな感じなんだ」で流して、オープンソースを参考にクライアントを作成する。
最終的にPythonで書いたWebSocketクライアントプログラムがこちら。
# -*- coding: utf-8 -*-
import time
import os
import base64
import socket
import struct
import array
try:
import thread
except ImportError:
import _thread as thread
## python3で動作
def ws_client(host, port, header):
print('++++++++++++HEADER START+++++++++++++++++')
print(header + '+++++++++++++HEADER END++++++++++++++++++')
address = (host, port)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(address)
s.send(header.encode())
response = b''
while True:
recv = s.recv(1)
if not recv:
break
response += recv
if b'\r\n\r\n' in response:
return response.decode(), s
def make_ws_data_frame(data):
FIN = 0x80 # 128(8bit分を確保、そしてFINフラグは1である -> 0b10000000)
RSV1 = 0x0
RSV2 = 0x0
RSV3 = 0x0
OPCODE = 0x1
# [OPCODE]
# 0x0 継続フレーム
# 0x1 テキストフレーム
# 0x2 バイナリフレーム
# 0x8 Closeフレーム(接続切断)
# 0x9 ping
# 0xA pong
# FINと同様の考えでMASKから8bit分確保する
MASK = 0x80
payload = 0x0
frame = struct.pack('B', FIN | RSV1 | RSV2 | RSV3 | OPCODE)
# frame=b'\x81' -> 0b10000001
# print(bin(int(frame.hex(), 16)))
data_len = len(data)
# print(data_len)
if data_len <= 125:
payload = struct.pack('B', MASK | data_len)
elif data_len < 0xFFFF:
payload = struct.pack('!BH', 126 | MASK, data_len)
else:
# B(unsigned char, 1byte) Q(unsigned long long, 8byte)
payload = struct.pack('!BQ', 127 | MASK, data_len)
frame += payload
#print(bin(int(frame.hex(), 16)))
# 0b10000001 111111100000000011111110
masking_key = os.urandom(4)
#print(masking_key)
mask_array = array.array('B', masking_key)
unmask_data = array.array('B', data.encode('UTF-8'))
for i in range(data_len):
unmask_data[i] = unmask_data[i] ^ masking_key[i % 4]
mask_data = unmask_data.tobytes()
frame += masking_key
frame += mask_data
return [frame, len(frame)]
if __name__ == "__main__":
HOST = '192.168.100.136'
PORT = 5555
HEADER = 'GET / HTTP/1.1\r\n'
HEADER += 'Host: ' + HOST + '\r\n'
HEADER += 'Connection: Upgrade\r\n'
HEADER += 'Sec-WebSocket-Key: ' + base64.b64encode(
os.urandom(16)).decode('UTF-8') + '\r\n'
HEADER += 'Sec-WebSocket-Version: 13\r\n'
HEADER += 'Upgrade: websocket\r\n'
HEADER += '\r\n'
res, client = ws_client(HOST, PORT, HEADER)
print(res)
if header != '':
def run(*args):
frame, frame_len = make_ws_data_frame("Hello, I'm from Mac!")
client.send(frame)
time.sleep(1)
# client.close()
print("thread terminating...")
thread.start_new_thread(run, ())
response = b''
while True:
recv = client.recv(1024)
print(recv)
if not recv:
break
response += recv
if b'\r\n' in response:
print(response.decode())
response = b''
データフレームのプログラムはこちらを参考 WebSocketクライアント - Qiita
Pythonでプログラムを実行すれば、Raspberry PiのWebSocketサーバーと通信できるはずだ。JavaScriptではあれほど簡単だったWebScocket通信も、実際は複雑な処理を内部で行っている。
とは言え、データフレーム以外はHTTPリクエストと同じなので難しくないと思う。WebSocketでの通信は今後ますます普及していくので、Raspberry Piなどで遊びながら仕組みを理解すると良い。