プログラミングについての情報を主に自分の活動、また単なる雑談を掲載するブログです。
websocket通信でブラウザ↔︎Wi-Fi子機をリアルタイム通信システムを開発しました。

websocket通信でブラウザ↔︎Wi-Fi子機をリアルタイム通信システムを開発しました。

2022/03/22

私の家族は兄弟全員実家に住んでいるのですが、リビングで食事をするときにそれぞれの部屋にいる家族をいちいち携帯で呼び出すのが面倒だと考えこれを作るに至りました。

以下参考にさせていただいたサイト様です

このシステムのソースはgithubに一応あげていますが、Wi-Fi子機を用意しなければならない都合上作る方はいないかなと思います。また持てる知識で想定するシステムが動作するように強引に作成した部分を多々あるためその点ご容赦ください。

今回作成したシステムについて以下にハードウェア側、サーバ側それぞれを書いていきますが、その中でみなさまの参考になれるような部分があれば幸いです。

名前は「bugle」にしました。

昔軍隊で使われていた指揮官の号令を伝えるために使われていたものらしいですね。名前を何にしようかと思ったときにアニメで見たことがあって名付けました。「とりあえずこの号令があったら速やかに部屋に来いや」って想いも込めてます。

以下から実際のプログラムの中身について説明します。


・プロセスフロー図


実装に必要なハードウェアは3つです。

・ブラウザ

webサーバにアクセスできる端末(これは携帯でいいですね。)

・リアルタイム通信システム

webサーバ(通信するデータ量は非常に少ないので捨てる予定だった古いPCを今回は使います)

・Wi-Fi子機

arduinoマイコンボード(下で実際に使用したマイコンボードのリンクなど貼っています。)


プロセスフローの動きを図にしてみました。




・フォルダ構成

プログラム全体のフォルダ構成を以下に出します。githubにも載せているものになります。

bugle
├─hardware                ハードウェアに対してコンパイルするプログラミングです。
│  └─wifi
│          wifi.ino         arduinoマイコンボードによって実装しました。こちらのファイルをコンパイルします。
│
└─server                  サーバ側に設置するフォルダです。index.phpをapacheのDocummentrootとします。
    │  index.php            ホームとなるページです。
    │  style.css
    │
    ├─bin
    │      chat-server.php  サーバにて常時稼働させるwebsocket serverです。phpコマンドにて実行してください。
    │      udp-socket.php   udpにてwifi子機と通信するためのudp serverです。phpコマンドにて実行してください。
    │
    └─src
    │   └─Chat.php          bin\chat-server.phpで受信したwebsocketデータを処理するプログラムです。
    │
    └─vendor         phpライブラリであるRatchetを利用させて頂いております。ratchet(https://github.com/ratchetphp/Ratchet)

プログラムは主に2つに分かれています。

・hardwareフォルダ – Wi-Fi子機にインストールするプログラムです。

・serverフォルダ – サーバに設置するプログラムです。

この2つを分けて以下で環境構築について解説していきます。





・hardwareフォルダ(Wi-Fi子機)の環境を構築

Wi-Fi子機を作成する上で電子工作の学習キットを提供しているOSOYOOの学習キットを利用して開発しました。

使用した学習キット↓

https://osoyoo.com/ja/2020/05/30/32527/

上記の関係図では、Wi-Fi子機を独立した機器のように表現していますが、電子回路や電子基盤には全く詳しくないため電源が独立した子機を作成することはできませんでした。いずれ電子基盤を勉強して電源も独立した完全に移設可能な子機(イメージはへぇボタン的なもの)にしてみたいと思っています。

以下はブレッドボードの構築図です。

制御するハードウェアは以下の3つです。

・ボタン

7pinについているものになります。呼び出し信号を受信したときに応答するためについています。

・サウンド

9pinについているものになります。呼び出し信号を受信したときに発音されます。

・LED

13pinについているものになります。呼び出し信号を受信したときに光ります。

以下は実際の画像です。USBから先はPCに繋がっています。USBを繋いだ時点で電源は供給されます。

以下はプログラムマイコンボードにコンパイルするプログラムです。

各種関数はarduinoリファレンスに従っています。

・日本語リファレンス

https://www.arduino.cc/reference/en/

・公式リファレンス

https://www.arduino.cc/reference/en/

/*  ___   ___  ___  _   _  ___   ___   ____ ___  ____  
 * / _ \ /___)/ _ \| | | |/ _ \ / _ \ / ___) _ \|    \ 
 *| |_| |___ | |_| | |_| | |_| | |_| ( (__| |_| | | | |
 * \___/(___/ \___/ \__  |\___/ \___(_)____)___/|_|_|_|
 *                  (____/ 
 * Osoyoo Wifi IoT  lesson 2
 * Remote control LED over UDP
 * tutorial url: https://osoyoo.com/?p=10000
 */

#include "WiFiEsp.h"
#include "WiFiEspUdp.h"
#include "SoftwareSerial.h"
#define ledPin 13
#define buzzer 9  //buzzer connect to D9
#define pullupSwitch 7

static const char *kRemoteIpadr = "****";
static const int kRmoteUdpPort = 5051;
unsigned long myTime = 0;
unsigned long BugleTimeoutTimer = 0;
unsigned long CountMyTime = 0;

SoftwareSerial softserial(4, 5); // A9 to ESP_TX, A8 to ESP_RX by default
char ssid[] = "****";            // your network SSID (name)
char pass[] = "****";        // your network password
int status = WL_IDLE_STATUS;     // the Wifi radio's status
WiFiEspUDP Udp;
unsigned int localPort = 8888;              // local port to listen on
char packetBuffer[5];
 
void setup() {
  //pinMode Configration
  pinMode(ledPin,OUTPUT);
  pinMode(buzzer, OUTPUT);
  pinMode(pullupSwitch, INPUT_PULLUP);
  digitalWrite(buzzer,HIGH);

  Serial.begin(9600);   // initialize serial for debugging
    softserial.begin(115200);
  softserial.write("AT+CIOBAUD=9600\r\n");
  softserial.write("AT+RST\r\n");
  softserial.begin(9600);    // initialize serial for ESP module
  WiFi.init(&softserial);    // initialize ESP module

  // check for the presence of the shield:
  if (WiFi.status() == WL_NO_SHIELD) {
    Serial.println("WiFi shield not present");
    // don't continue:
    while (true);
  }

  // attempt to connect to WiFi network
  while ( status != WL_CONNECTED) {
    Serial.print("Attempting to connect to WPA SSID: ");
    Serial.println(ssid);
    // Connect to WPA/WPA2 network
    status = WiFi.begin(ssid, pass);
  }
  
  Serial.println("Connected to wifi");
  printWifiStatus();
 
  // if you get a connection, report back via serial:
  Udp.begin(localPort);
  
  Serial.print(" target port ");
  Serial.println(localPort);
     
}
 
void loop() {
  int pullupSwitch_value = digitalRead( pullupSwitch );//test
  int ledPin_value = digitalRead( ledPin );//test
      myTime = millis() / 1000; //秒数を格納
  int packetSize = Udp.parsePacket();
  if (packetSize) {                               // if you get a client,
     Serial.print("Received packet of size ");
    Serial.println(packetSize);
    int len = Udp.read(packetBuffer, 255);
    if (len > 0) {
      packetBuffer[len] = 0;
    }
      char c=packetBuffer[0];
      switch (c)    //serial control instructions
      {  
 
        case 'F': 
        digitalWrite(ledPin, HIGH); //TURN ON LED
        digitalWrite(buzzer,LOW); //buzzer ON
        BugleTimeoutTimer = myTime;
        break;
        
        case 'G':
        digitalWrite(ledPin, LOW); //TURN OFF LED
        digitalWrite(buzzer,HIGH);//buzzer OFF
        BugleTimeoutTimer = 0;
        CountMyTime = 0;
        break;
 
        default:break;
      }
    }

  if (BugleTimeoutTimer){
    CountMyTime = myTime;
  }

//    Serial.print("Received packet of size ");
//    Serial.println(myTime);
  
  if(pullupSwitch_value == LOW && ledPin_value == HIGH){
    Serial.print("bugle Responded. ");
    
    digitalWrite(ledPin,LOW); //TURN OFF LED
    digitalWrite(buzzer,HIGH);//buzzer OFF

    Udp.beginPacket(kRemoteIpadr,kRmoteUdpPort);
    Udp.write("STOP");
    Udp.endPacket();  
    
  }else if( (CountMyTime-BugleTimeoutTimer) >= 30 ){
    CountMyTime = BugleTimeoutTimer = 0;
    Serial.print("bugle not respond. ");
    
    digitalWrite(ledPin,LOW); //TURN OFF LED
    digitalWrite(buzzer,HIGH);//buzzer OFF

    Udp.beginPacket(kRemoteIpadr,kRmoteUdpPort);
    Udp.write("NON");
    Udp.endPacket();
    
  }
 
}

void printWifiStatus()
{
  // print the SSID of the network you're attached to
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your WiFi shield's IP address
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);
  
  // print where to go in the browser
  Serial.println();
  Serial.print("please set your UDP APP target IP to: ");
  Serial.print(ip);
 
}

ソースの一番上で3つほどファイルをincludeしているのがわかりますが、

これらはarduino IDEの開発環境でincludeしているファイルをライブラリとして参照しなければなりません。

・arduino IDE

https://www.arduino.cc/en/software

mac、windowsそれぞれあるので、必要な方を選んでダウンロードしてください。

ライブラリは以下の方法でインクルードします。

arduino IDEアプリケーションを開く ⇨ 「スケッチ」 ⇨ 「ライブラリをインクルード」 ⇨ 「zip形式ライブラリをインストール」でzipファイルを選択。

必要なライブラリは下記zipファイルとなります。zipファイルを読み込むのでファイルの解凍はしないでください。

https://osoyoo.com/driver/WiFiEsp-master.zip


aruduinoにおける重要な各種関数の役割が以下となります。

・setup()関数

ハードウェア起動時、一度だけ実行される関数。初期値をここで設定します。

・loop()関数

起動中常にループする関数。この中に起動中の受信処理などをプログラムします。


また下記変数にはあらかじめ指定の値を設定しなければなりません。

  • *kRemoteIpadr = webサーバのipアドレス
    • (webサーバのipアドレスが変わらないようにルータの設定でwebサーバ用のipアドレスを固定ipアドレスにしなければなりません。)
  • kRmoteUdpPort = webサーバのipアドレス
    • (webサーバのUDPを受信するポート番号を設定します。どのポートで受信するかは、serverフォルダ内のudp-socket.phpの$udp_portが指定しています。)
  • ssid[] = wifiルータのSSID
  • pass[] = wifiルータのpassword


上記*kRemoteIpadrで説明しているとおり、webサーバやWi-Fi子機に割り振られるIPアドレスは固定でなければなりません。基本的に家庭で導入しされるようなルータは初期設定としてDHCPによる自動IP振り分けとなっているので、webサーバやWi-Fi子機のmacアドレスに割り振られたIPアドレスをルータの設定画面にて固定にしてあげなければなりません。

※本来このようなipアドレスの設定をしなくても、システム側でノードを判別できるような構築にしなければならないのですが、自分にはそこまでの技術力がありませんでした。いつかこの辺も簡単に構築できるように修正しなければと考えています。申し訳ありません。


・マイコンへ書き込み実行

arduinoIDEアプリケーションを立ち上げてマイコンボードに対して書き込みを実行します。IDEにてwifi.inoファイルを開いてください。矢印のボタンを押すことでマイコンボードに書き込みが実行されます。

緑色で印をつけている虫眼鏡ボタンを押すことでマイコンボードの状態や送受信しているデータをモニターするウィンドウを表示できるので、書き込みが完了したら開いてみるのもいいと思います。


・モニターウィンドウ

以上でWi-Fi子機の構築は終了となります。次からはwebサーバ側の構築となります。






・serverフォルダ(webサーバ)の環境を構築

webサーバについて詳しい方は自分でDMBSやapacheの環境を構築してみてもいいかもしれませんが、私は各種システムをそれぞれ自分で構築したときにシステム間でエラー発生する可能性を危惧して、DBMSやapache、phpの環境がパーケージされたxamppを利用しました。

https://www.apachefriends.org/jp/index.html

xamppの場合apacheのDocument rootを設定しているファイルはhttpd.confになります。

ドキュメントルート直下にserverフォルダをまとめて設置していただければOKです。


・udp受信ポートの設定

serverからWi-Fi子機に対してudpを送信する処理はsever/src/Chat.phpの48行目、socket_sendto()関数で実施しています。

<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use \PDO;

class Chat implements MessageComponentInterface {
    protected $clients;
    public $send_from;
 
    public function __construct() {
        $this->clients = new \SplObjectStorage;
        $this->send_from = 0;
        $this->test = 0;
    }
 
    public function onOpen(ConnectionInterface $conn) {
        // Store the new connection to send messages to later
        $this->clients->attach($conn);
 
        echo "New connection! ({$conn->resourceId})\n";
    }
 
    public function onMessage(ConnectionInterface $from, $msg) {
        $numRecv = count($this->clients) - 1;
            
            $this->send_from = $from; //ローカル関数udpstatus_loop()用に送信用オブジェクトをストックする。
            $pieces = explode(",",$msg);//クライアントから送信されたメッセージを分解して格納する。
            
            $pdo = new PDO('mysql:host=localhost;dbname=bugle;charset=utf8','staff','password');
            $sql = $pdo->prepare('update wifi_machine set client_status=? where INET_NTOA(ip_address) = ?');
            if($pieces[1] == "F"){
                $sql->execute(["呼出要求",$pieces[0]]);//データベースに呼出要求中フラグ格納
            }else if($pieces[1] == "G"){
                $sql->execute(["取消要求",$pieces[0]]);//データベースに呼出要求中フラグ格納
            }

            foreach($pdo->query('select * from wifi_machine') as $row){
                if(long2ip($row['ip_address']) == $pieces[0] && ($row['client_status'] == "呼出要求" or $row['client_status'] == "取消要求")){
                    echo("webブラウザから " . long2ip($row['ip_address']). " に " . $row['client_status'] . PHP_EOL . PHP_EOL);
                    break;
                }
            }

        if ($pieces[1] == "F" || $pieces[1] == "G"){
            $socket = socket_create(AF_INET,SOCK_DGRAM,SOL_UDP);
            $len = strlen($pieces[1]);//クライアントからの実送信メッセージ格納
            socket_sendto($socket,$pieces[1],$len,0,$pieces[0],8888);
        }
        $pdo = null;
    }
 
    public function onClose(ConnectionInterface $conn) {
        // The connection is closed, remove it, as we can no longer send it messages
        $this->clients->detach($conn);
 
        echo "Connection {$conn->resourceId} has disconnected\n";
    }
 
    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";
 
        $conn->close();
    }

    public function udpstatus_loop(){
        $pdo =new PDO('mysql:host=localhost;dbname=bugle;charset=utf8','staff','password');
        $sql = $pdo->prepare('update wifi_machine set client_status=? where INET_NTOA(ip_address) = ?');
        foreach($pdo->query('select * from wifi_machine') as $row){
            if($row['client_status'] == "停止要求"){
                $this->send_from->send(long2ip($row['ip_address']) . " STOP" );
                $sql->execute([null,long2ip($row['ip_address'])]);
                echo("クライアントからの次操作受信開始" . PHP_EOL);
            }else if($row['client_status'] == "応答無"){
                $this->send_from->send(long2ip($row['ip_address']) . " NON" );
                $sql->execute([null,long2ip($row['ip_address'])]);
                echo("クライアントからの次操作受信開始" . PHP_EOL);
            }
        }
        $pdo = null;
    }

}


それに対してWi-Fiからudpを受信する処理はserver/bin/udp-socket.phpの20行目、socket_recvfrom()関数にて実施しています。13行目のsocket_bind()関数では第2引数にて受信するipアドレス、第3引数にて受信するポート番号を確定しています。

第2引数のipアドレスはwebサーバのIPアドレスを設定してください。(こちらのIPアドレスもルータにて固定化してください。)

第3引数のポート番号は今回5051 を使用していますが、空いているポートであればお好きなもので構いません。

<?php

function udpsocket_loop(){
        //ここでデータベースの値であるブラウザからの送信状態を読み取って、送信をしている場合処理実行

	    $pdo =new PDO('mysql:host=localhost;dbname=bugle;charset=utf8','staff','password');
		
	    error_reporting(E_ALL | E_STRICT);
	    $udp_port = 5051;
   
	    //UDPのソケット作成
	    $udp_socket = socket_create(AF_INET,SOCK_DGRAM,SOL_UDP);
	    socket_bind($udp_socket,'****',$udp_port);

	//socket_set_nonblock($udp_socket);//socket_recvfrom()にてノンブロッキングでudp受信
	    echo("データ受信開始 : ");
	    while(true){

		    //socket_recvfrom()にてUDPのソケットを受信
		    socket_recvfrom($udp_socket,$udp_buf,50,0,$wifi_from,$wifi_port);
			echo "リモートアドレス $wifi_from のポート $wifi_port から $udp_buf を受信" . PHP_EOL;

			$sql = $pdo->prepare('update wifi_machine set client_status=? where INET_NTOA(ip_address) = ?');
			// $sql->execute(["呼出停止",$wifi_from]);//データベースに呼出停止フラグ格納
		    foreach($pdo->query('select * from wifi_machine') as $row){
				if(long2ip($row['ip_address']) == $wifi_from && $udp_buf == "STOP"){
					if($row['client_status'] == "呼出要求"){
						$sql->execute(["停止要求",$wifi_from]);
						echo($wifi_from . " より停止要求". PHP_EOL. PHP_EOL);
					}
					break;
				}else if(long2ip($row['ip_address']) == $wifi_from && $udp_buf == "NON"){
					if($row['client_status'] == "呼出要求"){
						$sql->execute(["応答無",$wifi_from]);
						echo($wifi_from . " より応答無". PHP_EOL. PHP_EOL);
					}
					break;
				}
			}

		   $wifi_from = "";
		   $wifi_port = "";
		   $udp_buf = "";
		   echo("次のudpデータ受信開始 : ");
	    }

	    //lose socket
	    socket_close($udp_socket);
   
}

udpsocket_loop();

?>


・データベースの作成

このシステムでは複数台の制御に対応し各Wi-Fi子機のステータスを保持しステータス状態判定をするためデータベースを利用しています。

mysqlの管理画面にてbugle.sqlファイルのsqlを実行してください。mysqlの管理画面はapacheが起動していないとwebサーバとして機能せず管理画面へのアクセスができないので、apacheを起動していることを確認して「admin」ボタンを押して管理画面を立ち上げてください。

1、xamppプリケーションを立ち上げて「apache」と「mysql」を起動、「mysql」の「admin」ボタンを押してmysqlの管理画面を立ち上げる。

以下の管理画面でsqlを実行します。図にもあるようにサーバ「127.0.0.1」上でsqlを実行してください。

sql内のデータベースパスワードが’password’となってしまっているのですが、独自で解読困難なpasswordに変更してください。


以上で環境設定は終了です。以下から実際に動作させる方法を解説します。






・システムの稼働、システムの利用

・webサーバシステムの起動

1、まずwebサーバに設置した「xampp」の「apache」と「mysql」を起動しましょう。両方の「start」ボタンを押して起動を確認してください。もちろんすでに起動している場合は次に進んでください。


2、次にターミナルを2つ開いてphpファイルserver/bin/chat-server.phpとserver/bin/udp-socket.phpをそれぞれ動作させましょう。

phpファイルは php [ファイル名] で動かすことができます。

webサーバシステムの起動は以上です。

・Wi-Fi子機の起動

子機はUSBに接続すれば電源が供給されるので、PCやコンセントにUSB接続すればOKです。

・ブラウザを立ち上げてWi-Fi子機の情報を登録

1、ブラウザから子機を操作するため、まず情報の登録を行います。ハンバーガーメニューボタンを選択してサイドバーを表示します。



2、サイドバーから「add」を選択します。



3、画面の「IPアドレス」欄にarduinoのモニタウィンドウで確認したWi-Fi子機のIPアドレスを設定します。「NAME」には任意のわかりやすい名前を設定します。設定が終わったら「追加」ボタンを押します。



4、登録が完了すると、ホーム画面に「NAME」で設定したボタンが出現します。



・動作確認

実際にブラウザからの操作から、ブラウザの結果表示までを実施してみます。




・まとめ、課題

課題としては、Wi-Fi子機がブレッドボード上でしか構築できていないため、実用性が皆無なのが問題です。いずれコンパクトな機器として作り上げて実生活で利用していけるようにできたらと考えています。

またIPアドレスの固定化をはじめとして環境構築が非常に複雑になってしまっているところですね。こちらもシステム側でフォローできるようにいずれ改善をしていきたいと思います。

ここまででお聞きしたいことがございましたらお気軽にコメント書きコメント欄にてコメントしていただけると幸いです。

ここまでご覧いただきありがとうございました。


0 0 votes
Article Rating
Subscribe
Notify of
guest

CAPTCHA


0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments