Raspberry PiでWebSocket|Lチカ、WebSocketの理解

Raspberry PiでWebSocket|Lチカ、WebSocketの理解
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をつないでおく。

Raspberry PiGPIOとLEDの回路図
Raspberry PiGPIOとLEDの回路図

RPi.GPIO を使って、LEDを5回点滅させるだけの簡単なプログラムだ。これでLEDをオンオフできるようになった。
py
# -*- 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でインストールできるので次のようにしてインストールしてく。

shell
$ sudo pip install websocket-server

そして作成したPythonプログラムがこちら。ただしIPアドレスはRaspberry PiのIPアドレスに置き換える。クライアントからWebSocketのメッセージを受信できるようになっている。メッセージが led_on であればLEDを点灯、led_off であれば消灯するプログラムである。ファイル名をled_switch_sever.py にしてRaspberry Piに保存しておこう。

py
# -*- 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
<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が点灯消灯を繰り返すはずだ。

WebブラウザーからRaspberry PiのLEDをリモート操作
Webブラウザーから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!」を送信するプログラムである。

py
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 を実行して wlan0inet に書かれている。

JavaScriptでWebSocketクライアントの作成

次に、Raspberry PiでWebSocketサーバーへ通信するために、WebSocketクライアントをJavaScriptで作ってみた。このコードはMacで動かすものとする。

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/");
            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メッセージを送る

JavaScriptでブラウザからRaspberry PiへWebSocketメッセージを送る
JavaScriptでブラウザからRaspberry PiへWebSocketメッセージを送る

今度は逆に、クライアントからサーバーへWebSocketでメッセージを送れるようにしてみよう。

Raspberry Piのサーバープログラム websocket-sever.py を少し改造する。

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
<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のハンドシェイクする形になっている。

shell
GET / HTTP/1.1
Host: 192.168.100.136
Connection: Upgrade
Sec-WebSocket-Key: vTu2gqiamRGo9gXd9dKbXg==
Sec-WebSocket-Version: 13
Upgrade: websocket
Sec-WebSocket-Keyは16byteのランダムな値をbased64でエンコードしたもの。これはサーバーとクライアントとのハンドシェイクの受信を立証するために必要なものだ。

上記のHTTPリクエストをWebSocketサーバーへ投げると、次のようにレスポンスが返ってくる。

shell
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Od7wpaRgvxXMSnWrv2qkQqREX7I=

これは、これからWebSocket通信に切り替わるメッセージ。このハンドシェイク後、WebSocketでのデータ通信が行えるようになる。

WebSocketのデータフレーム

さらに、WebSocketのデータフレームがどうなっているのか見てみよう。

WebSocketのデータフレーム
WebSocketのデータフレーム

RFC6455より RFC6455-The WebSocket Protocol

流石にこのデータフレームを1から作り上げていくのはシンドイ。今回は「へぇ、こんな感じなんだ」で流して、オープンソースを参考にクライアントを作成する。

最終的にPythonで書いたWebSocketクライアントプログラムがこちら。

py
# -*- 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などで遊びながら仕組みを理解すると良い。

関連記事

最後までご覧いただきありがとうございます!

▼ 記事に関するご質問やお仕事のご相談は以下よりお願いいたします。
お問い合わせフォーム

ESP32搭載ボード
ESP32の書籍
Arduinoで人気の周辺パーツ
M5Stackのオススメ参考書
M5Stack製品
M5StickCで使えるHat
ESP32ボード
関連記事