WebSocket接続でRasPiをリモート化するWebアプリのプログラム例

WebSocketDisp

Raspberry Piで制御する機器をリモートで操作する方法にはいろいろとあるが、WebブラウザからWebSocket接続してリモート化するWebアプリを作成したので、そのプログラム例を紹介する。
WebSocketServer.pyの一部コードを修正(2020.9.30)

●Raspberry Piをリモート化する方法

リモート化する方法でよく使われているのが、HTMLとJavaScriptを使ってウエブからWebIOPiでGPIOを制御する方法。JavaScriptを介してWebIOPiに繋げるワンクッションが必要で制限もありやや面倒である。前回紹介したTCP接続してリモート化するiPhoneアプリは、作成は簡単だが、iPhone/iPadなどapple機器しか使えない。Webブラウザを使えば汎用性があるが、JavaScriptでTCP接続のアプリを作ろうとすると、node.jsをインストールする必要があり、汎用性に欠ける。そこで、もう少し汎用性のあるWebSocket接続を使うことにした。JavaScriptもpythonもWebSocketのAPIが標準で用意されているので、特殊な作業は必要ない。

PythonのWebSocket-Serverの使い方は次のサイトを参考にした。
https://github.com/Pithikos/python-websocket-server
JavaScriptのWebSocket Clientの使い方は次のサイトを参考にした。
https://docs.oracle.com/cd/E92951_01/wls/WLPRG/websockets.htm

●プログラム例の構成

program-outline
プログラムの構成

●プログラム事例

<index.html>

WebSocketサーバーへの接続、Raspberry Piにコマンドコードを送るためのリモコンGUI画面を作るHTMLプログラム。(style.cssは省略)

<!-- index.html --> 
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>WebSocket 接続</title>
        <link rel="stylesheet" href="style.css">
    </head>
    
    <script type="text/javascript" language="JavaScript" src="./js/webSocket.js"></script>
    <script type="text/javascript" language="JavaScript" src="./js/remote.js"></script>

    <body>
        <h1>WebSocketリモート接続</h1>

        <fieldset id = "field">
        <legend>接続先</legend>
            <table border="1"  id = "tbl">
                <tr>
                    <td ><label id="lbl">IPアドレス</label></td>
                    <td ><input type="text" id="Host" value = "192.168.1.13"></td>
                    <td rowspan="2" id="btnTd"><input type="button" value="接続" onclick="wsOpen()" id ="btn"></td>
                </tr>

                <tr>
                    <td ><label id = "lbl">Port番号</label></td>
                    <td ><input type="text" id="Port"  value = "5000"></td>
                </tr>

                <tr>
                    <td ><label id="lbl">ステータス</label></td>
                    <td ><input type="text" id="Status" value = "切断中"></td>
                    <td id="btnTd" ><input type="button" value="切断" onclick="wsClose()" id ="btn"></td>
                </tr>
            </table>
        </fieldset>

        <fieldset id = "field">
        <legend>送受信</legend>
            <table border="1" id = "tbl">
                <tr>
                    <td ><label id = "lbl">送信メッセージ</label></td>
                    <td ><input type="text" id="SndMsg" placeholder="送信メッセージを入力"></td>
                    <td  rowspan="2"  id ="btnTd" ><input type="button" value="送信" onclick="sendMsg()" id ="btn"></td>
                </tr>

                <tr>
                    <td><label id = "lbl">受信メッセージ</label></td>
                    <td><input type="text" id="RcvMsg" placeholder="受信メッセージ"></td>
                </tr>
            </table>
        </fieldset>

        <fieldset id = "field">
        <legend>速度・加速度</legend>
            <table border="1" id = "tbl">
                <tr>
                    <td id = "lbl">速 度</td>
                    <td style="text-align: center"><input type="text" id="spd" value = 0 style="text-align: center; WIDTH: 120px;"></td>
                    <td id="btnTd"><input type="button"  value="-"  onClick="valueDown(0)" id ="btn"></td>
                    <td id="btnTd"><input type="button"  value="+" onClick="valueUp(0)" id ="btn"></td>
                </tr>

                <tr>
                    <td id = "lbl">加速度</td>
                    <td style="text-align: center"><input type="text" id="acc" value = 0 style="text-align: center; WIDTH: 120px;"></td>
                    <td id="btnTd"><input type="button"  value="-"  onClick="valueDown(1)" id ="btn"></td>
                    <td id="btnTd"><input type="button"  value="+" onClick="valueUp(1)" id ="btn"></td>
                </tr>
            </table>
        </fieldset>

        <fieldset id = "field">
        <legend>単位</legend>
            <table border="1" id = "tbl">
                <tr>
                    <td id = "radio">
                        <form name="formName0">
                        <div id="radioarea" onChange="unitChange()";>
                        <input type="radio" name="choice" value=1>1
                        <input type="radio" name="choice" value=10 checked>10
                        <input type="radio" name="choice" value=100>100
                        <input type="radio" name="choice" value=1000>1000
                        <input type="radio" name="choice" value=10000>10000
                        </div>
                        </form>
                    </td>
                </tr>
            </table>
        </fieldset>

        <fieldset id = "field">
        <legend>キーボード</legend>
            <table border="0" id = "tbl">
                <tr>
                    <td colspan = "3" id="keyTd"><input type="button"  value="↑" onClick="keyBd('1')" id = "keyBtn" ></td>
                </tr>
                <tr>
                    <td id="keyTdL"><input type="button"  value="←"  onClick="keyBd('2')" id = "keyBtn"></td>
                    <td id="keyTd"><input type="button"  value="□"  onClick="keyBd('0')" id = "keyBtn"></td>
                    <td id="keyTdR"><input type="button"  value="→" onClick="keyBd('3')" id = "keyBtn" ></td>
                </tr>
                <tr>
                    <td colspan = "3" id="keyTd"><input type="button"  value="↓" onClick="keyBd('4')" id = "keyBtn" ></td>
                </tr>
            </table>
        </fieldset>

    </body>
</html>
<remote.js>

リモコンGUI画面のデータを計算して、RasPiへコマンドコードを送るJavaScriptプログラム。

// remote.js

class ValUpDown {
    constructor(name, code, val, step, min, max) {
        this.name = name;
        this.code = code;
        this.val = val;
        this.step = step;
        this.min = min;
        this.max = max;
    }
    
    valUp() {
        if (this.val + this.step <= this.max) {
            this.val += this.step;
        }
        document.getElementById(this.name).value = this.val;
        sendCmnd(this.code + "," +  this.val.toString())
    }

    valDown() {
        if (this.val - this.step >= this.min) {
            this.val -= this.step;
        }
        document.getElementById(this.name).value = this.val;
        sendCmnd(this.code + "," + this.val.toString())
    }
}

function valueUp(no) {
    values[no].valUp()
}

function valueDown(no) {
    values[no].valDown()
}


function unitChange(){	// unit change
    var unit;
    for (var i = 0; i < document.formName0.choice.length; i++) {
        if(document.formName0.choice[i].checked) {
            unit = parseInt(document.formName0.choice[i].value);
            break;
        }
        unit = parseInt(document.formName0.choice[i].value);
    }
    sendCmnd("U, " + unit.toString())
    console.log("Unit=", unit);
}

function keyBd(keyNo) {
    sendCmnd("K, " + keyNo)
    console.log("key=", keyNo);
}

var values = []
values.push(new ValUpDown('spd', 'SPD', 0, 1, -10, 10))
values.push(new ValUpDown('acc', 'ACC', 0, 1, -10, 10))
<webSocket.js>

WebSocketサーバーへの接続、送受信、切断を行うJavaScriptプログラム

// webSocket.js
var connection;

function wsOpen() {
    host = document.getElementById("Host").value;
    port = document.getElementById("Port").value;
    connection = new WebSocket('ws://' + host + ':' + port);
    
    //接続成功
    connection.onopen = function(event) {
        document.getElementById("Status").value = "接続中";
    };

    //接続時エラー発生
    connection.onerror = function(error) {
        document.getElementById("Status").value = "接続エラー";
    };

    //メッセージ受信
    connection.onmessage = function(event) {
        document.getElementById("RcvMsg").value = event.data;
    };
    
    //切断
    connection.onclose = function() {
        document.getElementById("Status").value = "切断中";
    };
}

//メッセージ送信
function sendMsg(){
    connection.send(document.getElementById("SndMsg").value);
}

 //切断
 function wsClose(){
    connection.close();
 	document.getElementById("Status").value = "切断";
}

//コマンド送信
function sendCmnd(cmnd) {
    document.getElementById("SndMsg").value = cmnd;
    connection.send(cmnd);
}
<WebSocketServer.py>

Raspberry Pi側のWebSocketServerプログラムの一例。

WebSocketサーバーを設定し、Clientから送られる操作文字列を受信して、その操作を実行する。このプログラム例では、送られる文字列を「,」で分離して、文字情報を得るところまでとしているが、その文字情報を使って様々な動きのプログラムを入れることができる。WebSocket送受信をthreadingを使って行っている。WebsocketServerモジュールを使うために、websocket-serverのインストールが必要。最新版をインストールするには、上述のpython-websocket-serverのサイトから行う方が良いが、次のように、pip3 installを使っても問題ない。
sudo pip3 install websocket-server

# -*- coding: utf-8 -*-
# WebSocketServer.py
# sudo pip3 install websocket-server
from websocket_server import WebsocketServer
import threading

class WsServer():
    def __init__(self, host, port):
        self.server = WebsocketServer(port, host)
        self.rcvData = []

    def newClient(self, client, server):
        print("Connected client : ", client['address'])
        self.server.send_message(client, "OK! Connected")

    def clientLeft(self, client, server):
        print("Disconnected : ", client['address'])

    def messageReceived(self, client, server, message):
        self.server.send_message(client, "OK, Received : " + message)
        self.rcvData.append(message)
    
    def runServer(self):
        self.server.set_fn_new_client(self.newClient)	# Client接続時
        self.server.set_fn_client_left(self.clientLeft)	# Client切断時
        self.server.set_fn_message_received(self.messageReceived) 	# Clientからの受信時
        self.server.run_forever()

HOST = "" # IPアドレスopen
PORT = 5000 # ポートを指定
wsServer = WsServer(HOST, PORT)

def wsStart():
    wsServer.runServer()

# Start receiving data from client
recvThread = threading.Thread(target=wsStart)
recvThread.setDaemon(True)	# 修了時にハングアップしない
recvThread.start()
print("Threading Start")

key=["Stop","Forwrd","Right","Back", "Left"] 

try:
    while True:
        if len(wsServer.rcvData):	# Receive data from Client PC
            pdata = wsServer.rcvData[0].split(",")
            print("Receive data = "+wsServer.rcvData[0], pdata)
            wsServer.rcvData.pop(0)	# Pop Top data
            if pdata[0] == 'K' and pdata[1].isdecimal():	# 方向キーコマンド受信
                print("Direction=", key[pdata[1]])
                # ここに方向キーで制御するプログラムを入れる
            elif len(pdata) > 1 and pdata[1].isdecimal():	# その他のコマンド受信
                val = int(pdata[1])
                print("Command=", pdata[0], pdata[1])
            else:
                print("Others=", pdata)
                
except KeyboardInterrupt:
    print("Keyboard Interrupt")

print("Loop terminated")

 

●リモートGUI画面

WebSocketDisp
WebのGUI画面の例