16
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OthloTechAdvent Calendar 2018

Day 4

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

Posted at

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と競合しない[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()を呼ぶことでクライアントの接続を待機します。


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()でブロッキングして値を取り出します。

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)
                }
            }
        }
    }
}
16
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?