UMEHOSHI ITA TOP PAGE    COMPUTER SHIEN LAB

このページでは、Webサーバ側アプリケーションを構築して、任意のブラウザでロボットを制御できるようにし、
最終的にそのサーバアプリを終了すると、別途のTCPサーバプリが動作できるようになり、
TCPクライアントからの文字列受信で制御できるモードにする構築例を紹介しています。

Raspberry PI Zero WとUSB接続し、Wifi接続してWebブラウザで制御する例

このページで紹介したロボットにプログラムを追加して、 Webブラウザで制御できるようにするための情報です。
Raspberry PI ZeroをWifiアクセスポイントにして、そこに立てたWebサーバに接続することでブラウザで制御する作品です。
Webサーバを構築するまでの解説は、こちらのページで行っています
以下は、それが終わった後のCGI関連(python利用)を追加する解説をしています。
(下記画像のクリックでイメージが変わります。) このClickでYoutube 紹介動画へ移動できます。


右上のイメージが、ロボットするコントールパネルの「index.htm」の閲覧場面です。
このサイトと同じに作っていれば、http://192.168.100.1/index.htmのURLで見ることができます。
 (その場合pizeroのアクセスポイントにパスフレーズ「abcd1234」で接続してから行います。
 また、Tera Termでがあれば『ttermpro.exe 192.168.100.1 /user=pi /passwd=abc123』のようなアクセスも可能なはずです。)

ページ上部右端にあるパスワード『abc123』が正しい場合だけ、ボタン操作で、前進、後進、左回転、右回転、停止が可能です。
なお、ULは左モータのでデュティ比をアップし、DLはダウンです。URとDRは右モータ用です。
[+100]、[-100]、[+10]、[-10]、[+1]、[-1]のボタンで、撮影する写真フファイル(scr0000.jpg〜scr9999.jpg)の番号を設定できます。
この設定したファイルに撮影して記憶するボタンが「+GET」です。
この「+GET」による閲覧は、対象の写真フファイル(scr0000.jpg〜scr9999.jpg)が存在する場合はその過去に撮影したファイルが見えます。
存在しない場合は、撮影した写真フファイルが生成されてそれが見えます。
いずれにしても、この操作によって写真フファイルの番号は一つ進んだ番号に後進され、1秒ごとの自動更新処理が停止します。
この自動更新を再び実行させる場合は、左端の「現在」のボタンをクリックします。
カメラ画像更新処理はこの1秒ごとjavascriptインターバルで要求する画像ファイル名の背景色が[白]になり、 画像がloadされて更新時に[シアン]になります。
(この1秒周期の色変化で写真更新動作が確認できます)
以上の実現するため、Raspberry PI Zero側で、新たに追加するCGI関連のファイルと、Webサーバ関連のプログラムを示します。

index.htm

以前に紹介したこちらのページのWebサーバ(myweb.py)が動作していれば、[+GET] による撮影が可能です。

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Cache-Control" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width">
    <style>
        body {
            font-size: medium;
            background-color:aquamarine;
            padding: 0;
            margin: 0;
        }
        button {font-size: medium;}
        tr { width: 370px;}
        td {
            width: 120px;
            height: 70px;
            color: rgb(0, 0, 255);
            border: 1px solid blue;
            border-radius: 10px;
            text-align: center;
            font-size: 20px;
            background-color: lightgray;
            box-shadow: rgb(128, 128, 128) 5px 5px;
            cursor: pointer;
        }
    </style>
    <title>Image </title>
<script type="text/javascript">
var img_elements;
var text_file;
var filename ="imgs/tscr.jpg";
var count=0;

var timeId2 = null; // タイマーによる自動更新時、インターバルタイマーオブジェクトを記憶
var img_elements;   // ロード用imageオブジェクト(ロードイベントを設定して使う)
var img_w ,img_h;   //オリジナル画像サイズ
var messageTag;     // 応答メッセージ表示用
var getImageFileName=""; // ボタンで取得するイメージファイル(scr0000.jpg〜scr9999.jpg)
var current_N=0;//マニュアル要求(scrXXXXX.jpg)で使うXXXXXの番号

function re_load(){
    filename = "tscr" + count++ + ".jpg";
    text_file.textContent = filename;
    text_file.style.backgroundColor="white";
    img_elements.src = "imgs/" + filename;// 「画像ファイルの要求」
}

function init(){// 全体の初期化
    text_file = document.getElementById("CurrentImgName");
    messageTag = document.getElementById('MESSAGE');
    img_elements= document.getElementById("IMG_A");
    img_elements.onload= function (e) {
        text_file.style.backgroundColor="cyan";
	};
    btnInit();// ボタン初期化
    set_changeImgName(0);
    timeId2 = setInterval(re_load, 1000);//1秒ごとに実行
}

function set_changeImgName(n){// 要求用のイメージファイル名のgetImageFileNameをセット 
    if( timeId2 != null){
        clearInterval(timeId2);// タイマー停止
        timeId2=null;
    } 
    var nn = current_N + n;
    if( nn < 0 ) nn = 0; 
    path="00000"+nn;
    path = path.substring( path.length - 4);
    getImageFileName = "scr" +path + ".jpg";
    document.getElementById("CurrentImgName").textContent=getImageFileName;
    current_N = nn;
}

function get_ImageUp(){// getImageFileName のイメージファイルを要求
    img_elements.src="imgs/"+getImageFileName;
    text_file.style.backgroundColor="white";
    document.getElementById("CurrentImgName").style.backgroundColor="white";
    set_changeImgName(1);
}

function timer_on(){// 画像要求の自動更新インターバルタイマーをON
    if(timeId2 == null) timeId2 = setInterval(re_load, 1000);//1秒ごとに実行
    document.getElementById("CurrentImgName").style.backgroundColor="white";
}

//HTTPでサーバーと通信するためのXMLHttpRequestオブジェクトを取得する。
function getMyXmlHttp() {
	var xmlHttp;
	if (window.XMLHttpRequest) {
		try {
      			xmlhttp = new XMLHttpRequest();
		} catch (e) {
			xmlhttp = false;
		}
	}else if (window.ActiveXObject){
		try {
      			xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    		} catch (e){
      			try {
        			xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
			} catch (E){
				xmlhttp = false;
			}
		}
	}
	return xmlhttp;
}

function OnButtonProc(it){// it は押されたButtonオブジェクト
    var httpObj = getMyXmlHttp();//サーバと通信するためのオブジェクト取得
    var path="http://" + location.hostname + ":" + location.port + "/cgi-bin/ume.py";
    //messageTag.textContent = path;
    httpObj.open("POST", path);//HTTPで要求
    httpObj.setRequestHeader('Content-Type','application/x-www-form-urlencoded');//文字化け対策
    httpObj.onreadystatechange = function(){//HTTPのレスポンスで実行するように関数登録
        if(httpObj.readyState == 4 && httpObj.status == 200){
            //messageTag.textContent = httpObj.responseText;//レスポンスの表示
            it.style.backgroundColor = "lightgray";
            it.disabled = false;
        }
    }
    httpObj.send("CMD="+it.id + "&PWD=" + document.getElementById("PWD").value);
}

function btnInit(){ // ボタンの初期化
    var btns = document.getElementsByName("BTN");
    var chg = [ "UL", "Forward", "UR", 
                "Left", "Stop", "Right", 
                "DL", "Backword", "DR" ];
    for( var i=0; i < btns.length; i++){
        btns[i].id = chg[i];
        btns[i].textContent = "" + chg[i];
        btns[i].onmousedown=function(e){
            this.style.backgroundColor = "rgb(255, 255, 132)";
            this.disabled = true;
            OnButtonProc(this) // Ajaxによるコマンド送信処理
        };
        btns[i].onmouseup=function(e){
            this.style.backgroundColor = "lightgray";
        };        
    }
}

</script>
</head>
<body onload="init()">
<a href="index.html">閲覧へ</a>
<button onclick="timer_on()" id="CurrentImgChange">現在</button>
<button onclick="set_changeImgName(-100)">-100</button>
<button onclick="set_changeImgName(100)">+100</button>
<button onclick="set_changeImgName(-10)">-10</button>
<button onclick="set_changeImgName(10)">+10</button>
<button onclick="set_changeImgName(-1)">-1</button>
<button onclick="set_changeImgName(1)">+1</button>
<text id="CurrentImgName">tscr.png</text>
<button onclick="get_ImageUp()">+GET</button>
<input type="password" id="PWD" name="PWD" size="5"> <a href="imgs/">imgs</a>
<div style="display: inline-block; text-align: center;">
    <img id="IMG_A" name="img_main" src="imgs/tscr.jpg" width="100%"><br>
    <table style="display: inline;">
        <tr><td name="BTN"> </td><td name="BTN"> </td><td name="BTN"> </td></tr>
        <tr><td name="BTN"> </td><td name="BTN"> </td><td name="BTN"> </td></tr>
        <tr><td name="BTN"> </td><td name="BTN"> </td><td name="BTN"> </td></tr>
    </table>
    <p ><span id="MESSAGE"></span><br>
    [+GET]で自動更新が止めて、指定ファイルの写真あれば閲覧、なければ撮影して閲覧します。
        自動更新を再び実行させる場合は、左端の「現在」のボタンを使います。
    </p>
    <div>
</body>
</html>

ロボット制御用のボタンをクリックすると、背景を黄色にしてからOnButtonProc関数が実行されます。
この関数でAdjxの技術で、パスワードとボタンのidのコマンド文字列が、HTTPでPOST送信されます。 このHTTP応答ステータスが正常の200番であれば、ボタンの背景色が元の色にもどされます。
このPOST送信は、"/cgi-bin/ume.py"のサーバ側プログラムに伝達されて、そこでUSBを介して[UMEHOSHI ITA ]にコマンドが送らます。
なお、パスワード『abc123』は、下記に示すume.pyに記述されています。

/home/pi/umehoshi/cgi-bin/に配置するume.pyとumeutil.py

ume.pyのソースを以下に示します。これが上記のindex.htm内から呼び出されるCGIです。
よってこのファイルは配置後に、『sudo chmod +w ume.py』のコマンド操作で実行権限を与えてください。

#!/usr/bin/python3  # ume.py
# -*- coding: utf-8 -*-
import serial
import cgi
import os
import sys
import io
import cgitb
cgitb.enable()

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') # print命令のエンコードを指定
print("Content-Type: text/html; charset=utf-8\n\n")

import serial
cmd="X"
rtn="None"
try:
    serial = serial.Serial(port ='/dev/ttyACM0' , baudrate = 115200,timeout = 1)

    import umeutil

    # フォームから値を取得
    form = cgi.FieldStorage()
    pwd = ""
    if 'PWD' in form:
        pwd = form['PWD'].value 
    if 'CMD' in form and pwd == "abc123":
        cmd = form['CMD'].value
        if cmd=='Forward':
            rtn=umeutil.send_cmd(serial, "R0080005200005F")
            rtn=str(rtn)
        elif cmd=='Backword':
            rtn=umeutil.send_cmd(serial, "R0080005300005E")
            rtn=str(rtn)
        elif cmd=='Left':
            rtn=umeutil.send_cmd(serial, "R0080005500005C")
            rtn=str(rtn)
        elif cmd=='Right':
            rtn=umeutil.send_cmd(serial, "R0080005400005D")
            rtn=str(rtn)
        elif cmd=='UL':
            rtn=umeutil.send_cmd(serial, "R0080005600005B")
            rtn=str(rtn)
        elif cmd=='DL':
            rtn=umeutil.send_cmd(serial, "R0080005700005A")
            rtn=str(rtn)
        elif cmd=='UR':
            rtn=umeutil.send_cmd(serial, "R00800058000059")
            rtn=str(rtn)
        elif cmd=='DR':
            rtn=umeutil.send_cmd(serial, "R00800059000058")
            rtn=str(rtn)
        elif cmd=='Stop':
            rtn=umeutil.send_cmd(serial, "R0080005A000050")
            rtn=str(rtn)
        elif cmd[:1]=='R' or cmd[:1]=='S' or cmd[:1]=='G': 
            rtn=umeutil.send_cmd(serial, cmd)
            rtn=str(rtn)
        else: # cmd=='Beep'
            rtn=umeutil.send_cmd(serial, "R0080005D00004D")
            rtn=str(rtn)
        #
        serial.close()
except Exception as e:
    print(e)

print(cmd + ":" + rtn)

上記ではindex.htmから送信される『CMD=Forward&PWD=abc123』のような文字列を受信して、 CMDに対する文字列からコマンドを判定して、対応する「UME専用Hexコマンド」を[UMEHOSHI IT ]に USB経由で送信します。
このUSB送出に関するユーティリティモジュールの「umeutil.py」インポートしています。
以下に、この「umeutil.py」のソースを示します。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

def check_sum(s):
    ''' b = check_sum("G10800090000067") のように使う。 '''
    ck = 0
    n = len(s)
    if n == 0: return
    for i in range(n-2):
        ck += ord(s[i])
        # print(ck)
        # print(s[i] , end ="")
    # print(' ',s[-2:] , end ="")
    ck += int(s[-2:] , 16)
    ck &= 0x0f
    if ck != 0 :
        print("check sum error", ck)
        return False
    return True

def usb_send(serial, s, endstr=b"\n",quantity=2):
    '''  b = usb_send("G10800090000067", endstr=b"\n",quantity=1):
    sを送信して、受信したバイナリを返す。タイムアウト時は""を返す
    endstrがquantity個受信した時点でリターンする。'''
    if check_sum(s) == False: return b""
    serial.write(s.encode('utf-8'))
    serial.write(b'\r\n')
    rtnval=b""
    while True:
        b=serial.readline()
        if b == "": return ""
        rtnval+=b
        n=b.count(endstr)
        quantity-=n
        if quantity <= 0:
            return rtnval

def send_cmd(serial, ss): # ssの改行で区切られたコマンドを一括実行する。
    a = ss.split('\n')
    for s in a:
        s=s.strip()
        if s == "" : continue
        b = usb_send(serial, s)
        print(b)

以上のCGIを動作させるためには、あらかじめ[UMEHOSHI ITA]の初期プログラムの
「"datas/app_pwm_pizero.c.hex"」をロードしておく必要があります。
よってそれを行うWebサーバのmyweb.pyは、こちらでで示したコードで、 下記部分のコメント部を外してコードを有効にします。

#------------------------ 「CGIで[UMEHOSHI ITA ]を制御する際の初期転送」--ここから
#import umeusb
#umeusb.init_sub()
#start_path = "datas/app_pwm_pizero.c.hex"
#umeusb.send_cmdfile(start_path)
#umeusb.send_cmd("R00800050000061")
#umeusb.usb.close()
#------------------------ 「CGIで[UMEHOSHI ITA ]を制御する際の初期転送」--ここまで

このコメントを外して、「sudo python3 myweb.py」と実行することで、Webサーバの単独起動ができます。
(sudo systemctl disable umehoshi.serviceの状態でシステムを起動して実行してください。)
これで、CGIによるロボット制御ができるでしょう。

さてUSBのリソースのserialを、ume.pyの中でオープンしてCGIでUSB送受信をした後ににするclose()しています。
ですからCGIによるume.pyが起動される前に、オープンして"app_pwm_pizero.c.hex"を送信してclose()させているのですが、 それを行うのが、上記の部分というわけです。


myweb.pyのWebサーバも自動起動させる構築変更

最終的に、このWebサーバが自動的に起動して、以前にあった 「umehoshiアプリもどき」のumehoshi.serviceも利用できるように変更します。
上記で示したmyweb.pyのWebサーバの単独起動では、umehoshi.serviceを止めて行っていました。
それは、umehoshi.serviceの中で既に、USBやカメラを使っているからです。
これらデバイスはそれぞれのプロセスで同時に使えないため、一方使う場合にもう一方を止める必要があったからです。

結論として、 「umehoshiアプリもどき」と「上記Webサーバ」それぞれをサービスにする方法もありますが、 一方使う場合にもう一方を止める操作をするのが大変なので、 「umehoshiアプリもどき(umehoshi.py)」の中に、「上記Webサーバ」を組み込むことにしました。
これは、始めにWebサーバを起動させて、それを終了させると「umehoshiアプリ」のサーバが動作するようにします。
(実はそうすることを考慮して、「http://192.168.100.1/quit.jpg」のアクセスで、 Webサーバ(myweb.py)のプログラムが終了するように作っていたのです。)
(「上記Webサーバ」と「umehoshi」を同時に使うことはなく、「上記Webサーバ」の停止が簡単にできそうなのでこのようにしました。)

以上の仕組みを実現するために、umehoshi.serviceのumehoshi.pyで myweb.pyをインポートで起動させます。
なお、"app_pwm_pizero.c.hex"のコードは、umehoshi.pyでも使っています。
同じ[UMEHOSHI ITA]用の初期プログラムをロードしています。 ですからmyweb.pyの上記で示すロード部分は、コメント化戻しておきます。
そして、umehoshi.pyのコードはmyweb.pyのインポートに実行中にUSBやカメラを共有しないように 一時的に閉じてwebサーバ終了後に再び開くコードを追加します。
最終的に、umehoshi.pyは次のコードとなりました。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#   サーバ側「sudo python3 umehosi.py」で動かす
import os
import umetcp
from umetcp import send_file, send_message, recieve_file, recieve_message, \
   receiveData , path_datas
import socket
import umeusb
import sys
import time

sock = None # クライアント側のソケット

os.chdir("/home/pi/umehoshi/")
ip = "192.168.0.123" #[Raspberry PI Zero WH]の設定に合わせてください
#ip = "192.168.100.1"
if ip == "192.168.100.1":
    time.sleep(60) # 60秒待機

args=sys.argv

umeusb.init_sub()

start_path = path_datas + "/" + "app_pwm_pizero.c.hex"

if os.path.isfile( start_path ) == True:
    umeusb.send_cmdfile(start_path)
    umeusb.send_cmd("R00800050000061")

#======================= myweb start
umetcp.camera.close()
umeusb.usb.close()
import myweb # ここで、Webサーバを起動して、サーバを使い終わたら、改めてデバイスを初期化して使う。
umeusb.init_sub()
import picamera
umetcp.camera = picamera.PiCamera() # ★
#======================= myweb end


def my_usb_receive_func(bin):
    'usb受信のイベントで実行するデフォルト処理'
    global sock
    #print("-----------",bin)
    ss = bin.decode('utf-8')
    a=ss.split('\n')
    for s in a:
        s = s.strip()
        if sock != None:
            send_message(sock, s) # [UMEHOSHI ITA]からの応答メッセージをTCPで返す
        else: print( s )

umeusb.usb_receive_func = my_usb_receive_func # USB受信データの処理を置き換える。

def my_tcp_receive_file_func(filename): 
    ''' 受信したumehoshiアプリ用「.umh」のファイルより、
     「UME専用Hexコマンド」の文字列をusbへ出力する'''
    name,ext = os.path.splitext(filename)
    if not ext == ".umh" : return
    with open(path_datas + "/" + filename) as f: ss=f.readlines()
    ss="".join(ss)
    #print(ss)
    umeusb.send_cmd(ss) # TCPで受信した 「.umh」データを[UMEHOSHI ITA]へ送る

umetcp.tcp_receive_file_func = my_tcp_receive_file_func # tcp受信データの処理を置き換え

portnumber=59154
hostname=socket.gethostname()

if len(args) >= 2: ip = args[1] # 引数でIPアドレス指定があれば使う
server_addr =(ip, portnumber)
print("Serverの情報:",hostname, server_addr)
serversock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversock.bind(server_addr)  # IPとポート番号を指定します
print("接続要求を待つ")
serversock.listen()
while True:
   print("接続を待って、接続してきたら許可")
   sock, address = serversock.accept()#サーバの接続受け入れ
   print("接続相手:",address)
   try:
      receiveData( sock ) # 受信ループ
   except Exception as e:
      print(e, "相手が閉じた?")
   sock.close()

上記でインポートしているumeusb.pyやumetcp.pyのソースは、 「umehoshiアプリ」で遠隔操作で使ったコードをそのまま使います。
後は、「sudo systemctl enable umehoshi.service 」でサービスの自動起動を有効化して、再起動すれば使えます

起動後は、SSID:pizero、パスプレーズ:abcd1234のWifiアクセスポイントに接続します。
そしてブラウザで「http://192.168.100.1/index.htm」のURLで接続すれば、 ロボットを遠隔操作できます。 そして、「http://192.168.100.1/quit.jpg」とアクセスすれば、Webサーバが終了して、 「umehoshiアプリ」用のサーバがスタートそます。
その後は、59154のポート番号で「umehoshiアプリ」により接続すれば、「S」、「R」、「G」から始まる「UME専用Hexコマンド」と 写真撮影の遠隔操作が可能です。
以下にこのイメージを示します。 このClickでYoutube 紹介動画へ移動できます。