Pepperでソケット通信を行うボックスを作ってみた

#はじめに
　Pepperを業務で活用する場合、既存システムと連携させたいケースがあるかと思います。
システム間の通信にはTCP/IPが用いられることが多いですが、残念ながらChoregrapheにはソケット通信を行うボックスが用意されていません。

ということでソケット通信を行う「Socket Clientボックス」を作ってみました。

![SocketClientBox.png](https://qiita-image-store.s3.amazonaws.com/0/173229/012a7cc0-8263-d818-8545-c439bf27c000.png)

**サーバソケットは？**
　ソケット通信にはサーバとクライアントがありますが、Choregrapheでサーバポートがオープンできないようなので(例外になる)、今回はクライアント側のボックスだけご紹介します。

#「Socket Clientボックス」の機能
1. ソケットの作成と終了
2. サーバへの接続と切断
    - ボックスを開始したらサーバへは自動接続する。
    - コネクションは常に繋ぎ放しとする。切断された場合は自動で再接続する。
    - ボックス終了時に接続中の場合は強制切断する。
3. データ(文字列)の送信と受信
    - データ受信はサーバに接続したら自動開始する。
    - データを受信したらデータと共に上位に通知(I/O出力)する。
4. 通信イベント(接続、切断、データ受信、エラー)の検知と通知

**ポイント**
　再接続、データ受信など、上位側でイベントを検知してから制御すると結線が複雑になりそうなので、できるだけボックス内で自動で行うようにしました。

#パラメータ(変数)
![パラメータ.png](https://qiita-image-store.s3.amazonaws.com/0/173229/628a149e-0df7-ce44-a266-67414f11c07d.png)

| NO  | 名前  | タイプ | 説明                            |
|:---:|:------|:-----:|:--------------------------------|
| ①　 |host  | 文字列 | 接続サーバのIPアドレスを指定する。 |
| ②   |port　| 整数   | 接続サーバのポート番号を指定する。 |

#入力(I/O)
![入力.png](https://qiita-image-store.s3.amazonaws.com/0/173229/9b214094-e0ba-a80c-a198-fdeaea223e1f.png)

| NO  | 名前 　 |  タイプ  | 性質    | 説明                                                      |
|:---:|:--------|:------:|:-------:|:----------------------------------------------------------|
| ①　 | onStart | バン  | onStart | ソケットを作成し、パラメータで指定されたサーバに自動接続する。  |
| ②   | onStop  | バン  | onStop  | ソケットを終了する。サーバと接続中の場合は強制切断後に終了する。|
| ③   | onSend  | 文字列 | onEvent | 指定した文字列をサーバに送信する。                           |

#出力(I/O)
![出力.png](https://qiita-image-store.s3.amazonaws.com/0/173229/12d5bf74-af47-abf1-4a87-a5f831de2eec.png)

| NO  | 名前 　        |  タイプ | 性質      | 説明                                               |
|:---:|:---------------|:------:|:---------:|:--------------------------------------------------|
| ①　 | onStopped	 | バン	  | onStopped | ボックスが終了したら出力する。                       |
| ②   | onStarted	 | バン	  | 即時	      |	ボックスが開始されたら出力する。                     |
| ③   | onConnected	 | 文字列 | 即時	      |	サーバに接続されたらサーバのIPアドレスと共に出力する。 |
| ④   | onDisconnected | バン	  | 即時	      |	通信が切断された場合に出力する。                     |
| ⑤   | onReceived	 | 文字列 | 即時	      |	データを受信した際、データ(文字列)と共に出力する。    |
| ⑥   | onError	 | バン	 | 即時	      |	通信エラーが発生した場合に出力する。                 |

#スクリプト(Python)
```
import socket
import time

class MyClass(GeneratedClass):
    def __init__(self):
        GeneratedClass.__init__(self, False)

    def onLoad(self):
        self.bIsRunning = False    # 起動中フラグ
        self.bConnected = False    # 接続中フラグ

    def onUnload(self):
        self.bMustStop = True    # 停止要求フラグ（Trueにすると接続&受信ループから抜ける）

    def onInput_onStart(self):
        # 二重起動を抑止する。
        if( self.bIsRunning ):
            return

        # ソケットを作成する。
        try:
            self.logger.debug("socket")
            self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        except Exception as e:
            self.logger.error("socket error:%s", e.args[0])
            self.onError()    # エラー通知
            self.onInput_onStop()    # 終了
            return

        self.bIsRunning = True
        self.bMustStop = False

        # パラメータを設定する。
        host = self.getParameter("host")    # サーバIP
        port = self.getParameter("port")    # サーバポート番号
        bufsize = 1024    # 受信バッファサイズ
        secs = 10    # 再接続間隔(秒)

        self.onStarted()    # 開始通知

        # 接続＆受信ループ
        # 未接続の場合は接続処理を接続中の場合は受信処理を繰り返す。
        while ( not self.bMustStop ):
            if( not self.bConnected ):
                # 未接続⇒サーバと接続する。
                try:
                    self.logger.debug("connect %s:%s...", host, port)
                    self.client.connect((host, port))
                    self.logger.debug("connected")
                    self.bConnected = True
                    self.onConnected(host)    # 接続通知
                except Exception as e:
                    self.logger.error("connect error:%s", e.args[0])
                    time.sleep(secs)    # 一定時間経ってからリトライする。
            else:
                # 接続済⇒データを受信する。
                try:
                    self.logger.debug("receive...")
                    data = self.client.recv(bufsize)
                    if(data):
                        #　データ有り
                        self.logger.debug("received data:%s", data)
                        self.onReceived(data)    # 受信通知
                    else:
                        #　データ無し(切断)
                        self.logger.debug("disconnected")
                        self.bConnected = False
                        # ソケットを作成し直す。
                        self.client.close()
                        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                        self.onDisconnected()    # 切断通知
                        time.sleep(secs)    # 一定時間経ってからリトライする。
                except Exception as e:
                    self.logger.error("receive error:%s", e.args[0])
                    self.onError()

        # 接続＆受信ループ終了
        if( self.bConnected ):
            self.logger.debug("disconnect")
            self.bConnected = False
            # ソケットをクローズする。
            self.client.close()
            self.onDisconnected()    # 切断通知

        self.bIsRunning = False

    def onInput_onStop(self):
        self.onUnload()
        self.onStopped()

    # 指定されたデータを送信する。
    def onInput_onSend(self, p):
        self.logger.debug("send %s", p)
        try:
            self.client.send(p)    # データ送信
        except Exception as e:
            self.logger.error("send error:%s", e.args[0])
            self.onError()    # エラー通知
```
**Pepper 2.5.5**
ちなみに上記コードは、NAOqi OS 2.5.5でも変更なしで動きました。

#使用例
![使用例.png](https://qiita-image-store.s3.amazonaws.com/0/173229/a36bbd5d-526a-b217-c5f9-6091cae40708.png)

#サーバサンプル(C#)
　対向アプリのサンプルです。
　ファンクションキーかボタンを押すと対応するコマンド("F1"～"F12")をPepperに送信します。

![対向サンプル.png](https://qiita-image-store.s3.amazonaws.com/0/173229/59313e11-4824-7748-ceae-b9d8820d23e9.png)

```
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;

namespace PepperHost
{
    public partial class FormMain : Form
    {
        delegate void ConsoleWriteDelegate(string format, params object[] args);
        delegate void EnableOnlineButtonDelegate(bool enable);
        delegate void EnableCommandButtonsDelegate(bool enable);

        Thread m_ServerThread = null;
        bool m_ServerShouldStop = false;
        Thread m_RecvThread = null;
        bool m_RecvShouldStop = false;

        string m_IPString = null;
        int m_Port = 2001;
        TcpListener m_Listener = null;
        TcpClient m_Client = null;
        NetworkStream m_NetworkStream = null;

        bool m_Closing = false;

        public FormMain()
        {
            InitializeComponent();

            EnableOnlineButton(false);
            EnableCommandButtons(false);
        }

        private void FormMain_Load(object sender, EventArgs e)
        {
            string hostname = Dns.GetHostName();
            IPAddress[] adrList = Dns.GetHostAddresses(hostname);
            foreach (IPAddress adr in adrList)
            {
                if (adr.AddressFamily == AddressFamily.InterNetwork)
                {
                    m_IPString = adr.ToString();
                    break;
                }
            }
        }

        /// <summary>
        /// ボタンのクリック通知
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void buttonX_Click(object sender, EventArgs e)
        {
            Button button = (Button)sender;

            switch (button.Text)
            {
                case "開始":
                    CmdStart();
                    break;
                case "停止":
                    CmdStop();
                    break;
                case "F1":
                case "F2":
                case "F3":
                case "F4":
                case "F5":
                case "F6":
                case "F7":
                case "F8":
                case "F9":
                case "F10":
                case "F11":
                case "F12":
                    CmdSend(button.Text);
                    break;
                case "終了":
                    CmdExit();
                    break;
            }
        }

        /// <summary>
        /// ファンクションキーの押下通知
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void FormMain_KeyDown(object sender, KeyEventArgs e)
        {
            if ((m_Client != null) && m_Client.Connected)
                if ((e.KeyCode >= Keys.F1) && (e.KeyCode <= Keys.F12))
                {
                    int id = e.KeyCode - Keys.F1 + 1;
                    CmdSend("F" + id);
                }
        }

        /// <summary>
        /// サーバ処理を開始する。
        /// </summary>
        private void CmdStart()
        {
            LogWrite("サーバを開始します...");
            m_ServerThread = new Thread(new ThreadStart(ServerThread));
            m_ServerThread.Start();
            buttonStart.Text = "停止";
        }

        /// <summary>
        /// サーバ処理を停止する。
        /// </summary>
        private void CmdStop()
        {
            LogWrite("サーバを停止します...");

            if ((m_Client != null) && m_Client.Connected)
            {
                EnableCommandButtons(false);
                m_Client.Close();
            }

            if ((m_RecvThread != null) && m_RecvThread.IsAlive)
            {
                m_RecvShouldStop = true;
                m_NetworkStream.Close();
            }

            if ((m_ServerThread != null) && m_ServerThread.IsAlive)
            {
                m_ServerShouldStop = true;
                m_Listener.Stop();
            }

            buttonStart.Text = "開始";
        }

        /// <summary>
        /// コマンドを送信する。
        /// </summary>
        /// <param name="key"></param>
        private void CmdSend(string key)
        {
            LogWrite("コマンド{0}を送信します...", key);
          
            Encoding enc = System.Text.Encoding.UTF8;
            byte[] sendBytes = enc.GetBytes(key);
            m_NetworkStream.Write(sendBytes, 0, sendBytes.Length);
            string resMsg = enc.GetString(sendBytes, 0, (int)sendBytes.Length);
            LogWrite("送信 {0}", resMsg);
        }

        /// <summary>
        /// アプリを終了する。
        /// </summary>
        private void CmdExit()
        {
            m_Closing = true;
            CmdStop();
            this.Close();
        }

        /// <summary>
        /// サーバスレッド
        /// </summary>
        void ServerThread()
        {
            LogWrite("サーバスレッド開始");
            IPAddress ipAddress = IPAddress.Parse(m_IPString.ToString());
            m_Listener = new TcpListener(ipAddress, m_Port);

            m_Listener.Start();

            while (!m_ServerShouldStop)
            {
                try
                {
                    LogWrite("接続待ち {0}:{1}...",
                        ((IPEndPoint)m_Listener.LocalEndpoint).Address,
                        ((IPEndPoint)m_Listener.LocalEndpoint).Port);

                    m_Client = m_Listener.AcceptTcpClient();
                    LogWrite("接続 {0}:{1}",
                        ((IPEndPoint)m_Client.Client.RemoteEndPoint).Address,
                        ((IPEndPoint)m_Client.Client.RemoteEndPoint).Port);

                    m_NetworkStream = m_Client.GetStream();
                    m_RecvThread = new Thread(new ThreadStart(RecvThread));
                    m_RecvThread.Start();
                }
                catch(Exception ex)
                {
                    LogWrite("サーバスレッド停止:{0}", ex.Message);
                    m_Listener.Stop();
                    break;
                }
            }

            if(m_ServerShouldStop)
                m_ServerShouldStop = false;

            LogWrite("サーバスレッド終了");
        }

        /// <summary>
        /// 受信スレッド
        /// </summary>
        void RecvThread()
        {
            LogWrite("受信スレッド開始");

            Encoding enc = System.Text.Encoding.UTF8;
            byte[] recvBytes = new byte[1024];
            int recvLength = 0;
            string resMsg;

            EnableOnlineButton(true);
            EnableCommandButtons(true);

            while (!m_RecvShouldStop)
            {
                try
                {
                    recvLength = m_NetworkStream.Read(recvBytes, 0, recvBytes.Length);
                    resMsg = enc.GetString(recvBytes, 0, recvLength);
                    if (resMsg.Length == 0)
                    {
                        LogWrite("切断");
                        m_Client.Close();
                        break;
                    }

                    LogWrite("受信 {0}", resMsg);
                }
                catch(Exception ex)
                {
                    LogWrite("受信スレッド停止:{0}", ex.Message);
                    m_Client.Close();
                    break;
                }
            }

            EnableOnlineButton(false);
            EnableCommandButtons(false);

            if(m_RecvShouldStop)
                m_RecvShouldStop = false;

            LogWrite("受信スレッド終了");
        }

        /// <summary>
        /// ログを出力する。
        /// </summary>
        /// <param name="format"></param>
        /// <param name="args"></param>
        private void LogWrite(string format, params object[] args)
        {
            if (m_Closing)
                return;

            int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
            string format2 = string.Format("{0}[{1}] {2}", DateTime.Now, id.ToString("x4"), format);
            ConsoleWriteLine(format2, args);
        }

        /// <summary>
        /// 画面に行出力する。
        /// </summary>
        /// <param name="format"></param>
        /// <param name="args"></param>
        private void ConsoleWriteLine(string format, params object[] args)
        {
            if (InvokeRequired)
            {
                object[] args2 = { format, args };

                Invoke(new ConsoleWriteDelegate(ConsoleWriteLine), args2);
            }
            else
            {
                textBox1.AppendText(string.Format(format + "\r\n", args));
            }
        }

        /// <summary>
        /// 通信状態ボタンを有効/無効にする。
        /// </summary>
        /// <param name="fEnable"></param>
        private void EnableOnlineButton(bool fEnable)
        {
            if (m_Closing)
                return;

            if (InvokeRequired)
            {
                object[] args2 = { fEnable };

                Invoke(new EnableOnlineButtonDelegate(EnableOnlineButton), args2);
            }
            else
            {
                if (fEnable)
                {
                    checkBox1.Text = "ONLINE";
                    checkBox1.ForeColor = Color.Green;
                    checkBox1.FlatAppearance.BorderColor = checkBox1.ForeColor;
                    checkBox1.FlatAppearance.MouseDownBackColor = checkBox1.FlatAppearance.CheckedBackColor;
                    checkBox1.FlatAppearance.MouseOverBackColor = checkBox1.FlatAppearance.CheckedBackColor;
                }
                else
                {
                    checkBox1.Text = "OFFLINE";
                    checkBox1.ForeColor = Color.DarkRed;
                    checkBox1.FlatAppearance.BorderColor = checkBox1.ForeColor;
                    checkBox1.FlatAppearance.MouseDownBackColor = checkBox1.BackColor;
                    checkBox1.FlatAppearance.MouseOverBackColor = checkBox1.BackColor;
                }
                checkBox1.Checked = fEnable;
            }
        }

        /// <summary>
        /// コマンドボタンを有効/無効にする。
        /// </summary>
        /// <param name="fEnable"></param>
        private void EnableCommandButtons(bool fEnable)
        {
            if (m_Closing)
                return;

            if (InvokeRequired)
            {
                object[] args2 = { fEnable };

                Invoke(new EnableCommandButtonsDelegate(EnableCommandButtons), args2);
            }
            else
            {
                panel1.Enabled = fEnable;
            }
        }
    }
}
```

#まとめ
　Pythonスクリプトを使ったら簡単にソケットボックスが作れてしまいました。Windowsアプリとも問題なく繋がります。
　次はこちらのボックスをベースにLPRサーバかTelnetサーバに接続してみたいみたいと思います。