Pythonロボティクスコース レッスン 46
テーマ.12-1 Webサーバーの開発
Studuino:bitをWebサーバーとして立ち上げよう!
チャプター1
このレッスンで学ぶこと
このレッスンでは、Studuino:bitをWi-Fiのアクセスポイントにして構築したローカルネットワーク内で、Webサーバーとして動かす方法を紹介します。ローカルネットワークに接続したクライアントから届いたリクエストに応じてHTMLファイルのデータを送信するようにしてみましょう。
レッスンの後半では、Studuino:bitに内蔵されている各種センサの値を、手元にあるスマートフォンやタブレットからモニタリングできるアプリケーションを制作したWebサーバー上で実行します。
チャプター2
Studuino:bitをWi-Fiのアクセスポイントとして起動する方法
テーマ.11では、教室に設置されているWi-FiのアクセスポイントへStuduino:bitをクライアント(別名:ステーション)として接続し、インターネットから時刻や気象情報などを取得しました。Studuino:bitはWi-Fiのアクセスポイントとして起動させることもでき、独自のローカルエリアネットワーク(LAN)を構築することができます。
2. 1 Wi-Fiのアクセスポイントとして起動するプログラムの作成
Wi-Fiを利用するときは、network
モジュールのWLAN
クラスを使います。ステーションとしてWLAN
クラスのオブジェクトを作成するときは、コンストラクタの引数にnetwork.STA_IF
を指定し、アクセスポイントとしたいときはnetwork.AP_IF
を指定します。
network.WLAN(network.STA_IF) # ステーションとしてWLANオブジェクトを作成 network.WLAN(network.AP_IF) # アクセスポイントとしてWLANオブジェクトを作成
また、アクセスポイントとして立ち上げるためには、一般的なWi-Fiルーターと同じように、SSIDや認証パスワードなどのパラメータを設定しておく必要があります。この設定を行うためには、WLAN
クラスのconfig()
メソッドに以下の引数を渡して実行します。
【 config()メソッドの引数 】
|引数名|データ型|説明|
|:—|:—|:—|
|essid
|文字列|Wi-Fiのアクセスポイント名(SSID)|
|password
|文字列|認証が必要な場合のパスワード|
|authmode
|数値|認証モード|
|channel
|数値|使用するWi-Fiのチャンネル|
|hidden
|ブール値|essid
の表示・非表示の設定|
authmode
では、以下の番号で認証モードを設定します。これらの認証モードは「 WEP < WPA-PSK < WPA2-PSK 」の順に、よりセキュリティが強化されています。「0:認証なし」を設定した場合を除き、パスワード(password
)が必要になります。
- 0:認証なし
- 1:WEP
- 2:WPA-PSK
- 3:WPA2-PSK
- 4:WPA/WPA2-PSK
channel
には、1~13の範囲でWi-Fiのチャンネルを指定します。Wi-Fiは電波どうしの干渉による通信速度の低下を防ぐために、2.4GHz帯や5GHz帯などWi-Fiに割り当てられた帯域幅をさらに複数のチャンネルで分割して利用しています。例えば、Studuino:bitをWi-Fiのアクセスポイントとして起動したときは「IEEE802.11g」という規格に従って無線通信が行われており、この規格では1ch~13chで2.4GHzの帯域幅が分割されています。各チャンネルは中心となる周波数から両側に11MHzの幅を使って通信を行います。
※ 2.4GHz = 2400MHz
hidden
ではSSIDの表示/非表示が設定でき、True
を指定すると、スマートフォンやタブレットから利用可能なWi-Fiのアクセスポイントを検索しても一覧に表示されなくなります。
それではここまでのことを踏まえた上で、以下のようにパラメータを設定して、Studuino:bitをWi-Fiのアクセスポイントとして起動するプログラムを書きましょう。コードはlaunch_ap()
という名前の関数にまとめます。
【 config()メソッドの各引数に設定する値 】
教室で同時に複数のStuduino:bitをWi-Fiのアクセスポイントとして起動する場合は、SSIDが重ならないように名前付けをしてください。
引数 | 設定値 |
---|---|
essid | "studuinobit" もしくは好きな文字列 |
password | "Artecrobo2" もしくは好きな文字列 |
authmode | 4 (WPA/WPA2-PSK) |
channel | 11 |
hidden | False |
【 Wi-Fiのアクセスポイントとして起動するためのサンプルコード 】
import network
SSID = "studuinobit" # SSID
PASSWORD = "Artecrobo2" # パスワード
def launch_ap(ssid, password):
wlan = network.WLAN(network.AP_IF) # アクセスポイントしてWLANオブジェクトを作成
wlan.config(essid=ssid, password=password, authmode=4, channel=11, hidden=False) # パラメータの設定
wlan.active(True) # ネットワークインターフェースを有効化する
return wlan # 作成したWLANオブジェクトを返す
wlan = launch_ap(SSID, PASSWORD) # SSIDとパスワードを指定してWi-Fiのアクセスポイントを起動する
上のプログラムを実行したら、スマートフォンやタブレット端末のWi-Fi設定画面を開き、一覧に設定したSSIDが表示されていることを確認しましょう。
※ 以下はiPhone(iOS)の画面です。
そのSSIDを選択して、設定したパスワードを入力して接続を試みます。
スマートフォンやタブレットの画面上で無事に接続できたことを確認しましょう。
(※インターネット回線には接続されません)
※ スマートフォンを使用している場合に接続が安定しないときは、モバイル通信機能をOFFにすると接続が安定することがあります。
端末からの接続があると、Muのターミナルには、以下のようなメッセージが表示されます。
I (6644591) wifi: new:<1,0>, old:<1,0>, ap:<1,1>, sta:<255,255>, prof:1 I (6644591) wifi: station: 34:08:bc:7b:09:53 join, AID=1, bgn, 20 I (6644611) network: event 15 I (6644771) tcpip_adapter: softAP assign IP to station,IP is: 192.168.4.2 I (6644781) network: event 17
上のメッセージの4行目「I (6644771) tcpip_adapter: softAP assign IP to station,IP is: 192.168.4.2
」の末尾に注目すると、192.168.4.2
が接続した端末に自動的に割り当てられたIPアドレスになっています。このようにローカルエリアネットワークに接続した端末にIPアドレスなどの情報を自動で割り当てる仕組みを「DHCP(Dynamic Host Configuration Protocol)」といいます。
※最新のアンドロイド、IOSだと、違う表示がされます。
【豆知識】
DHCPとは?
DHCPとは、「Dynamic Host Configuration Protocol(ダイナミック ホスト コンフィギュレーション プロトコル)」の頭文字をとった言葉で、コンピュータがネットワークに接続して通信する際に必要な設定情報を自動的に割り当てる機能です。
コンピュータが通信を行うには、IPアドレスをコンピュータ1台1台に割り当てることによって正しく通信が行えます。しかしすべてのコンピュータに手動でIPアドレスを割り当てるのは時間や手間がかかる上に、台数が増えれば増えるほど大きな業務負担となります。
この課題を解決するのがDHCPです。
DHCPのしくみ
DHCPでは「DHCPサーバー」「DHCPクライアント」の二つのしくみにより設定情報を割り当てています。
DHCPサーバーとは、ネットワークなどに接続したいコンピュータに、IPアドレスなど必要な情報を自動的に発行するサーバーです。
DHCPクライアントとは接続しようとするコンピュータに備わる機能です。コンピュータのユーザーは接続設定で内蔵されたDHCPクライアント機能を有効にします。すると、DHCPクライアントが適切な設定を取得してくれるため、設定を手動で行う必要がなくなります。
DHCPクライアントによるIPアドレス設定の流れ
DHCPクライアントによりIPアドレスが自動的に設定されるまでの流れを解説します。
1.DHCPサーバーを探す
DHCPクライアントは、ネットワーク接続の際に、まずDHCPサーバーを探します。
2.IPアドレスの提案が返ってくる
探し当てられたDHCPサーバーはDHCPクライアントへ、IPアドレスの提案を返します。
3.DHCPサーバーにIPアドレスの払い出しを要求する
DHCPクライアントは、DHCPサーバーが提案した、IPアドレスの払い出しを要求します。
4.DHCPサーバーは払い出しを承認し、DHCPクライアントにIPアドレスを払い出す
DHCPサーバーは払い出し要求を受け取って承認した後、DHCPクライアントにIPアドレスを払い出します。これによりIPアドレス割り当てが完了します。
DHCPを利用するために準備すべきこと
DHCPはIPアドレス割り当ての利便性を高めるために必要な機能といえます。利用を進めるためには、まず次の二つの準備が必要です。
DHCPサーバーの構築
DHCPを利用するためには、DHCPサーバーの構築が必要になります。小規模な組織の場合は、ルーターのDHCP機能を利用することがありますが、大規模なオフィスなどでは、専用のDHCPサーバーやネットワーク機器を設置するケースが多くあります。
DHCPクライアントの設定
DHCPクライアントの設定を行います。例えば、PCにIPアドレスを割り振る際には、Windowsに備わるDHCPクライアントの設定を行います。ネットワークの設定で「IPアドレスを自動的に取得する」「DNSサーバーのアドレスを自動的に取得する」といった機能を利用します。
チャプター3
Webサーバーとして動作するためのプログラムの作成
Studuino:bitをアクセスポイントとして起動できたので、続いてWebサーバーとして動かしてみます。まずは下の図で全体の処理の流れを確認しておきましょう。
【 Webサーバーとして動かすときの全体の処理の流れ 】
- Studuino:bitはソケットを開き、クライアントからのアクセスを受け入れられるようにします。
- クライアント側の端末ではWebブラウザを開き、URLを指定してStuduino:bitへHTTPリクエストを送ります。
- Studuino:bitは受け取ったリクエストに応じて、内部に保管しているHTMLデータをレスポンスとしてクライアントへ送信します。
- クライアントはWebブラウザの画面上で、受け取ったHTMLデータを表示します。
サンプルで表示するHTMLデータとして、以下のページを準備しています。このページのデータをStuduino:bit内に保管しておきます。
【 Studuino:bitに保管するサンプルページ 】
3. 1 Webサーバーとして必要な機能の確認
実際のWebサーバーには、リクエストに応じてアプリケーションや画像、動画を送信したり、ユーザーの認証を行ったりするために様々な機能を備えています。それを一から構築するとなるとプログラムが複雑になってしまいますので、ここでは以下の機能だけを持つシンプルなWebサーバーとしてStuduino:bitを動かしてみましょう。
【 Studuino:bitに持たせる機能 】
- GETメソッドで送信されたリクエストを受け付けて、URLで指定されたファイルのテキストデータを返す。
- GET以外のメソッドでリクエストが送信された場合、非対応であることを表すステータスコード「405」を返す。
- リクエストされたファイルがサーバー内に存在しない場合は、発見に失敗したことを表すステータスコード「404」を返す。
3. 2 プログラムの作成
それでは、前のチャプターの続きからプログラムを作成します。以下の順番でコードを書いていきましょう。
- アドレス情報を設定してソケットを開く
- クライアントからの接続を受け入れる
- クライアントのリクエストを受信する
- クライアントへ送信するレスポンスを作成する
- クライアントへレスポンスを送る
■ アドレス情報を設定してソケットを開く
Webサーバーとして動作する処理をlaunch_webserver()
関数としてまとめていきます。まずは先頭で、引数として受け取ったWLAN
クラスのオブジェクトのifconfig()
を実行して、ソケットを開くために必要なIPアドレスを取得します。
追加【13~15行目】
import network
SSID = "studuinobit"
PASSWORD = "Artecrobo2"
def launch_ap(ssid, password):
wlan = network.WLAN(network.AP_IF)
wlan.config(essid=ssid, password=password, authmode=4, channel=11, hidden=False)
wlan.active(True)
return wlan def launch_webserver(wlan): # Webサーバーとして動作する処理をまとめた関数
ip_addr = wlan.ifconfig()[0] # WLANオブジェクトからIPアドレスを取得
print(ip_addr) # IPアドレスの表示
wlan = launch_ap(SSID, PASSWORD)
ソケットを使うため、usocket
モジュールをインポートします。このモジュールのgetaddrinfo()
メソッドを実行して、ソケット用のアドレス情報を取得します。そして、socket()
メソッドでソケットオブジェクトを作成し、bind()
メソッドで取得したアドレス情報をひもづけます。最後に、listen()
メソッドで接続を受け付けられるようにします。このメソッドの引数に指定した数だけ接続を受け付けることができますが、ここでは自分以外が接続することはないため「1」を設定しておきましょう。
追加【2行目、20~23行目】
import network
import usocket
def launch_webserver(wlan):
ip_addr = wlan.ifconfig()[0]
print(ip_addr)
sock_addr = usocket.getaddrinfo(ip_addr, 80)[0][-1] # ソケット用のアドレス情報の取得
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM) # IPv4, TCP/IP を使うソケットオブジェクトを作成
sock.bind(sock_addr) # ソケットにアドレス情報をひもづける
sock.listen(1) # 受け付け可能な接続数の設定。これを超えた場合は新しい接続を拒否する
■ クライアントからの接続を受け入れる
次はクライアントからの接続を受け入れるためのコードを書きます。
作成したソケットオブジェクトのaccept()
メソッドを実行して、クライアントの接続を受け入れます。このメソッドは接続を受け入れると、クライアントとデータを送受信するための新たなソケットオブジェクトとクライアントのIPアドレスのタプルを返します。そして、データの送受信を終えたあとは、close()
メソッドを実行して接続を解除します。受け入れは繰り返し行うため、このコードはwhile True:
の無限ループで囲みます。
追加【23~27行目】
def launch_webserver(wlan):
ip_addr = wlan.ifconfig()[0]
print(ip_addr)
sock_addr = usocket.getaddrinfo(ip_addr, 80)[0][-1]
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
sock.bind(sock_addr)
sock.listen(1)
while True:
conn, addr = sock.accept() # 接続を受け入れる。返り値はクライアントとデータを送受信するための新しいソケットオブジェクトとクライアントのIPアドレスのタプル
print("A client connected from {}.".format(addr)) # デバッグ用にクライアントのIPアドレスを表示する
# この間にソケットを用いてデータを送受信する処理を書きます。
conn.close() # 接続を解除する
ここまでの動作を確認します。プログラムの最後の行に、作成した関数を実行するコードを追加しましょう。
追加【30行目】
wlan = launch_ap(SSID, PASSWORD)
launch_webserver(wlan) # 作成した関数を実行
一度Studuino:bitのリセットボタンを押してから、プログラムを実行します。
(実行結果の例)
I (5810) phy: phy_version: 4007, 9c6b43b, Jan 11 2019, 16:45:07, 0, 0 192.168.4.1
ターミナルに表示された数字の並びがStuduino:bitのIPアドレスになります。
続けて、上で説明した順番でスマートフォンやタブレット端末をStuduino:bitのアクセスポイントへ接続して、ブラウザを立ち上げましょう。
立ち上げたブラウザのURLの入力欄に、以下のURLを指定します。
http://192.168.4.1
これでHTTPで「192.168.4.1」のIPアドレスをもつサーバーにリクエストを送信することができます。ブラウザから上記のURLにアクセスすると、ターミナルに次のようなメッセージが表示され、ソケットの接続に成功したことが分かります。
(実行結果の例)
A client connected from ('192.168.4.2', 54720). A client connected from ('192.168.4.2', 54721). A client connected from ('192.168.4.2', 54722).
ブラウザから連続して数回接続が試みられていますが、これはサーバーであるStuduino:bitからのレスポンスがなかったため、ブラウザが同じリクエストを自動的に再送した結果です。
■ クライアントのリクエストを受信する
ソケットの接続が確認できたので、次はソケットを通してクライアントから送られてきたリクエストデータを受信します。
データの受信には、ソケットオブジェクトのread()
メソッドやrecv()
メソッドを使います。
【 データを受信するためのソケットオブジェクトのメソッド 】
メソッド名 | 説明 |
---|---|
recv(bufsize) | ソケットからbufsize を最大量として、データを受信します。返り値は受信したデータを表すバイト列オブジェクトです。 |
recvfrom(bufsize) | recv() メソッドと同様に、ソケットからbufsize を最大量として、データを受信します。戻り値は受信したデータを表すバイト列オブジェクトと接続先のIPアドレスのタプルです。 |
read(size) | ソケットからsize で指定されたバイト数だけデータを読み込みます。戻り値は読み込んだデータを表すバイト列オブジェクトです。size が指定されなかった場合は、ソケットから終端までデータを読み込みます。 |
readinto(buf,[,nbytes]) | ソケットから読み込んだデータのバイト列をbuf に格納します。nbyte が指定された場合は、最大でそのバイト数だけデータを読み込みます。 |
readline() | ソケットから改行文字で終わる1行を読み込みます。戻り値は読み込んだ行を表すバイト列オブジェクトです。 |
ここではrecv()
メソッドを使って、データを100バイトずつソケットから受信してみましょう。
ソケットからリクエストデータを受信する処理をget_request()
関数としてまとめます。この関数では引数として接続が確立されたソケットオブジェクトを受け取ります。
また、今回のケースではrecv()
メソッドを実行すると、データが受信できるまで次の処理の実行を待機します。そのため、もしもデータの受信途中に接続が切れてしまった場合は、そこで待機状態がずっと続くことになってしまいます。
これでは、次のクライアントからの接続を受け入れることができなくなってしまうため、settiomeout()
メソッドを使って一定時間が経過すると自動で処理が中断されるようにします。ここではその時間を3秒に設定しておきましょう。
制限時間を超えた場合は、例外としてOSError
が返されます。この例外を捕捉したときは、その時点で関数の処理を中断するようにします。
ここまでのことを踏まえて、以下のようにコードを書きましょう。
追加【29行目~42行目】
def launch_webserver(wlan):
ip_addr = wlan.ifconfig()[0]
print(ip_addr)
sock_addr = usocket.getaddrinfo(ip_addr, 80)[0][-1]
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
sock.bind(sock_addr)
sock.listen(1)
while True:
conn, addr = sock.accept()
print("A client connected from {}.".format(addr))
conn.close()
def get_request(conn):
conn.settimeout(3.0) # 制限時間を3秒に設定
req = bytes() # リクエストを格納するためのバイト列オブジェクト
try:
while True: # 100バイトずつデータを受信する処理
data = conn.recv(100) # 100バイト分のデータを受信
req += data # 受信したデータを末尾に追加
if len(data) < 100: # 受信データが100バイトを下回った場合、
break # データの終端に到達したと判断してループ処理を終える
except OSError: # 例外を補足した場合
print("Time out.")
return False # 失敗を表すためにFalseを返す。
else: # 例外が発生しなかった場合
return req.decode("utf-8") # バイト列から文字列へデコードして返す
実際にプログラムを実行して、クライアントからのリクエストが受信できているかどうかを確認します。launch_webserver()
関数内に作成したget_request()
関数を実行する以下のコードを追加しましょう。
追加【26行目~27行目】
def launch_webserver(wlan):
ip_addr = wlan.ifconfig()[0]
print(ip_addr)
sock_addr = usocket.getaddrinfo(ip_addr, 80)[0][-1]
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
sock.bind(sock_addr)
sock.listen(1)
while True:
conn, addr = sock.accept()
print("A client connected from {}.".format(addr))
req = get_request(conn) # 定義した関数を実行してリクエストデータを取得
print(req) # デバッグ用にリクエストデータを表示
conn.close()
一度Studuino:bitのリセットボタンを押してから、プログラムを実行します。先ほどと同じ手順でStuduino:bitのアクセスポイントに接続し、ブラウザでURLに「http://192.168.4.1
」を指定して、リクエストを送りましょう。Studuino:bitがリクエストを受信すると、以下のようなメッセージがターミナルに表示されます。
(実行結果の例)
GET / HTTP/1.1 Host: 192.168.4.1 Upgrade-Insecure-Requests: 1 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1 Accept-Language: ja-jp Accept-Encoding: gzip, deflate Connection: keep-alive
以前のテーマで、HTTPのリクエストは次のような構造になっていることを説明しました。
つまり、実行例の1行目のGET / HTTP/1.1
はリクエスト行です。リクエスト行には「メソッド名」「パス名またはURL」「HTTPのバージョン」の3つの情報が半角スペース区切りで示されています。
2行目以下はヘッダです。実行例の各ヘッダの意味は次の通りです。
ヘッダ | 意味 |
---|---|
Host | リクエスト送信先のサーバーのホスト名とポート番号を指定します。ポート番号が省略されている場合は要求したサービスで既定のポート番号(HTTPの場合は80、HTTPSの場合は443)とみなされます。 |
Upgrade-Insecure-Requests | HTTPで指定したURLでもセキュリティで保護されたHTTPSで代替可能な場合は、クライアント側でもHTTPSでURLを指定したものとして正常に処理できることをサーバーに伝えます。 |
Accept | クライアントが受け入れられるコンテンツの種類をMIMEタイプでサーバーに伝えます。例えば、text/html はHTMLファイルを示しています。 |
User-Agent | クライアントが使用しているブラウザの種類やバージョンなどの情報をサーバーに伝えます。 |
Accept-Language | クライアント側が受信可能な言語をサーバーに伝えます。 |
Accept-Encoding | クライアント側でデコードが可能なエンコーディングの種類をサーバーに伝えます。 |
Connection | 現在のトランザクション(1回のリクエストの送信からレスポンスの受信までの処理)が完了したあとも接続を開いたままにするかどうかを制御します。keep-alive は接続を維持したいという意思を表します。 |
GETメソッドの場合、リクエストにボディはありません。POSTメソッドやPUTメソッドではボディがあります。
Webサーバーはこれらの情報を受けて、クライアントへ送信するレスポンスのデータを用意します。
■ クライアントへ送信するレスポンスを作成する
それでは、クライアントへ送信するレスポンスのデータを作成するコードを書いていきます。ここではサンプルとして、以下のシンプルなWebページのデータを送信できるようにしてみましょう。
まずは、Studuino:bitに以下のソースコードをもつHTMLファイルを保存します。Muエディタで「新規」を選択し、以下のコードを複製して貼り付けましょう。
※ 以下のソースコードはHTMLとCSS、JavaScriptの3種類の言語で書かれています。これらの言語の学習は本講座の対象ではありませんので、説明は割愛します。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Studuino:bit</title>
</head>
<!-- CSSで書かれた文書スタイル -->
<style>
body{
width:100%;
height:100%;
background-color: #000000;
font-family: Verdana, Geneva, "sans-serif";
}
h1#title{
font-size: 2em;
line-height: 3em;
margin-top: 2em;
text-align: center;
color: #ffffff;
}
</style>
<!-- ここまで -->
<!-- HTMLで書かれた文書の内容 -->
<body>
<h1 id="title">Welcome to Studuino:bit World!</h1>
</body>
<!-- ここまで -->
<!-- JavaScriptで書かれたプログラム -->
<script>
window.onload = function(){
let elm_title = document.getElementById("title")
let text = "Welcome to Studuino:bit World!";
let count = 0;
function scroll(){
count += 1
elm_title.innerHTML = text.slice(0, count);
if (count != text.length){
setTimeout(scroll, 250);
}else{
count = 0;
setTimeout(scroll, 2000);
}
}
scroll();
}
</script>
<!-- ここまで -->
</html>
貼り付けたソースコードを保存します。Muエディタのメニューから「保存」を選択しましょう。表示されたウィンドウで「index.html
」と名前を入力し、ファイルの種類で「Ohter(*.*)
」を選択します。ファイルの種類で「Python(*.py)
」を選択すると、Pythonのプログラムとして保存されてしまいますので注意しましょう。
保存先として「mu_code
」を選択できたら「保存(S)」ボタンをクリックして保存しましょう。
※ htmlファイルの拡張子は「.html」です。
続けて保存したファイルをStuduino:bitに転送します。Muエディタのメニューから「ファイル」を選択して「index.html
」を転送しましょう。
クライアントからこのHTMLファイルを要求するときは、以下のURLを指定します。
http://192.168.4.1/index.html
では、上記のURLでリクエストが送られてきたときに、このファイルのデータを含むレスポンスを作成するコードを書いていきます。このコードはmake_response()
関数としてまとめましょう。
上記の関数は引数として、リクエストのデータを受け取ります。このリクエストのデータか、最初にメソッド名を確認します。
メソッド名は1行目のリクエスト行に含まれています。リクエスト行は情報が半角スペースで区切られているため、先頭から最初に半角スペースが現れる位置のひとつ手前までの文字列を取り出すことでメソッド名が得られます。
この処理を行うために、文字列オブジェクトのスライス操作とfind()
メソッドを利用します。find()
メソッドは引数に指定した文字列を検索し、最初にヒットした位置を返します。そのため、半角スペースを指定して、_str[:_str.find(" ")]
とすることで、先頭から最初に半角スペースが現れた位置の手前までを取り出すことができます。
追加【47・48行目】
def get_request(conn):
conn.settimeout(3.0)
req = bytes()
try:
while True:
data = conn.recv(100)
req += data
if len(data) < 100:
break
except OSError:
print("Time out.")
return False
else:
return req.decode("utf-8")
def make_response(req):
method_name = req[:req.find(" ")]
このWebサーバーでは、GETメソッドで送られたリクエストだけを受け入れます。
※ 実際は、GETの他にHEADもWebサーバーが必ず受け入れなければならないメソッドとして定められています。
それ以外のメソッドでリクエストが送られた場合は、対応していないことを示すステータスコード「405 Method Not Allowed
」を返します。
レスポンスは上記のステータスコードを含む、ステータス行に加えて、レスポンスヘッダとレスポンスボディで構成されています。
ここでは、レスポンスのヘッダに以下の情報を含めます。
ヘッダ | 説明 |
---|---|
Content-Encoding | コンテンツを圧縮して転送するときのエンコーディング方式を伝えます。 |
Content-Length | ボディに格納したコンテンツの長さをバイト単位で伝えます。 |
Content-Type | MIMEタイプで表したコンテンツの種類と文字エンコーディング方式を伝えます。 |
Server | サーバーで使用されているソフトウェアの情報を伝えます。 |
Content-Encoding:identity Content-Length: 実際のボディの長さ Content-Type: text/plain; charset=UTF-8 Server: MicroPython
Content-Encoding
の値「identity
」は、コンテンツを圧縮していないことを表します。Content-Length
の値はプログラム内でバイト列化したボディの長さを調べて設定します。また、Content-Type
の値「text/plain
」はコンテンツがテキスト(普通の文章)であることを表します。そして、Studuino:bitはMicroPythonで制御しているので、Server
の値には「MicroPython
」を設定します。
レスポンスのボディには、リクエストで指定されたメソッドを受け入れできないことを伝えるためのメッセージを入れておきましょう。
以上のことを踏まえて以下のようにコードを書きます。ここでは、status
、body
、header
に分けて変数に格納しておき、最後に3つを結合します。また、ヘッダは辞書型のオブジェクトとして定義しておきましょう。
追加【50~59行目】
def make_response(req):
method_name = req[:req.find(" ")]
if method_name != "GET": # GETメソッド以外の場合
status = "HTTP/1.1 405 Method Not Allowed" # ステータス行に405を指定
body = "{} method is unacceptable on this server.".format(method_name) # レスポンスのボディにそのメソッドに対応していないことを表すメッセージを表示
header = {
"Content-Encoding": "identity", # コンテンツの圧縮方式
"Content-Length": str(len(body.encode("utf-8"))), # レスポンスボディの長さ
"Content-Type": "text/plain; charset=UTF-8", # レスポンスボディのコンテンツの種類と文字コード
"Server": "MicroPython", # サーバーのソフトウェア情報
else: # 以下にGETメソッドだった場合の処理を書く
pass
続けて、GETメソッドのリクエストを受信した場合のコードを書いていきます。GETはリクエスト行で指定されているURL(またはパス名)のファイルの送信を要求するメソッドのため、サーバー内にそのファイルが存在している場合は、それを送信します。もしファイルが存在しなかった場合は、「404 Not Found
」のステータスコードを送信します。
まずは、リクエストのデータからURLを取り出すコードを以下のように書きます。手順は最初の改行までをリクエスト行として取り出して、さらにfind()
メソッドとrfind()
メソッドで1番目の半角スペースと2番目の半角スペースの位置を調べて取り出すというものです。rfind()
メソッドは、find()
メソッドと同じ検索を末尾から行います。
変更・追加【60~67行目】
def make_response(req):
method_name = req[:req.find(" ")]
if method_name != "GET":
status = "HTTP/1.1 405 Method Not Allowed"
body = "{} method is unacceptable on this server.".format(method_name)
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8",
"Server": "MicroPython"
}
else:
req_line = req[:req.find("\n")] # リクエスト行を取り出す
url = req_line[req_line.find(" ")+1:req_line.rfind(" ")] # URLを取り出す
また、URLは、「http://~
」から書かれた絶対パスと、ホスト名以降のパスのみが書かれた相対パスのいずれかで示されています。ここでは、相対パスで書かれたURLを取得したいので、絶対パスの場合を想定して、http://192.168.4.1
の文字列を削除しておきます。
else:
req_line = req[:req.find("\n")]
url = req_line[req_line.find(" ")+1:req_line.rfind(" ")]
path = url.replace("http://192.168.4.1", "")
次にStuduino:bit内にこのパス名で示されたファイルが存在しているかどうかを調べます。これには、例外処理を使います。try
文の中でパスを指定してファイルを開くコードを実行すると、ファイルが存在しなかった場合は、例外としてOSError
が発生します。これをexcept
文で捕捉します。
変更・追加【60~67行目】
else:
req_line = req[:req.find("\n")]
url = req_line[req_line.find(" ")+1:req_line.rfind(" ")]
path = url.replace("http://192.168.4.1", "")
try:
file = open(path, "rt")
except OSError:
# ここにファイルが存在しなかった場合の処理を書きます。
else:
# ここにファイルが存在した場合の処理を書きます。
ファイルが存在しなかった場合は、「404 Not Found
」のステータスコードと、存在しなかったこを示すメッセージを送信します。ファイルが存在した場合は、ファイルから読み込んだテキストをボディとして格納して、成功を表すステータスコード「200 OK
」を送信します。それぞれボディに格納するコンテンツの種類が異なる点に注意して、以下のようにコードを書きましょう。
追加【66~83行目】
else:
req_line = req[:req.find("\n")]
url = req_line[req_line.find(" ")+1:req_line.rfind(" ")]
path = url.replace("http://192.168.4.1", "")
try:
file = open(path, "rt")
except OSError:
status = "HTTP/1.1 404 Not Found" # ファイルが存在しなった場合のステータスコード
body = "The requested document was not found on this server." # ファイルが存在しなったことを示すメッセージ
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8", # コンテンツはテキスト
"Server": "MicroPython"
}
else:
status = "HTTP/1.1 200 OK" # 成功を示すステータスコード
body = file.read() # ボディはファイルの中身のテキスト
file.close() # 忘れずに開いたファイルを閉じる
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/html; charset=UTF-8", # コンテンツはHTMLファイル
"Server": "MicroPython"
}
これで、「GET以外でリクエストが送られたとき」「GETでリクエストされたファイルが存在しなかったとき」「GETでリクエストされたファイルが存在したとき」の3つの場合におけるレスポンスの「ステータス行」「ヘッダ」「ボディ」が用意できたので、最後に結合します。ヘッダとボディの間には空白が1行必要な点に注意してください。データはバイト列化して送る必要があるため、encode()
メソッドを使いutf-8
でエンコードしましょう。
追加【85~90行目】
def make_response(req):
method_name = req[:req.find(" ")]
if method_name != "GET":
status = "HTTP/1.1 405 Method Not Allowed"
body = "{} method is unacceptable on this server.".format(method_name)
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8",
"Server": "MicroPython"
}
else:
req_line = req[:req.find("\n")]
url = req_line[req_line.find(" ")+1:req_line.rfind(" ")]
path = url.replace("http://192.168.4.1", "")
try:
file = open(path, "rt")
except OSError:
status = "HTTP/1.1 404 Not Found"
body = "The requested document was not found on this server."
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8",
"Server": "MicroPython"
}
else:
status = "HTTP/1.1 200 OK"
body = file.read()
file.close()
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/html; charset=UTF-8",
"Server": "MicroPython"
}
res = status + "\n" # ステータス行の結合
for key, val in header.items(): # ヘッダの結合
res += "{}: {}\n".format(key, val) # 辞書型からヘッダ形式の文字列に変換
res += "\n" + body # ボディの結合
res = res.encode("utf-8") # バイト列化
return res # 作成したレスポンスのデータを返す
■ クライアントへレスポンスを送信する
いよいよレスポンスをクライアントへ送信します。クライアントへデータを送信するときは、ソケットオブジェクトのsend()
メソッドやwrite()
メソッドを使います。
【 データを送信するためのソケットオブジェクトのメソッド 】
メソッド名 | 説明 |
---|---|
send(bytes) | 指定されたバイト列オブジェクト(bytes )のデータをソケットに送信します。このメソッドは、戻り値として送信に成功したデータのバイト数を返します。ただし、指定されたデータが大きい場合、全てのデータが送信できず、戻り値が実際のデータの長さよりも短くなる場合があります。 |
sendall(bytes) | 指定されたバイト列オブジェクト(bytes )のすべてのデータをソケットに送信します。このメソッドは、send() メソッドと違い、データ量が大きい場合は、分割して連続して送ることによって、すべてのデータを送信しようとします。 |
write(bytes) | sendall() と同様に、指定されたバイト列オブジェクト(bytes )をすべてソケットに送信しようとしますが、必ずしもすべてのデータが送信できるとは限りません。返り値はソケットに送信できたデータのバイト数です。 |
細かな違いはありますが、どのメソッドを使ってもデータを送ることができます。ここでは、sendall()
メソッドを使ってデータを送信してみましょう。launch_webserver
に以下のコードを追加します。
追加【31~34行目】
def launch_webserver(wlan):
ip_addr = wlan.ifconfig()[0]
print(ip_addr)
sock_addr = usocket.getaddrinfo(ip_addr, 80)[0][-1]
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
sock.bind(sock_addr)
sock.listen(1)
while True:
conn, addr = sock.accept()
print("A client connected from {}.".format(addr))
req = get_request(conn)
print(req)
if req: # 正常にリクエストを受信できたとき
res = make_response(req) # レスポンスのデータを作成する
print(res) # デバッグ用にレスポンスの内容をターミナルに表示
conn.sendall(res) # ソケットを使ってレスポンスを送信
conn.close()
では、作成したプログラムの動作を確認しましょう。
一度Studuino:bitのリセットボタンを押してから、プログラムを実行します。Studuino:bitのアクセスポイントに接続し、ブラウザのURL欄に「http://192.168.4.1/index.html
」を入力して、リクエストを送りましょう。ブラウザ上に以下のページが表示されれば成功です。
また、誤ったURL(例えばhttp://192.168.4.1/sample.html
など)でリクエストを送ったときには、以下のようなページが表示されることも確認しておきましょう。
チャプター4
現在の内蔵センサの値を送信するプログラムの作成
前のチャプターでは、クライアントから要求されたファイルを送信する機能をもったWebサーバーのプログラムを作成しました。また、送信するファイルはあらかじめ別で作成してサーバー内に保管していたものでした。
実際のWebサーバーには、要求されたファイルを送信する以外にも、要求されたプログラムを実行して、その結果として生成されたファイルを送信する仕組みがあります。
ここでは、Studuino:bitに内蔵された「ボタンA」「ボタンB」「光センサ」「温度センサ」の4つのセンサの現在の状態を取得して、その結果をレスポンスとして送信できるようにプログラムを改造してみましょう。
4. 1 内蔵センサの値を取得するプログラムの作成
Webサーバーとして起動するプログラムとは別に、4つの内蔵センサの値を取得して、一覧にしたテキストとMIMEタイプを返す関数を定義したプログラムを作成します。
Muエディタのメニューから「新規」を選択して、以下のコードを書きましょう。
【 サンプルコード 4-1-1 】
from pystubit.board import button_a, button_b, lightsensor, temperature
def main(): # 関数の名前は「main」としておく
text = "Button A: {}, Button B: {}, LightSensor: {}, Temperature: {}" # 内蔵センサの値の一覧を示すテキスト
but_a_val = button_a.get_value() # ボタンAの値を取得
but_b_val = button_b.get_value() # ボタンBの値を取得
light_val = lightsensor.get_value() # 光センサの値を取得
temp_val = temperature.get_celsius() # 温度センサの値を取得
text = text.format(but_a_val, but_b_val, light_val, temp_val) # 4行目で定義した文字列に各センサの値を埋め込む
return text, "text/plain" # 作成したテキストとそのMIMEタイプを呼び出し元に返す
このプログラムを「monitor(.py)」と名前を付けて保存します。Muエディタのメニューから「ファイル」を選択して、保存したプログラムをStuduino:bitに転送しましょう。
このプログラムファイルをあらかじめWebサーバーのプログラム内でインポートしておき、クライアントから実行が要求されたときは、main()
関数を実行します。
4. 2 Webサーバーのプログラムの変更
プログラムの実行が要求された場合に対応するコードを追加していきます。
先頭では、Studuino:bitに転送した「monitor(.py)」をモジュールとしてインポートします。また、ファイルのURLとインポートしたモジュール名をひもづけした辞書を定義しておきましょう。
追加【3行目、8~10行目】
import network
import usocket
import monitor # 転送したプログラムファイルをインポート
SSID = "studuinobit"
PASSWORD = "Artecrobo2"
# ファイルのURLとインポートしたモジュール名をひもづけした辞書
modules = {
"/monitor.py": "monitor" # URL: モジュール名
}
次に、make_response()
関数を変更します。
ファイルが存在する場合は、ファイルの種類によって実行する処理を分けます。ファイルの拡張子が「.py」の場合(Pythonのプログラムの場合)は、関連するモジュール名を上で定義した辞書から取得し、eval()
関数を使って、そのmain()
関数を実行します。そして、その結果をレスポンスのボディとします。
ファイルの拡張子が「.html」の場合(HTMLファイルの場合)は、読み込んだテキストをレスポンスのボディとします。
変更・追加【83~97行目】
def make_response(req):
method_name = req[:req.find(" ")]
if method_name != "GET":
status = "HTTP/1.1 405 Method Not Allowed"
body = "{} method is unacceptable on this server.".format(method_name)
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8",
"Server": "MicroPython"
}
else:
req_line = req[:req.find("\n")]
url = req_line[req_line.find(" ")+1:req_line.rfind(" ")]
path = url.replace("http://192.168.4.1", "")
try:
file = open(path, "rt")
except OSError:
status = "HTTP/1.1 404 Not Found"
body = "The requested document was not found on this server."
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8",
"Server": "MicroPython"
}
else:
status = "HTTP/1.1 200 OK" # ステータス行はファイルの種類に関係なく共通
if ".py" in path: # ファイルの拡張子が「.py」の場合
file.close() # 読み込む必要がないため開いたファイルを閉じる
module = modules[path] # 関連するモジュール名の取得
body, mimetype = eval("{}.main()".format(module)) # eval()関数は文字列をコードとして解釈して実行してその結果を返す
else: # ファイルの拡張子が「.py」以外の場合(ここでは「.html」の場合)
body = file.read() # ファイルのテキストデータをレスポンスボディとする
file.close() # ファイルを閉じる
mimetype = "text/html" # HTMLファイルのMIMEタイプ
header = { # レスポンスボディの内容によってMIMEタイプが異なる
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "{}; charset=UTF-8".format(mimetype), # MIMEタイプを挿入する
"Server": "MicroPython"
}
res = status + "\n"
for key, val in header.items():
res += "{}: {}\n".format(key, val)
res += "\n" + body
res = res.encode("utf-8")
return res
これで変更が完了しました。プログラムの動作を確認しましょう。
一度Studuino:bitのリセットボタンを押してから、プログラムを実行します。Studuino:bitのアクセスポイントに接続し、ブラウザのURL欄に「http://192.168.4.1/monitor.py
」を入力して、リクエストを送りましょう。ブラウザ上に4つのセンサの値が表示されたら成功です。
完成したプログラムは名前を付けて保存しておきましょう。次回以降のテーマでもこのプログラムを使います。
【 プログラムの完成例(サンプルコード 4-2-1) 】
import network
import usocket
import monitor
SSID = "studuinobit"
PASSWORD = "Artecrobo2"
modules = {
"/monitor.py": "monitor"
}
def launch_ap(ssid, password):
wlan = network.WLAN(network.AP_IF)
wlan.config(essid=ssid, password=password, authmode=4, channel=11, hidden=False)
wlan.active(True)
return wlan
def launch_webserver(wlan):
ip_addr = wlan.ifconfig()[0]
print(ip_addr)
sock_addr = usocket.getaddrinfo(ip_addr, 80)[0][-1]
sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
sock.bind(sock_addr)
sock.listen(1)
while True:
conn, addr = sock.accept()
print("A client connected from {}.".format(addr))
req = get_request(conn)
print(req)
if req:
res = make_response(req)
print(res)
conn.sendall(res)
conn.close()
def get_request(conn):
conn.settimeout(3.0)
req = bytes()
try:
while True:
data = conn.recv(100)
req += data
if len(data) < 100:
break
except OSError:
print("Time out.")
return False
else:
return req.decode("utf-8")
def make_response(req):
method_name = req[:req.find(" ")]
if method_name != "GET":
status = "HTTP/1.1 405 Method Not Allowed"
body = "{} method is unacceptable on this server.".format(method_name)
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8",
"Server": "MicroPython"
}
else:
req_line = req[:req.find("\n")]
url = req_line[req_line.find(" ")+1:req_line.rfind(" ")]
path = url.replace("http://192.168.4.1", "")
try:
file = open(path, "rt")
except OSError:
status = "HTTP/1.1 404 Not Found"
body = "The requested document was not found on this server."
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "text/plain; charset=UTF-8",
"Server": "MicroPython"
}
else:
status = "HTTP/1.1 200 OK"
if ".py" in path:
file.close()
module = modules[path]
body, mimetype = eval("{}.main()".format(module))
else:
body = file.read()
file.close()
mimetype = "text/html"
header = {
"Content-Encoding": "identity",
"Content-Length": str(len(body.encode("utf-8"))),
"Content-Type": "{}; charset=UTF-8".format(mimetype),
"Server": "MicroPython"
}
res = status + "\n"
for key, val in header.items():
res += "{}: {}\n".format(key, val)
res += "\n" + body
res = res.encode("utf-8")
return res
wlan = launch_ap(SSID, PASSWORD)
launch_webserver(wlan)
チャプター5
課題:ページの見た目を整えよう
前のチャプターでは、取得したセンサの値をそのままテキストとしてクライアントへ送信していましたが、HTMLファイルにまとめて送信すると、以下のように表に整理された形でクライアントがセンサの値を確認できるようになります。
このチャプターでは、上のページを表示するためのベースとなるHTMLファイルのテキストから、一部を取得したセンサの値に変更して、そのファイルをクライアントへ送信するようにコードを改造する課題に取り組みましょう。
5. 1 ベースとするHTMLファイルの確認
ベースとなるHTMLファイルのソースコードは以下になります。Muエディタのメニューで「新規」を選択して、このソースコードを貼り付けましょう。保存するときは、ファイルの種類で「Other(*.*)
」を選び、ファイル名を「base.html
」としましょう。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width , initial-scale=1">
<title>Monitoring sensors</title>
<style>
body{
width: 100%;
height: 100%;
margin-right: auto;
margin-right: left;
font-family: Tahoma, Geneva, "sans-serif";
}
div#main{
position: absolute;
width: 100%;
height: 100%;
}
#main h1{
font-size: 2em;
margin: 0.5em;
}
#main table{
display: table;
border: solid 2px #666666;
border-collapse: collapse;
margin: 1em;
}
#main table td{
font-size: 1.2em;
padding:0.5em 1em;
border
}
#main table thead tr{
background-color: #666666;
}
#main table thead td{
color: #ffffff;
}
#main table tbody tr:nth-child(odd){
background-color: #eeeeee;
}
#main table tbody tr:nth-child(even){
background-color: #ffffff;
}
#main a{
display: inline-block;
margin: 1em;
}
#main a button{
font-size:1.2em;
padding: 0.5em 1em;
color: #ffffff;
background-color: #22AC38;
border: none;
border-radius: 5px;
}
</style>
</head>
<body>
<div id="main">
<h1>Sensor values</h1>
<table>
<thead>
<tr><td>Name</td><td>Value</td></tr>
</thead>
<!--ここから下の{}で囲まれた文字列を取得したセンサ値に変換します。-->
<tr><td>Button A</td><td>{val_buttonA}</td></tr>
<tr><td>Button B</td><td>{val_buttonB}</td></tr>
<tr><td>Light Sensor</td><td>{val_lightsensor}</td></tr>
<tr><td>Temperature</td><td>{val_temp}</td></tr>
<!--ここまで-->
<tbody>
</tbody>
</table>
<a href=""><button>Update</button></a>
</div>
</body>
</html>
このファイルをそのままブラウザで開くと次のような画面が表示されます。
この中の{val_buttonA}
や{val_buttonB}
の部分をそれぞれ取得したセンサ値に書き換えます。HTMLのソースコードを確認すると、この部分は70行目~73行目に書かれています。HTMLは<>
を使ったタグで文書の構造を表すようになっており、65行目の<table>
から77行目の</table>
のタグで囲まれたところが、画面上に表示された表(テーブル)の構造を定義しています。
<body>
<div id="main">
<h1>Sensor values</h1>
<table>
<thead>
<tr><td>Name</td><td>Value</td></tr>
</thead>
<!--ここから下の{}で囲まれた文字列を取得したセンサ値に変換します。-->
<tr><td>Button A</td><td>{val_buttonA}</td></tr>
<tr><td>Button B</td><td>{val_buttonB}</td></tr>
<tr><td>Light Sensor</td><td>{val_lightsensor}</td></tr>
<tr><td>Temperature</td><td>{val_temp}</td></tr>
<!--ここまで-->
<tbody>
</tbody>
</table>
<a href=""><button>Update</button></a>
</div>
</body>
それでは準備として、保存したHTMLファイル(base.html
)を転送しておきましょう。
5. 2 プログラムの改造
上で保存したHTMLファイルの一部を書き換えて送信するようにプログラムを変更します。変更するプログラムファイルは「monitor.py」です。
■ monitor.pyの追加・変更点
前のチャプターで作成した「monitor.py」へ以下の3つの処理を追加・変更します。
base.html
を開いて、テキストを読み込む。- 文字列の
replace()
メソッドを使って、読み込んだテキストの中から{val_buttonA}
、{val_buttonB}
、{val_lightsensor}
、{val_temp}
の4つの文字列を、取得したそれぞれのセンサ値に置き換える。 - 2の置き換えたテキストとMIMEタイプとして「
html/text
」を呼び出し元に返す。
以上をヒントにして自分でコードを書きましょう。手順が分からない場合は、以下の例を参考にしてください。
■ プログラムの改造例
PCに保存したmonitor.py
を開いて、以下の箇所を変更します。
追加・変更【5~7行目、14~17行目、19行目】
from pystubit.board import button_a, button_b, lightsensor, temperature
def main():
# ベースになるHTMLファイルを開いてテキストを読み込む
with open("base.html", "rt") as file: # 読み取り専用のテキストモードで開く
text = file.read()
file.close()
# 現在の各センサの値を取得。ここは変更なし
but_a_val = button_a.get_value()
but_b_val = button_b.get_value()
light_val = lightsensor.get_value()
temp_val = temperature.get_celsius()
# HTMLファイルから読み込んだテキストの一部を上で取得したセンサの値に置き換える
text = text.replace("{val_buttonA}", str(but_a_val)) # センサの値は数値のため、文字列への変換が必要な点に注意
text = text.replace("{val_buttonB}", str(but_b_val))
text = text.replace("{val_lightsensor}", str(light_val))
text = text.replace("{val_temp}", str(temp_val))
# 呼び出し元に書き換え後のテキストとMIMEタイプを返す
return text, "text/html" # MIMEタイプは「text/html」に変更
変更したプログラムを保存して、Studuino:bitに転送し、上書き保存します。
動作を確認するときは、一度Studuino:bitのリセットボタンを押してから、Webサーバーのプログラムを実行します。Studuino:bitのアクセスポイントに接続し、ブラウザのURL欄に「http://192.168.4.1/monitor.py
」を入力して、リクエストを送りましょう。
表示された画面上で「Update」と表示されたボタンをタップすると、同じURLに再度リクエストを送ることができ、センサの値が更新されます。
※ ボタンが見切れる場合は、端末の画面を縦にして表示してください。
チャプター6
おわりに
6. 1 このレッスンのまとめ
このレッスンでは、Studuino:bitをWi-Fiのアクセスポイントとして起動し、さらにWebサーバーとして動作させるためのプログラムの作り方を新たに学習しました。
実際にStuduino:bit内にHTMLファイルを保管し、アクセスポイントに接続した手元のスマートフォンやタブレットからリクエストを送信すると、そのページを開くことができました。また、クライアントからプログラムファイルの実行を要求できることも確認し、Studuino:bitに内蔵された各種センサの値をブラウザの画面に表示する課題に取り組みました。
テーマ11ではクライアントとしてStuduino:bitをネットワークに接続して、インターネットから情報を取得しましたが、このレッスンで取り組んだようにStuduino:bitをWebサーバーとすることで、反対にクライアントの要求の応じてセンサから得たデータを送ったり、モーターを制御したりすることができます。本格的にIoTを実践するときには、どちらの知識も必要になってきますので、残りのテーマの学習を通してさらに理解を深めていきましょう。
6. 2 次回のレッスンについて
次のレッスンでは、同じくStuduino:bitをWebサーバーとして動かし、今度はブラウザの画面に表示されたボタンやスライダーを操作することで、Studuino:bitを遠隔で制御できるプログラムを作成します。