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を開始します。
# 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()
# 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することで接続が開始されます。
// dependenciesにcoroutinesを追加
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1"
// メッセージ用の識別子
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()
}
}
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)
}
}
}
}
}