RaspberryPiとAndroidデバイス間で通信する


Raspberry Piをサーバーサイド、AndroidをクライアントサイドとしてSocket通信をします。
サーバーサイドに関しては同じ条件でPythonが動く環境であればRaspberryPiである必要はありません。

## 環境  
#### クライアントサイド
- Kotlin 1.3.10
- Android 9
- Pixel 2

#### サーバーサイド
- Python 3.7.1
- Raspbian Stretch 
- Raspberry Pi Zero W

## 準備
どちらのソースもサーバーサイド（Raspberry Pi）のIPアドレスとポート番号が必要となります。

IPアドレスは`ifconfig`コマンドの`wlan0`の項目で調べることができます。
今回はIpv4で指定するので`inet`のアドレスを指定します。
ちなみに、この環境では`192.168.10.7`でした。


ポート番号は[well-known ports](https://ja.wikipedia.org/wiki/TCP%E3%82%84UDP%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7%E3%81%AE%E4%B8%80%E8%A6%A7)と競合しない[49152–65535]の間の適当な番号を指定します。
今回は`55555`としました。

## サーバー側（Python）
Socket通信のラッパークラスを用意してmain.pyから利用します。インスタンス化されたタイミングでconnectを開始します。

```main.py
# coding=utf-8
from com_socket import *

if __name__ == '__main__':

    # ドメイン名、もしくはIPアドレス。
    # ドメイン名は socket.gethostname() で取得することもできる。
    host = "192.168.10.7"

    # wellknownと衝突しない適当なポート番号
    port = 55555

    connection = TcpServer(host, port)

    try:
        while True:
            data = connection.recv_str()

            if data == 'ON_CLICK_BUTTON_1':
                print("ON_CLICK_BUTTON_1")
                # 行いたい何かしらのアクション (ex LEDの点灯やサーボの動作などなど）

            elif data == 'ON_CLICK_BUTTON_2':
                print("ON_CLICK_BUTTON_2")
                # 行いたい何かしらのアクション (ex LEDの点灯やサーボの動作などなど）

            elif data == 'ON_CLICK_BUTTON_QUIT':
                print("ON_CLICK_BUTTON_QUIT")
                quit()

    finally:
        connection.close()
```

```tcp_server.py
# coding=utf-8
import socket
import concurrent.futures

class TcpServer:
    def __init__(self, address, port, recv_size=1024):

        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.bind((address, port))  
        self.socket.listen(5)

        print('クライアントデバイスからの接続待ち....')
        self.client_socket, self.client_info = self.socket.accept()
        print("接続完了")

        self.recv_func = lambda: self.client_socket.recv(recv_size)
        self.send_func = lambda data: self.client_socket.send(data)

        self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)

    def recv_str(self):
        future = self.executor.submit(self.recv_func)
        result = future.result()
        # 受け取ったデータをutf8にデコードする
        return bytes(result).decode('utf-8')

    def send_str(self, data):
        self.executor.submit(self.send_func, bytes(data.encode('utf-8')))

```

### ソケット通信
`SOCK_STREAM`でTCP、`SOCK_DGRAM`でUDPとなります。
`socket.AF_INET`でIPv4を指定しています。
`accept()`を呼ぶことでクライアントの接続を待機します。

```python

self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.bind((address, port))  
self.socket.listen(5)

self.client_socket, self.client_info = self.socket.accept()
```



### スレッド処理
実際の通信処理は別スレッドで行います。
executorにコールバックをsubmitすると、スレッドプールで実行されます。
結果の取り出しは`result()`でブロッキングして値を取り出します。

```python
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)

# 受信処理をラムダ式で定義（送信処理も同様）
self.recv_func = lambda: self.client_socket.recv(recv_size)

future = self.executor.submit(self.recv_func)
result = future.result()
```



## クライアント側（Android）
Python同様に通信はUIスレッドとは別に、coroutinesで非同期処理します。
`connectionFab`をClickすることで接続が開始されます。

```build.gradle
// dependenciesにcoroutinesを追加
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1"
```


```MainActivity.kt

// メッセージ用の識別子
const val MSG_CONNECTION_SUCCESS = 111 // 接続成功
const val MSG_CONNECTION_FAILED = 222  // 接続失敗
const val MSG_IOEXCEPTION = 333        // 例外発生


class MainActivity : AppCompatActivity() {

    private var tcpcom: ComTcpClient? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setSupportActionBar(toolbar)
        setContentView(R.layout.activity_main)

        val channel = Channel<Int>()
        GlobalScope.launch(Dispatchers.Main) {
            when (channel.receive()) {
                MSG_CONNECTION_SUCCESS -> {
                    connectionStatus.text = "Successfully!!"
                }

                MSG_CONNECTION_FAILED -> {
                    connectionStatus.text = "Failed!!"
                    // エラー処理
                }

                MSG_IOEXCEPTION -> {
                    connectionStatus.text = "Error!!"
                    //エラー処理
                }
            }
        }

        connectionFab.setOnClickListener {
            val ip = editIpAddress.text.toString() // 今回は "192.168.10.7"が代入される。
            val port = editPort.text.toString()    // "55555"が代入される。

            if (!ip.isEmpty() && !port.isEmpty()) {
                tcpcom = ComTcpClient(ip, port.toInt(), channel)
                tcpcom?.connect()
                connectionStatus.text = "Connecting..."
            }
        }

        button1.setOnClickListener {
            tcpcom?.sendOrReceive { outputStream, _ ->
                outputStream.write("ON_CLICK_BUTTON_1".toByteArray())
            }
        }

        button2.setOnClickListener {
            tcpcom?.sendOrReceive { outputStream, _ ->
                outputStream.write("ON_CLICK_BUTTON_2".toByteArray())
            }
        }

        buttonQuit.setOnClickListener {
            tcpcom?.sendOrReceive { outputStream, _ ->
                outputStream.write("ON_CLICK_BUTTON_QUIT".toByteArray())
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        tcpcom?.close()
    }
}


```

```TcpClient.kt 

class ComTcpClient(val ip: String, val port: Int, val channel: Channel<Int>) {

    private var socket: Socket? = null
    private val TAG = ComTcpClient::class.java.simpleName

    fun connect() {
        GlobalScope.launch(Dispatchers.Default) {
            Log.d(TAG, "接続開始...")
            try {
                socket = Socket(ip, port)
                channel.send(MSG_CONNECTION_SUCCESS)

            } catch (e: IOException) {
                Log.e(TAG, "IOException", e)
                channel.send(MSG_IOEXCEPTION)

            } catch (e: UnknownHostException) {
                Log.e(TAG, "UnknownHostException", e)
                channel.send(MSG_CONNECTION_FAILED)
            }
        }
    }

    fun sendOrReceive(callback: (OutputStream, InputStream) -> Unit) {
        if (socket == null) throw java.lang.IllegalStateException()
        socket?.also { socket ->
            GlobalScope.launch(Dispatchers.Default) {
                try {
                    if (socket.isConnected) {
                        callback(socket.outputStream, socket.inputStream)
                    } else {
                        channel.send(MSG_CONNECTION_FAILED)
                    }
                } catch (e: IOException) {
                    channel.send(MSG_IOEXCEPTION)
                }
            }
        }
    }

    fun close() {
        if (socket == null) throw java.lang.IllegalStateException()
        socket?.also { socket ->
            GlobalScope.launch(Dispatchers.Default) {
                try {
                    if (socket.isConnected) socket.close()
                } catch (e: IOException) {
                    channel.send(MSG_IOEXCEPTION)
                }
            }
        }
    }
}
```