今回は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と組み合わせると決まったボタンを順番にタップするなんてこともできます。
コメント