PythonでAndroidスマホのシリアル通信をする。

# はじめに
WindosやLinuxのPCからPythonでシリアル通信をする場合```pyserial```を使えば可能。しかしAndroidスマホでは使えなかった。本記事では、Androidスマホでシリアル通信するライブラリ```usbserial4a```についてまとめる。

# 目次
+ [usbserial4a](#usbserial4a)
+ [ポート番号のリストとポートのオープン](#ポート番号のリストとポートのオープン)
+ [シリアル通信の送受信](#シリアル通信の送受信)
+ [Kivyでシリアル通信アプリを作る](#kivyでシリアル通信アプリを作る)
+ [作ったコード](#作ったコード)


# usbserial4a
Androidのスマホでシリアル通信するためのライブラリ。```pip install usbserial4a```と```pip install usb4a```でライブラリ取得。kivy上で動作するライブラリなので、Kivyとセットで使う。Kivyについての記事は　 [PythonのGUIライブラリKivyを使う](https://qiita.com/tapitapi/items/a4a4e3a7164afd922115) を参照。
```usbserial4a```はAndroid用なのでPCで動作できない。スマホにPydroid3をインストールするか、Buildozerでアプリ化して動作させる必要がある点注意。

https://github.com/jacklinquan/usbserial4a

[戻る](#目次)

# ポート番号のリストとポートのオープン
スマホにつながっているUSBシリアルデバイスの確認をする場合は下記のように```get_usb_device_list()```でUSBデバイスリストを取得し、```getDeviceName()```で、ポートの番号を取得する。
```python:port_list.py
from usb4a import usb
from usbserial4a import serial4a
# 合わせてkivyライブラリもインポート必要

#port取得
serial_ports=[]
for port in usb.get_usb_device_list():
	serial_list.append(port.getDeviceName())
```

ポートのオープンは下記の通りにポート番号、ボーレート、パリティーの設定をする。下記例は、リストの先頭のポートを、115200bps、パリティーなしでポートオープンする場合の例。

```python:port_open.py
from usb4a import usb
from usbserial4a import serial4a
# 合わせてkivyライブラリもインポート必要

serial_ports=[]
for port in usb.get_usb_device_list():
	serial_ports.append(port.getDeviceName())
 
port_val = serial_ports[0] # この例ではポートリストの最初のものを設定
boud_val = 115200          # 数値で設定
prty_val = 'N'             # 'N:None','O:Odd','E:Even'のN/O/Eを抜き出す。
		
Serial_Port=serial4a.get_serial_port(device_name=port_val, baudrate=boud_val, parity= prty_val)
```
[戻る](#目次)

# シリアル通信の送受信
シリアル通信の送信受信はwrite()：送信、read()：受信でできる。データの送受信はバイナリデータで行うため、文字列を送受信する場合はencode()/decode()する必要がある。受信はデータを受信するまで留まるので、リアルタイムに送受信したい場合は、並列処理できるよう別スレッドで実装する必要あり。

```python:tx_rx.py
from usb4a import usb
from usbserial4a import serial4a
# 合わせてkivyライブラリもインポート必要

# ポートオープン：'先頭のポート' 115200bps ParityなしのOpen
serial_ports=[]
for port in usb.get_usb_device_list():
	serial_ports.append(port.getDeviceName())
 
port_val = serial_ports[0] # この例ではポートリストの最初のものを設定
boud_val = 115200          # 数値で設定
prty_val = 'N'             # 'N:None','O:Odd','E:Even'のN/O/Eを抜き出す。
Serial_Port=serial4a.get_serial_port(device_name=port_val, baudrate=boud_val, parity= prty_val)

#送信
data='test'
data=data.encode('utf-8')
Serial_Port.write(data)

#受信
data=Serial_Port.read() # dataに受信データが格納される。
data=data.decode('utf-8')
```
[戻る](#目次)

# Kivyでシリアル通信アプリを作る
Kivyで下記のようなアプリを作る。今回kivyで使ったウィジェットはButton、Spinner、TextInput。Kivyについての記事は　 [PythonのGUIライブラリKivyを使う](https://qiita.com/tapitapi/items/a4a4e3a7164afd922115) を参照。
Spinnerでポート番号(PortNo)、ボーレート(Speed(bps))、パリティー(Parity)を選択できるようにして、Openボタンでシリアル通信を開始。ポート番号は、PortNoをクリックしたときにリアルタイムにポート番号をスキャンする形にした。

:::note info
ポートオープンと、USB_Serialのアクセス許可画面の兼ね合いで、1回目はうまく通信できないときがある。その場合は、もう一度起動しなおすと通信できるようになる。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299802/bc1e239a-4812-4b57-c900-c3a9730375d7.png)

[戻る](#目次)

# 作ったコード
👇の```main.py```をPydroid3で起動させればアプリを確認できる。
また、buildozerでアプリ化するためのbuildozer.specも併せて記載する。buildozer.specは、初期値から変更した行だけ抜き出した。

<details><summary><b>main.py/buildozer.specファイルはこちら。</summary><div>

```python:main.py
from kivy.app import App
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout #boxのレイアウト
import time
from usb4a import usb
from usbserial4a import serial4a
import threading

#kivy GUI
Layout = Builder.load_string('''

#第一階層
BoxLayout:
	orientation:'vertical'
	#Serial設定===============================
	BoxLayout:
		orientation:'horizontal'
		size_hint_y:0.05	#y軸のサイズ
		# Port
		Spinner:
			id			:spinner_port
			size_hint_x	:0.25	#x軸のサイズ
			text		:'PortNo'
			values		:''
			on_press	:app.port_list()	# Spinnerを押されたとき
		# Speed
		Spinner:
			id			:spinner_speed
			size_hint_x	:0.25	#x軸のサイズ
			text		:'Speed(bps)'
			values		:'9600','38400','115200'
		# Parity
		Spinner:
			id			:spinner_parity
			size_hint_x	:0.25	#x軸のサイズ
			text		:'Parity'
			values		:'N:None','O:Odd','E:Even'
		#Open
		Button:
			id:button_open
			size_hint_x		:0.25	#x軸のサイズ
			background_color:(0,1,0,1)
			text:'Open'
			on_press:app.serial_open()
	#送信データ設定==========================
	BoxLayout:
		orientation:'horizontal'
		size_hint_y:0.05	#y軸のサイズ
		# Text
		TextInput:
			id:textinput_sendout
			text: ''
			font_size:50
			size_hint_x		:0.80				#x軸のサイズ
			multiline:False						# 1行
			on_text_validate:app.enter_out()	#Enterキーでイベント発生
		# SendOut
		Button:
			id:button_sendout
			size_hint_x		:0.20	#x軸のサイズ
			background_color:(0,1,0,1)
			text:'SendOut'
			on_press:app.send_out()
	#送受信データ出力========================
	TextInput:
		id:textinput_serial
		text: ''
		font_size:50
		size_hint_y:0.95	#y軸のサイズ
''')

#--------------------
# Serial通信
#--------------------
class Serial_app(App):
	
	#Global
	Serial_Port=''
	OutPut=''
	Thread_rx=''
	
	#----------------------
	# Kivy初期化
	#----------------------
	def build(self):
		self.title = 'Serial_app'
		
		#データ受信関数を設定
		self.Thread_rx = threading.Thread(target=self.serial_read)
		self.Thread_rx.start()
		
		#Kivyの周期関数を設定
		Clock.schedule_interval(self.update_gui, 1/30)
		
		return Layout

	#----------------------
	# Kivy終了
	#----------------------
	def on_stop(self):
		self.Serial_Port.close()
	
	#----------------------
	# 1/30 sec 毎に画面更新
	#----------------------
	def update_gui(self, dt):
		self.root.ids.textinput_serial.text = self.OutPut
		
		#50000文字以上になると古い文字から削除
		if len(self.OutPut)>50000:
			self.OutPut = self.OutPut[50000:]
	
	#----------------------
	# thread_rx(データを受信)
	#----------------------
	def serial_read(self):
		while(1):
			if self.Serial_Port !='':
				data=self.Serial_Port.read()	#受信
				data=data.decode('utf-8')
				self.OutPut+=data
			else:
				time.sleep(0.1)

	#----------------------
	# [SendOut]ボタンでデータ送信
	#----------------------
	def send_out(self):
		self.Serial_Port.write((self.root.ids.textinput_sendout.text+'\r\n').encode('utf-8'))
		
	#----------------------
	# データ入力+[Enter]でデータ送信
	#----------------------
	def enter_out(self):
		self.Serial_Port.write((self.root.ids.textinput_sendout.text+'\r\n').encode('utf-8'))
		self.root.ids.textinput_sendout.text=''
	
	#----------------------
	# シリアルポートのlist
	#----------------------
	def port_list(self):
		self.root.ids.spinner_port.values.clear()
		for port in usb.get_usb_device_list():
			self.root.ids.spinner_port.values.append(f'{port.getDeviceName()}')
			
	#----------------------
	# シリアルポートをOpen
	#----------------------
	def serial_open(self):
		port_val = self.root.ids.spinner_port.text
		boud_val = self.root.ids.spinner_speed.text
		prty_val = self.root.ids.spinner_parity.text[:1] # 'N:None','O:Odd','E:Even'のN/O/Eを抜き出す。
		
		#OpenPort
		if self.Serial_Port =='':
			try:
				self.Serial_Port=serial4a.get_serial_port(device_name=port_val, baudrate=int(boud_val), parity= prty_val)
				self.OutPut = f'OpenPort({port_val},{boud_val},{prty_val})\n'
		
			except:
				self.OutPut = 'The parameter is not available.'
				
if __name__ == '__main__':
    Serial_app().run()
```

```python:buildozer.spec
[app]

# (str) Title of your application
title = USB_Serial

# (str) Package name
package.name = usb_serial

# (list) Application requirements
requirements = python3,kivy,usbserial4a,usb4a

# (list) Permissions
android.permissions = android.permission.INTERNET, (name=android.permission.WRITE_EXTERNAL_STORAGE;maxSdkVersion=18)

```
</div></details>

アプリの作り方は 下記記事を参照。

https://qiita.com/tapitapi/items/e91d67d59ed10caa59f6



[戻る](#目次)