PythonでAndroidをリモート制御する【ADB】

Python

今回はPythonからAndroid端末をリモートで制御する方法です。

adb.exeのパスを確認する

以前にNOX Playerをインストールした際、なにを思ったかEドライブにインストールしていたみたいです。

Androidの開発環境とかを入れても一緒に入ったかも。

私の場合は”E:\nox_vm\Nox\bin\adb.exe”になります。

adb.exeでできること

adb.exeを使用するとコマンドライン上でコマンドを実行することでAndroidを操作できます。

例えばある座標をタップしたり、アプリを起動したりすることができます。

まずは繋がっているAndroid端末の一覧を取得してみます。

adb.exeに対して「devices」という引数を与えて実行してみました。

List of~~以降のところが実行結果となります。

この場合、127.0.0.1:62001という接続先にdeviceという名前の端末があることになります。

私はNOX Playerを起動しながら実行しましたのでこのような結果となります。

実機のAndroid端末をUSB等で繋ぐとまた別の結果になります。

この他にもタップしたり、スワイプしたりするコマンドがあるということですね。

今回はこれをPythonから実行してリモートで操作をするという方針の話です。

一通り作成して、クラス化までしてます。

ソースコード(クラス)

作ったクラスを見てみましょう。これは以前作っていたものをリファクタリングしたものです。

下記がAdbCtrl.pyの全てです。

# -*- coding: Shift-JIS -*-

from enum import IntEnum
import subprocess
import os
import cv2
import time
import numpy as np

from Common import *

class AdbCtrl(object):
	pass

	class InitParam(object):
		"""
		初期化パラメータ
		"""
		def __init__( self) -> None:
			pass
			#self.AdbRootDir = r"H:\nox_vm\Nox\bin"
			self.AdbRootDir : str = r""				# adb.exeがあるディレクトリパス
			return

	class Device(object):
		"""
		接続先端末情報モデル
		"""
		def __init__( self, deviceId : str = "" , deviceName : str = "") -> None:
			pass
			self.DeviceId : str = deviceId
			self.DeviceName : str = deviceName
			return

	class eKeyCode(IntEnum):
		Home = 3
		Back = 4
		Task = 187

	def __init__( self) -> None:
		"""
		コンストラクタ
		"""
		pass

		object.__init__(self)
		self.initParam = AdbCtrl.InitParam()
		self.isInitialized  = False

		return

	def Initialize(self, param: InitParam) -> None:
		"""
		初期化
		"""
		pass
		if (os.path.isdir(param.AdbRootDir)) == False:
			raise SimpleError(f"ADBのフォルダパス({param.AdbRootDir})が不正です")

		self.initParam = param
		self.Connect()
		self.isInitialized  = True

		return 

	def Dispose(self) -> None:
		"""
		解放
		"""
		pass
	
		self.isInitialized  = False
		self.Disconnect()

		return

	def IsInitialized(self) -> bool:
		"""
		初期化済みステータス
		"""
		pass

		return self.isInitialized

	def Sleep(self, msec: int) -> None:
		"""
		"""
		pass

		print(f"sleep {msec}")
		time.sleep(msec/1000.0)

		return

	def ExecDevices(self) -> list[Device]:
		pass

		recvs = self.Query(f"adb devices")
		
		devices : list[AdbCtrl.Device] = []
		for recv in recvs:
			arr = recv.split('\t')
			if (len(arr) != 2):
				continue
			device = AdbCtrl.Device()
			device.DeviceId = arr[0]
			device.DeviceName = arr[1]
			devices.append(device)

		return devices

	def ExecTap(self, device: Device , x: int, y: int) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell input touchscreen tap {int(x)} {int(y)}"
		self.NonQuery(send)

		return 

	def ExecSwipe(self, device : Device, x1: int, y1 : int, x2 : int, y2 : int, timeMs : int) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell input touchscreen swipe {x1} {y1} {x2} {y2} {timeMs}"
		self.NonQuery(send)

		return

	def ExecScreenCap(self, device : Device) -> any:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}exec-out screencap -p"
		png_bytes  = self.QueryBin(send)
		if (png_bytes.count == 0):
			raise SimpleError("ExecScreenCap エラー")

		image = cv2.imdecode(np.frombuffer(png_bytes, np.uint8), cv2.IMREAD_COLOR)

		return image

	def ExecKeyEvent(self, device : Device, key : eKeyCode) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell input keyevent {key.value}"
		self.NonQuery(send)

		return

	def ExecInputText(self, device : Device, text : str) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell input text {text}"
		self.NonQuery(send)

		return

	def ExecStartN(self, device : Device, arg : str) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell am start -n {arg}"
		self.NonQuery(send)

		return

	def ExecDumpsysBattery(self, device : Device) -> int:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell dumpsys battery"
		recvs = self.Query(send)
		
		batt = -1
		for recv in recvs:
			if (recv.find('level:') != -1):
				sBatt = recv.replace('level:','')
				batt = int(sBatt)

		return batt

	def GetOptionDeviceStr(self, device : Device) -> str:
		pass

		if (device is None):
			return "";

		return f"-s {device.DeviceId} ";

	def Connect(self) -> None:
		pass
		return 
	
	def Disconnect(self) -> None:
		pass
		return
	
	def NonQuery(self, send : str) -> None: 
		pass

		#recvs = self.Query(send)
		ret = self.QueryBin(send)

		return 
	
	def Query(self, send : str) -> list[str]: 
		pass

		#cmd = f"{self.param.AdbRootDir}\\{send}"
		#print(cmd)
		#result = subprocess.run(cmd,shell=True, stdout=subprocess.PIPE)
		#recv = result.stdout.decode("utf-8")
		recv = self.QueryBin(send).decode("utf-8")

		print(recv)
		recvs : list[str] = []
		recvs += (recv.split("\r\n"))

		return recvs
	
	def QueryBin(self, send : str) -> bytes: 
		pass

		cmd = f"{self.initParam.AdbRootDir}\\{send}"
		print(cmd)
		pipe = subprocess.Popen(f"{self.initParam.AdbRootDir}\\{send}", stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
		recv  = pipe.stdout.read()
		pipe.communicate();

		return recv

順番に見ていきましょう

定義

	class InitParam(object):
		"""
		初期化パラメータ
		"""
		def __init__( self) -> None:
			pass
			#self.AdbRootDir = r"H:\nox_vm\Nox\bin"
			self.AdbRootDir : str = r""				# adb.exeがあるディレクトリパス
			return

クラスを初期化する際に与えるパラメータの構造体です。

パラメータはAdbRootDirのみですね。adb.exeがあるフォルダのパスだけ持っています。

	class Device(object):
		"""
		接続先端末情報モデル
		"""
		def __init__( self, deviceId : str = "" , deviceName : str = "") -> None:
			pass
			self.DeviceId : str = deviceId
			self.DeviceName : str = deviceName
			return

デバイス一覧コマンドの返り値を格納する構造体です。

	class eKeyCode(IntEnum):
		Home = 3
		Back = 4
		Task = 187

Androidに対してキーコードを送る際の列挙ですね。

ホームボタン、戻るボタン、タスク一覧ボタンを押した動作をするためにあります。

コンストラクタ・初期化・Dispose

	def __init__( self) -> None:
		"""
		コンストラクタ
		"""
		pass

		object.__init__(self)
		self.initParam = AdbCtrl.InitParam()
		self.isInitialized  = False

		return

コンストラクタですね。特に大したことはしていません。

	def Initialize(self, param: InitParam) -> None:
		"""
		初期化
		"""
		pass
		if (os.path.isdir(param.AdbRootDir)) == False:
			raise SimpleError(f"ADBのフォルダパス({param.AdbRootDir})が不正です")

		self.initParam = param
		self.Connect()
		self.isInitialized  = True

		return 

初期化関数です。

初期化パラメータを受け取り、クラスを使えるように整えます。

こんな呼ぶ側はイメージで使えますね

			initParam = AdbCtrl.InitParam()
			initParam.AdbRootDir = self.appConfig.AdbRootDir
			self.adbCtrl.Initialize(initParam)
	def Dispose(self) -> None:
		"""
		解放
		"""
		pass
	
		self.isInitialized  = False
		self.Disconnect()

		return

Dispose関数ですね。形式的に作っていますが、大したことはしていません。

	def IsInitialized(self) -> bool:
		"""
		初期化済みステータス
		"""
		pass

		return self.isInitialized

初期化済みかを返す関数です。

コマンド実行関数(実際使うところ)

	def ExecDevices(self) -> list[Device]:
		pass

		recvs = self.Query(f"adb devices")
		
		devices : list[AdbCtrl.Device] = []
		for recv in recvs:
			arr = recv.split('\t')
			if (len(arr) != 2):
				continue
			device = AdbCtrl.Device()
			device.DeviceId = arr[0]
			device.DeviceName = arr[1]
			devices.append(device)

		return devices

接続できそうな端末の一覧を列挙し、Device構造体のリストに入れて返しています。

	def ExecTap(self, device: Device , x: int, y: int) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell input touchscreen tap {int(x)} {int(y)}"
		self.NonQuery(send)

		return 

x,yで指定した座標をタップします。

deviceはNoneでも接続可能な一台目に繋がります。

	def ExecSwipe(self, device : Device, x1: int, y1 : int, x2 : int, y2 : int, timeMs : int) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell input touchscreen swipe {x1} {y1} {x2} {y2} {timeMs}"
		self.NonQuery(send)

		return

x1,y1で指定した場所からx2,y2で指定した場所までスワイプします。

スワイプの時間はtimeMsで指定します。

	def ExecScreenCap(self, device : Device) -> any:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}exec-out screencap -p"
		png_bytes  = self.QueryBin(send)
		if (png_bytes.count == 0):
			raise SimpleError("ExecScreenCap エラー")

		image = cv2.imdecode(np.frombuffer(png_bytes, np.uint8), cv2.IMREAD_COLOR)

		return image

スクリーンショット画像を返します。cv2のimage型になります。

	def ExecKeyEvent(self, device : Device, key : eKeyCode) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell input keyevent {key.value}"
		self.NonQuery(send)

		return

キーイベントを送ります。ホームボタンや戻るボタンを押した動作ができます。

	def ExecStartN(self, device : Device, arg : str) -> None:
		pass

		send = f"adb {self.GetOptionDeviceStr(device)}shell am start -n {arg}"
		self.NonQuery(send)

		return

argにアプリを指定するとそのアプリを起動します。

使い方

クラスの実体生成

		self.adbCtrl = AdbCtrl()
			initParam = AdbCtrl.InitParam()
			initParam.AdbRootDir = self.appConfig.AdbRootDir
			self.adbCtrl.Initialize(initParam)

解放するとき

		self.adbCtrl.Dispose()

タップするときx=100,y=200の座標

self.adbCtrl.ExecTap(None, 100, 200)

スワイプするときx=100の位置でy=100からy=200の位置まで1秒かける

self.adbCtrl.ExecSwipe(device, 100, 100, 100, 200, 1000)

スクリーンショットを取得するとき

image = self.adbCtrl.ExecScreenCap(device)

最後に

実際にこのクラスを使用して、定型作業を自動化することもできます。

画像を取れて、任意の位置をタップできるので、プログラム次第で何でも制御できると思います。

OpenCVと組み合わせると決まったボタンを順番にタップするなんてこともできます。

コメント

タイトルとURLをコピーしました