├── .idea ├── .gitignore ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── mumuApi.iml ├── README.md ├── mumu ├── __pycache__ │ ├── config.cpython-38.pyc │ ├── constant.cpython-38.pyc │ ├── control.cpython-38.pyc │ ├── mumu.cpython-38.pyc │ └── utils.cpython-38.pyc ├── api │ ├── adb │ │ └── Adb.py │ ├── core │ │ ├── Core.py │ │ ├── __pycache__ │ │ │ ├── Core.cpython-38.pyc │ │ │ ├── app.cpython-38.pyc │ │ │ ├── performance.cpython-38.pyc │ │ │ ├── power.cpython-38.pyc │ │ │ ├── shortcut.cpython-38.pyc │ │ │ ├── simulation.cpython-38.pyc │ │ │ └── window.cpython-38.pyc │ │ ├── app.py │ │ ├── performance.py │ │ ├── power.py │ │ ├── shortcut.py │ │ ├── simulation.py │ │ └── window.py │ ├── develop │ │ ├── __pycache__ │ │ │ └── androidevent.cpython-38.pyc │ │ └── androidevent.py │ ├── driver │ │ ├── Driver.py │ │ ├── __pycache__ │ │ │ ├── Driver.cpython-38.pyc │ │ │ └── bridge.cpython-38.pyc │ │ └── bridge.py │ ├── network │ │ ├── Network.py │ │ └── __pycache__ │ │ │ └── Network.cpython-38.pyc │ ├── permission │ │ ├── Permission.py │ │ ├── __pycache__ │ │ │ ├── Permission.cpython-38.pyc │ │ │ └── root.cpython-38.pyc │ │ └── root.py │ ├── screen │ │ ├── __pycache__ │ │ │ └── screen.cpython-38.pyc │ │ ├── gui.py │ │ └── screen.py │ └── setting │ │ ├── __pycache__ │ │ └── setting.cpython-38.pyc │ │ └── setting.py ├── config.py ├── constant.py ├── control.py ├── mumu.py └── utils.py ├── requirements.txt └── test.py /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 48 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/mumuApi.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mumu模拟器Python API 2 | 3 | - [Mumu模拟器Python API](#mumu模拟器python-api) 4 | - [项目介绍](#项目介绍) 5 | - [如何使用?](#如何使用) 6 | - [设置MuMuManager路径](#设置mumumanager路径) 7 | - [模拟器索引说明](#模拟器索引说明) 8 | - [选择模拟器](#选择模拟器) 9 | - [举个例子](#举个例子) 10 | - [注意](#注意) 11 | - [API类](#api类) 12 | - [驱动类(driver)](#驱动类driver) 13 | - [网络桥接驱动(bride)](#网络桥接驱动bride) 14 | - [安装桥接驱动(install)](#安装桥接驱动install) 15 | - [卸载桥接驱动(uninstall)](#卸载桥接驱动uninstall) 16 | - [权限类(permission)](#权限类permission) 17 | - [\* ROOT权限(root)](#-root权限root) 18 | - [开启ROOT权限(enable)](#开启root权限enable) 19 | - [关闭ROOT权限(disable)](#关闭root权限disable) 20 | - [电源类(power)](#电源类power) 21 | - [启动模拟器(start)](#启动模拟器start) 22 | - [关闭模拟器(shutdown|stop)](#关闭模拟器shutdownstop) 23 | - [重启模拟器(restart|reboot)](#重启模拟器restartreboot) 24 | - [窗口类(window)](#窗口类window) 25 | - [显示窗口(show)](#显示窗口show) 26 | - [隐藏窗口(hidden)](#隐藏窗口hidden) 27 | - [调整窗口大小或位置(layout)](#调整窗口大小或位置layout) 28 | - [应用类(app)](#应用类app) 29 | - [安装一个应用(install)](#安装一个应用install) 30 | - [卸载一个应用(uninstall)](#卸载一个应用uninstall) 31 | - [启动模拟器里的应用(launch)](#启动模拟器里的应用launch) 32 | - [关闭模拟器里的应用(close)](#关闭模拟器里的应用close) 33 | - [\* 判断应用是否存在(exists)](#-判断应用是否存在exists) 34 | - [\* 判断应用是否不存在(doesntExists)](#-判断应用是否不存在doesntexists) 35 | - [获取已经安装的应用列表(get\_installed)](#获取已经安装的应用列表get_installed) 36 | - [\* 获取应用状态(state)](#-获取应用状态state) 37 | - [核心类(core)](#核心类core) 38 | - [创建模拟器(create)](#创建模拟器create) 39 | - [克隆模拟器(clone)](#克隆模拟器clone) 40 | - [删除模拟器(delete)](#删除模拟器delete) 41 | - [重命名模拟器(rename)](#重命名模拟器rename) 42 | - [备份模拟器(export)](#备份模拟器export) 43 | - [导入模拟器(import\_)](#导入模拟器import_) 44 | - [限制CPU使用率(limit\_cpu)](#限制cpu使用率limit_cpu) 45 | - [安卓事件类(androidEvent)](#安卓事件类androidevent) 46 | - [屏幕旋转(rotate)](#屏幕旋转rotate) 47 | - [返回主页(go\_home)](#返回主页go_home) 48 | - [返回(back)](#返回back) 49 | - [窗口置顶(top\_most)](#窗口置顶top_most) 50 | - [窗口全屏(fullscreen)](#窗口全屏fullscreen) 51 | - [摇一摇(shake)](#摇一摇shake) 52 | - [截图(screenshot)](#截图screenshot) 53 | - [音量增加(volume\_up)](#音量增加volume_up) 54 | - [音量减少(volume\_down)](#音量减少volume_down) 55 | - [音量静音(volume\_mute)](#音量静音volume_mute) 56 | - [任务键(go\_task)](#任务键go_task) 57 | - [修改虚拟定位(location)](#修改虚拟定位location) 58 | - [修改重力感应(gyro)](#修改重力感应gyro) 59 | - [快捷方式类(shortcut)](#快捷方式类shortcut) 60 | - [创建桌面快捷方式(create)](#创建桌面快捷方式create) 61 | - [删除桌面快捷方式(delete)](#删除桌面快捷方式delete) 62 | - [机型类(simulation)](#机型类simulation) 63 | - [修改MAC地址(mac\_address)](#修改mac地址mac_address) 64 | - [修改IMEI(imei)](#修改imeiimei) 65 | - [修改IMSI(imsi)](#修改imsiimsi) 66 | - [修改Android ID(android\_id)](#修改android-idandroid_id) 67 | - [设置模拟器设备型号(model)](#设置模拟器设备型号model) 68 | - [设置模拟器主板品牌(brand)](#设置模拟器主板品牌brand) 69 | - [设置模拟器硬件制造商(solution)](#设置模拟器硬件制造商solution) 70 | - [设置模拟器手机号码(phone\_number)](#设置模拟器手机号码phone_number) 71 | - [设置模拟器GPU型号(gpu\_model)](#设置模拟器gpu型号gpu_model) 72 | - [配置类(setting)](#配置类setting) 73 | - [获取模拟器配置所有配置项(all)](#获取模拟器配置所有配置项all) 74 | - [获取一个或多个配置项(get)](#获取一个或多个配置项get) 75 | - [修改一个或多个配置(set)](#修改一个或多个配置set) 76 | - [根据JSON文件内容修改配置(set\_by\_json)](#根据json文件内容修改配置set_by_json) 77 | - [\*判断某个配置是否等于某个值(equal)](#判断某个配置是否等于某个值equal) 78 | - [\*判断某个配置是否不等于某个值(not\_equal)](#判断某个配置是否不等于某个值not_equal) 79 | - [\*判断某个配置等于某个值时,修改为另一个值(equal\_then\_set)](#判断某个配置等于某个值时修改为另一个值equal_then_set) 80 | - [\*判断某个配置不等于某个值时,修改为另一个值(not\_equal\_then\_set)](#判断某个配置不等于某个值时修改为另一个值not_equal_then_set) 81 | - [\*屏幕类(screen)](#屏幕类screen) 82 | - [调整模拟器分辨率(resolution)](#调整模拟器分辨率resolution) 83 | - [设置为手机分辨率(resolution\_mobile)](#设置为手机分辨率resolution_mobile) 84 | - [设置为平板分辨率(resolution\_tablet)](#设置为平板分辨率resolution_tablet) 85 | - [设置为超宽屏分辨率(resolution\_ultrawide)](#设置为超宽屏分辨率resolution_ultrawide) 86 | - [调整模拟器DPI(dpi)](#调整模拟器dpidpi) 87 | - [调整模拟器亮度(brightness)](#调整模拟器亮度brightness) 88 | - [调整模拟器最大帧率(max\_frame\_rate)](#调整模拟器最大帧率max_frame_rate) 89 | - [设置动态调整帧率(dynamic\_adjust\_frame\_rate)](#设置动态调整帧率dynamic_adjust_frame_rate) 90 | - [设置垂直同步(vertical\_sync)](#设置垂直同步vertical_sync) 91 | - [显示帧率(show\_frame\_rate)](#显示帧率show_frame_rate) 92 | - [设置窗口自动旋转(window\_auto\_rotate)](#设置窗口自动旋转window_auto_rotate) 93 | - [性能类(performance)](#性能类performance) 94 | - [设置CPU和内存(set)](#设置cpu和内存set) 95 | - [设置CPU个数(cpu)](#设置cpu个数cpu) 96 | - [设置内存大小(memory)](#设置内存大小memory) 97 | - [设置强制使用独立显卡(force\_discrete\_graphics)](#设置强制使用独立显卡force_discrete_graphics) 98 | - [显存使用策略(renderer\_strategy)](#显存使用策略renderer_strategy) 99 | - [设置磁盘类型(disk\_readonly)](#设置磁盘类型disk_readonly) 100 | - [\*网络类(network)](#网络类network) 101 | - [获取所有可被桥接的网卡(get\_bridge\_card)](#获取所有可被桥接的网卡get_bridge_card) 102 | - [设置网络桥接模式(bridge)](#设置网络桥接模式bridge) 103 | - [设置网络为NAT模式(nat)](#设置网络为nat模式nat) 104 | - [设置桥接模式IP设置方式为DHCP(bridge\_dhcp)](#设置桥接模式ip设置方式为dhcpbridge_dhcp) 105 | - [设置桥接模式IP设置方式为静态(bridge\_static)](#设置桥接模式ip设置方式为静态bridge_static) 106 | - [ADB类(adb)](#adb类adb) 107 | - [获取模拟器的ADB连接信息(get\_connect\_info)](#获取模拟器的adb连接信息get_connect_info) 108 | - [点击屏幕(click)](#点击屏幕click) 109 | - [滑动屏幕(swipe)](#滑动屏幕swipe) 110 | - [文本输入(input\_text)](#文本输入input_text) 111 | - [模拟按键(key\_event)](#模拟按键key_event) 112 | - [上传文件(push)](#上传文件push) 113 | - [上传文件到Download目录(push\_download)](#上传文件到download目录push_download) 114 | - [下载文件(pull)](#下载文件pull) 115 | - [清理应用数据(clear)](#清理应用数据clear) 116 | - [GUI自动化类(auto)](#gui自动化类auto) 117 | - [处理模拟器实时帧(create\_handle)](#处理模拟器实时帧create_handle) 118 | - [保存模拟器实时帧(save)](#保存模拟器实时帧save) 119 | - [在帧中查找第一个图片(locateOnScreen)](#在帧中查找第一个图片locateonscreen) 120 | - [在帧中查找第一张图片的中心点(locateCenterOnScreen)](#在帧中查找第一张图片的中心点locatecenteronscreen) 121 | - [在帧中查找所有图片(locateAllOnScreen)](#在帧中查找所有图片locateallonscreen) 122 | - [获取Box的中心点(center)](#获取box的中心点center) 123 | - [实战举例](#实战举例) 124 | - [支持本项目](#支持本项目) 125 | 126 | ## 项目介绍 127 | 128 | 该项目是基于MuMu提供的``MuMuManager.exe``实现的Python API,可以通过Python代码控制MuMu模拟器的大量操作。 129 | 130 | 该项目要求你已经安装了MuMu模拟器,且Mumu模拟器版本`>=4.0.0`以上。 131 | 132 | ## 如何使用? 133 | 134 | 将本项目安装到您的Python环境中 135 | 136 | 导入模块 137 | 138 | ```python 139 | from mumu.mumu import Mumu 140 | ``` 141 | 142 | ### 设置MuMuManager路径 143 | 144 | 如果你的Mumu模拟器不是使用``默认路径``安装,则需要在创建Mumu对象时传入MuMuManager的路径。 145 | 默认路径``C:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe`` 146 | 147 | ```python 148 | from mumu.mumu import Mumu 149 | 150 | Mumu(r'your_path').select(1) 151 | ``` 152 | 153 | ### 模拟器索引说明 154 | 155 | 在Mumu模拟器种,操作模拟器通过索引,索引可以在创建时指定,也可以创建时自动分配,如果你不知道你的模拟器索引是多少,可以打开“Mumu多开器”,找到你的模拟器,最前面那个数字就是你的模拟器索引。 156 | 157 | ### 选择模拟器 158 | 159 | 如果指定的操作需要操作模拟器,可以通过``select``方法选择模拟器 160 | 161 | ```python 162 | from mumu.mumu import Mumu 163 | 164 | mumu = Mumu().select(1) 165 | ``` 166 | 167 | 对于选择多个模拟器时可以这样,一下4种方式都是等价的,均可以选择1、2、3号模拟器 168 | 169 | ```python 170 | from mumu.mumu import Mumu 171 | 172 | # 第一种 173 | mumu = Mumu().select([1, 2, 3]) 174 | # 第二种 175 | mumu = Mumu().select(1, 2, 3) 176 | # 第三种 177 | mumu = Mumu().select((1, 2, 3)) 178 | # 也可以混合使用 179 | mumu = Mumu().select([1, 2], 3) 180 | ``` 181 | 182 | 如果希望选择所有模拟器,可以使用以下两种方法 183 | 184 | ```python 185 | from mumu.mumu import Mumu 186 | 187 | # 第一种 188 | mumu = Mumu().select() # 当select什么都不传时,默认选择所有模拟器 189 | # 第二种:通过all方法选择所有模拟器 190 | mumu = Mumu().all() 191 | ``` 192 | 193 | ### 举个例子 194 | 195 | 我希望开启索引为1的模拟器的root权限,然后启动它。 196 | 197 | ```python 198 | from mumu.mumu import Mumu 199 | 200 | # 选择索引为1的模拟器 201 | mumu = Mumu().select(1) 202 | # 开启Root权限 203 | mumu.permission.root.enable() 204 | # 启动模拟器 205 | mumu.power.start() 206 | ``` 207 | 208 | ### 注意 209 | 210 | 带`*`的类是本项目提供的`超类`,并不是MuMu模拟器的API原生提供。 211 | 212 | ## API类 213 | 214 | 本项目提供了多个操作类,可以通过这些类实现对模拟器的控制。 215 | 216 | ### 驱动类(driver) 217 | 218 | 说明:具官方文档,目前仅支持网络桥接驱动。 219 | 220 | #### 网络桥接驱动(bride) 221 | 222 | ##### 安装桥接驱动(install) 223 | 224 | 说明:安装桥接驱动需要管理员权限 225 | 226 | ```python 227 | Mumu().driver.bridge.install() 228 | ``` 229 | 230 | ##### 卸载桥接驱动(uninstall) 231 | 232 | 说明:卸载桥接驱动需要管理员权限 233 | 234 | ```python 235 | Mumu().driver.bridge.uninstall() 236 | ``` 237 | 238 | ### 权限类(permission) 239 | 240 | #### * ROOT权限(root) 241 | 242 | ##### 开启ROOT权限(enable) 243 | 244 | 说明:开启ROOT权限需要在模拟器关机状态下进行 245 | 246 | ```python 247 | Mumu().select('your_index').permission.root.enable() 248 | ``` 249 | 250 | ##### 关闭ROOT权限(disable) 251 | 252 | 说明:关闭ROOT权限需要在模拟器关机状态下进行 253 | 254 | ```python 255 | Mumu().select('your_index').permission.root.disable() 256 | ``` 257 | 258 | ### 电源类(power) 259 | 260 | #### 启动模拟器(start) 261 | 262 | ```python 263 | Mumu().select('your_index').power.start() 264 | ``` 265 | 266 | 启动完成后打开指定包名的应用 267 | 268 | ```python 269 | Mumu().select('your_index').power.start('com.tencent.mobileqq') 270 | ``` 271 | 272 | #### 关闭模拟器(shutdown|stop) 273 | 274 | ```python 275 | Mumu().select('your_index').power.shutdown() 276 | # or 277 | Mumu().select('your_index').power.stop() 278 | ``` 279 | 280 | #### 重启模拟器(restart|reboot) 281 | 282 | ```python 283 | Mumu().select('your_index').power.restart() 284 | # or 285 | Mumu().select('your_index').power.reboot() 286 | ``` 287 | 288 | ### 窗口类(window) 289 | 290 | #### 显示窗口(show) 291 | 292 | ```python 293 | Mumu().select('your_index').window.show() 294 | ``` 295 | 296 | #### 隐藏窗口(hidden) 297 | 298 | ```python 299 | Mumu().select('your_index').window.hidden() 300 | ``` 301 | 302 | #### 调整窗口大小或位置(layout) 303 | 304 | 该方法可以接受4个参数,分别是x坐标、y坐标、宽度、高度 305 | 306 | ```python 307 | Mumu().select('your_index').window.layout(0, 0, 800, 600) 308 | ``` 309 | 310 | 如果只希望调整窗口的大小 311 | 312 | ```python 313 | Mumu().select('your_index').window.layout(None, None, 1080, 1920) 314 | ``` 315 | 316 | 也可以只调整一个参数 317 | 318 | ```python 319 | Mumu().select('your_index').window.layout(300, None, None, None) 320 | # or 321 | Mumu().select('your_index').window.layout(None, None, 1080, None) 322 | ``` 323 | 324 | ### 应用类(app) 325 | 326 | #### 安装一个应用(install) 327 | 328 | 该方法接受一个参数,即apk文件的路径,当apk路径无法访问时,会抛出``FileNotFoundError``异常 329 | 330 | ```python 331 | Mumu().select('your_index').app.install(r'C:\test.apk') 332 | ``` 333 | 334 | #### 卸载一个应用(uninstall) 335 | 336 | 该方法接受一个参数,即应用的包名 337 | 338 | ```python 339 | Mumu().select('your_index').app.uninstall('com.miHoYo.Yuanshen') 340 | ``` 341 | 342 | #### 启动模拟器里的应用(launch) 343 | 344 | 该方法接受一个参数,即应用的包名 345 | 346 | ```python 347 | # 启动原神 348 | Mumu().select('your_index').app.launch('com.miHoYo.Yuanshen') 349 | ``` 350 | 351 | #### 关闭模拟器里的应用(close) 352 | 353 | 该方法接受一个参数,即应用的包名 354 | 355 | ```python 356 | # 关闭原神 357 | Mumu().select('your_index').app.close('com.miHoYo.Yuanshen') 358 | ``` 359 | 360 | #### * 判断应用是否存在(exists) 361 | 362 | 该方法接受一个参数,即应用的包名 363 | 364 | ```python 365 | # 判断原神是否存在 366 | if (Mumu().select('your_index').app.exists('com.miHoYo.Yuanshen')): 367 | print('原神已安装') 368 | else: 369 | print('原神未安装') 370 | ``` 371 | 372 | #### * 判断应用是否不存在(doesntExists) 373 | 374 | 该方法接受一个参数,即应用的包名 375 | 376 | ```python 377 | # 判断原神是否不存在 378 | if (Mumu().select('your_index').app.doesntExists('com.miHoYo.Yuanshen')): 379 | print('原神未安装') 380 | else: 381 | print('原神已安装') 382 | ``` 383 | 384 | #### 获取已经安装的应用列表(get_installed) 385 | 386 | ```python 387 | # 获取已经安装的应用列表 388 | Mumu().select(1).app.get_installed() 389 | ``` 390 | 391 | 返回一个列表,当没有安装任何应用时,返回一个空列表 392 | 393 | ```python 394 | [ 395 | { 396 | 'package': 'com.miHoYo.Yuanshen', # 包名 397 | 'app_name': '原神', # 应用名称 398 | 'version': '4.1.8' # 版本号 399 | }, 400 | ] 401 | ``` 402 | 403 | #### * 获取应用状态(state) 404 | 405 | 该方法接受一个参数,即应用的包名,返回一个字符串,`running`表示应用正在运行,`stopped`表示应用未运行,`not_installed`表示应用未安装 406 | 407 | ```python 408 | # 获取原神的状态 409 | Mumu().select('your_index').app.state('com.miHoYo.Yuanshen') 410 | ``` 411 | 412 | ### 核心类(core) 413 | 414 | #### 创建模拟器(create) 415 | 416 | 该方法接受一个参数,即模拟器的名称,返回一个list包含所创建的模拟器索引 417 | 418 | 举例:创建一个模拟器 419 | 420 | ```python 421 | Mumu().core.create() 422 | ``` 423 | 424 | 举例:创建5个模拟器 425 | 426 | ```python 427 | Mumu().core.create(5) 428 | ``` 429 | 430 | 举例:创建一个索引为``10``的模拟器 431 | 432 | ```python 433 | Mumu().select(10).core.create() 434 | ``` 435 | 436 | 从索引3开始创建5个模拟器 437 | 438 | ```python 439 | Mumu().select(3).core.create(5) 440 | ``` 441 | 442 | 从索引 3,20 开始,分别创建10次模拟器,索引依次递增(即创建3-12,20-29索引的模拟器) 443 | 444 | ```python 445 | Mumu().select(3, 20).core.create(10) 446 | ``` 447 | 448 | #### 克隆模拟器(clone) 449 | 450 | 该方法接受一个参数,即为克隆的数量,但是调用该方法前需要先选择一个模拟器,返回一个list包含所创建的模拟器索引 451 | 452 | 举例:克隆索引为2的模拟器 453 | 454 | ```python 455 | Mumu().select(2).core.clone() 456 | ``` 457 | 458 | 举例:复制索引为 2,4,6 的模拟器 459 | 460 | ```python 461 | Mumu().select(2, 4, 6).core.clone() 462 | ``` 463 | 464 | 举例:复制索引为2的模拟器,复制10次 465 | 466 | ```python 467 | Mumu().select(2).core.clone(10) 468 | ``` 469 | 470 | 举例:复制所有模拟器 471 | 472 | ```python 473 | Mumu().all().core.clone() 474 | ``` 475 | 476 | #### 删除模拟器(delete) 477 | 478 | 该方法不需要传入参数,但是调用该方法前需要先选择一个模拟器,返回bool值,表示是否删除成功 479 | 480 | 举例:删除索引为2的模拟器 481 | 482 | ```python 483 | if Mumu().select(2).core.delete(): 484 | print('删除成功') 485 | ``` 486 | 487 | 举例:删除索引为2,4,6的模拟器 488 | 489 | ```python 490 | Mumu().select(2, 4, 6).core.delete() 491 | ``` 492 | 493 | 举例:删除所有模拟器(危险操作) 494 | 495 | ```python 496 | Mumu().all().core.delete() 497 | ``` 498 | 499 | #### 重命名模拟器(rename) 500 | 501 | 该方法接受一个参数,即为新的模拟器名称,但是调用该方法前需要先选择一个模拟器,返回bool值,表示是否重命名成功。 502 | 503 | 举例:重命名索引为2的模拟器为“测试” 504 | 505 | ```python 506 | if Mumu().select(2).core.rename('测试'): 507 | print('重命名成功') 508 | ``` 509 | 510 | 举例:重命名索引为2,4,6的模拟器为“测试” 511 | 512 | ```python 513 | Mumu().select(2, 4, 6).core.rename('测试') 514 | ``` 515 | 516 | 举例:重命名所有的模拟器为“测试” 517 | 518 | ```python 519 | Mumu().all().core.rename('测试') 520 | ``` 521 | 522 | #### 备份模拟器(export) 523 | 524 | 该方法接受三个参数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否备份成功。 525 | 526 | | 参数 | 说明 | 527 | |------|-----------| 528 | | dir | 备份的路径 | 529 | | name | 备份的名称 | 530 | | zip | 是否使用zip压缩 | 531 | 532 | 举例:备份索引为2的模拟器到C盘目录 backup 下,名称为 test.mumudata,以非压缩的格式 533 | 534 | ```python 535 | if Mumu().select(2).core.export(r'C:\backup', 'test'): 536 | print('备份成功') 537 | ``` 538 | 539 | 举例:备份索引为2,4,6的模拟器到C盘目录 backup 下,文件名基于 test 自动加后缀,以非压缩的格式 540 | 541 | ```python 542 | Mumu().select(2, 4, 6).core.export(r'C:\backup', 'test') 543 | ``` 544 | 545 | 举例:备份所有的模拟器到C盘目录 backup 下,文件名基于 test 自动加后缀,以压缩的格式 546 | 547 | ```python 548 | Mumu().all().core.export(r'C:\backup', 'test', True) 549 | ``` 550 | 551 | #### 导入模拟器(import_) 552 | 553 | 该方法接受一个参数,即为备份文件的路径,如果传入一个列表,则表示导入多个,调用该方法前需要先选择一个模拟器,返回bool值,表示是否导入成功。 554 | 555 | 举例:导入C盘下的 test.mumudata 并创建模拟器 556 | 557 | ```python 558 | if Mumu().select(2).core.import_(r'C:\test.mumudata'): 559 | print('导入成功') 560 | ``` 561 | 562 | 举例:导入C盘下的 test.mumudata 并创建模拟器,导入10次 563 | 564 | ```python 565 | Mumu().select(2).core.import_(r'C:\test.mumudata', 10) 566 | ``` 567 | 568 | 举例:导入C盘下的 test.mumudata 和D盘下的 test2.mumudata 并创建模拟器,分别导入10次 569 | 570 | ```python 571 | Mumu().select(2).core.import_([r'C:\test.mumudata', r'D:\test2.mumudata'], 10) 572 | ``` 573 | 574 | #### 限制CPU使用率(limit_cpu) 575 | 576 | 该方法接受一个参数,即为CPU使用率,调用该方法前需要先选择一个模拟器,返回bool值,表示是否设置成功。 577 | 578 | 举例:在索引为2的模拟器中限制CPU为50% 579 | 580 | ```python 581 | Mumu().select(2).core.limit_cpu(50) 582 | ``` 583 | 584 | 举例:在索引为2,4,6的模拟器中限制CPU为50% 585 | 586 | ```python 587 | Mumu().select(2, 4, 6).core.limit_cpu(50) 588 | ``` 589 | 590 | 举例:在所有模拟器中限制CPU为50% 591 | 592 | ```python 593 | Mumu().all().core.limit_cpu(50) 594 | ``` 595 | 596 | ### 安卓事件类(androidEvent) 597 | 598 | 该类提供了安卓事件操作,可以通过该类实现对模拟器的操作。 599 | 600 | 调用类的所有方法前需要先选择一个模拟器,返回bool值,表示是否操作成功。 601 | 方法未特殊说明时,均无需传入参数。 602 | 603 | #### 屏幕旋转(rotate) 604 | 605 | ```python 606 | Mumu().select(1).androidEvent.rotate() 607 | ``` 608 | 609 | #### 返回主页(go_home) 610 | 611 | ```python 612 | Mumu().select(1).androidEvent.go_home() 613 | ``` 614 | 615 | #### 返回(back) 616 | 617 | ```python 618 | Mumu().select(1).androidEvent.go_back() 619 | ``` 620 | 621 | #### 窗口置顶(top_most) 622 | 623 | ```python 624 | Mumu().select(1).androidEvent.top_most() 625 | ``` 626 | 627 | #### 窗口全屏(fullscreen) 628 | 629 | ```python 630 | Mumu().select(1).androidEvent.fullscreen() 631 | ``` 632 | 633 | #### 摇一摇(shake) 634 | 635 | ```python 636 | Mumu().select(1).androidEvent.shake() 637 | ``` 638 | 639 | #### 截图(screenshot) 640 | 641 | ```python 642 | Mumu().select(1).androidEvent.screenshot() 643 | ``` 644 | 645 | #### 音量增加(volume_up) 646 | 647 | ```python 648 | Mumu().select(1).androidEvent.volume_up() 649 | ``` 650 | 651 | #### 音量减少(volume_down) 652 | 653 | ```python 654 | Mumu().select(1).androidEvent.volume_down() 655 | ``` 656 | 657 | #### 音量静音(volume_mute) 658 | 659 | ```python 660 | Mumu().select(1).androidEvent.volume_mute() 661 | ``` 662 | 663 | #### 任务键(go_task) 664 | 665 | ```python 666 | Mumu().select(1).androidEvent.go_task() 667 | ``` 668 | 669 | #### 修改虚拟定位(location) 670 | 671 | 该方法接受两个参数,分别是经度和纬度 672 | 673 | 举例:在索引为2的模拟器中修改虚拟定位为经度114.1,纬度-23 674 | 675 | ```python 676 | Mumu().select(2).androidEvent.location(114.1, -23) 677 | ``` 678 | 679 | 举例: 在索引为2,4,6的模拟器中修改虚拟定位为经度114.1,纬度-23 680 | 681 | ```python 682 | Mumu().select(2, 4, 6).androidEvent.location(114.1, -23) 683 | ``` 684 | 685 | 举例:在所有模拟器中修改虚拟定位为经度114.1,纬度-23 686 | 687 | ```python 688 | Mumu().all().androidEvent.location(114.1, -23) 689 | ``` 690 | 691 | #### 修改重力感应(gyro) 692 | 693 | 该方法接受三个参数,分别是x轴、y轴、z轴 694 | 695 | 举例:在索引为2的模拟器中修改重力感应X=40,Y=20,Z=30 696 | 697 | ```python 698 | Mumu().select(2).androidEvent.gyro(40, 20, 30) 699 | ``` 700 | 701 | 举例:在索引为2,4,6的模拟器中修改重力感应X=40,Y=20,Z=30 702 | 703 | ```python 704 | Mumu().select(2, 4, 6).androidEvent.gyro(40, 20, 30) 705 | ``` 706 | 707 | 举例:在所有模拟器中修改重力感应X=40,Y=20,Z=30 708 | 709 | ```python 710 | Mumu().all().androidEvent.gyro(40, 20, 30) 711 | ``` 712 | 713 | ### 快捷方式类(shortcut) 714 | 715 | #### 创建桌面快捷方式(create) 716 | 717 | 该方法接受三个参数,分别是快捷方式名称、快捷方式图标路径、应用包名 718 | 719 | 举例:在桌面创建索引为2的模拟器的快捷方式 test,图标用 C 盘的 test.ico,自动启动原神 720 | 721 | ```python 722 | Mumu().select(2).shortcut.create('test', r'C:\test.ico', 'com.miHoYo.Yuanshen') 723 | ``` 724 | 725 | 举例:在桌面创建索引为2,4,6的模拟器的快捷方式 test,图标用 C 盘的 test.ico,自动启动原神 726 | 727 | ```python 728 | Mumu().select(2, 4, 6).shortcut.create('test', r'C:\test.ico', 'com.miHoYo.Yuanshen') 729 | ``` 730 | 731 | 举例:在所有模拟器中创建快捷方式 test,图标用 C 盘的 test.ico,自动启动原神 732 | 733 | ```python 734 | Mumu().all().shortcut.create('test', r'C:\test.ico', 'com.miHoYo.Yuanshen') 735 | ``` 736 | 737 | #### 删除桌面快捷方式(delete) 738 | 739 | 该方法无需传入参数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否删除成功。 740 | 741 | 举例:删除索引为2的模拟器的快捷方式 742 | 743 | ```python 744 | Mumu().select(2).shortcut.delete() 745 | ``` 746 | 747 | 举例:删除索引为2,4,6的模拟器的快捷方式 748 | 749 | ```python 750 | Mumu().select(2, 4, 6).shortcut.delete() 751 | ``` 752 | 753 | 举例:删除所有模拟器的快捷方式 754 | 755 | ```python 756 | Mumu().all().shortcut.delete() 757 | ``` 758 | 759 | ### 机型类(simulation) 760 | 761 | 该类提供了模拟器机型操作。这玩意非常的鸡肋!! 762 | 763 | #### 修改MAC地址(mac_address) 764 | 765 | 该方法接受一个参数,即为新的MAC地址,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 766 | 767 | ```python 768 | Mumu().select(1).simulation.mac_address('00:11:22:33:44:55') 769 | ``` 770 | 771 | 举例:为索引为1的模拟器随机生成一个MAC地址,两种方式均可 772 | 773 | ```python 774 | from mumu.constant import MacAddress 775 | 776 | # 第一种:传入一个MAC地址 777 | Mumu().select(1).simulation.mac_address(MacAddress.random()) 778 | 779 | # 第二种:当不传入参数时,表示随机生成一个MAC地址 780 | Mumu().select(1).simulation.mac_address() 781 | ``` 782 | 783 | #### 修改IMEI(imei) 784 | 785 | 安卓12不允许应用获取IMEI 786 | 787 | 该方法接受一个参数,即为新的IMEI,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 788 | 789 | ```python 790 | Mumu().select(1).simulation.imei('123456789012345') 791 | ``` 792 | 793 | 举例:为索引为1的模拟器随机生成一个IMEI,两种方式均可 794 | 795 | ```python 796 | from mumu.constant import IMEI 797 | 798 | # 第一种:传入一个IMEI 799 | Mumu().select(1).simulation.imei(IMEI.random()) 800 | 801 | # 第二种:当不传入参数时,表示随机生成一个IMEI 802 | Mumu().select(1).simulation.imei() 803 | ``` 804 | 805 | #### 修改IMSI(imsi) 806 | 807 | 安卓12不允许应用获取IMSI 808 | 809 | 该方法接受一个参数,即为新的IMSI,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 810 | 811 | ```python 812 | Mumu().select(1).simulation.imsi('460000000000000') 813 | ``` 814 | 815 | 举例:为索引为1的模拟器随机生成一个IMSI,两种方式均可 816 | 817 | ```python 818 | from mumu.constant import IMSI 819 | 820 | # 第一种:传入一个IMSI 821 | Mumu().select(1).simulation.imsi(IMSI.random()) 822 | 823 | # 第二种:当不传入参数时,表示随机生成一个IMSI 824 | Mumu().select(1).simulation.imsi() 825 | ``` 826 | 827 | #### 修改Android ID(android_id) 828 | 829 | 该方法接受一个参数,即为新的Android ID,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 830 | 831 | ```python 832 | Mumu().select(1).simulation.android_id('1234567890123456') 833 | ``` 834 | 835 | 举例:为索引为1的模拟器随机生成一个Android ID,两种方式均可 836 | 837 | ```python 838 | from mumu.constant import AndroidID 839 | 840 | # 第一种:传入一个Android ID 841 | Mumu().select(1).simulation.android_id(AndroidID.random()) 842 | 843 | # 第二种:当不传入参数时,表示随机生成一个Android ID 844 | Mumu().select(1).simulation.android_id() 845 | ``` 846 | 847 | #### 设置模拟器设备型号(model) 848 | 849 | 该方法接受一个参数,即为新的设备型号,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 850 | 851 | ```python 852 | Mumu().select(1).simulation.model('MI 10') 853 | ``` 854 | 855 | #### 设置模拟器主板品牌(brand) 856 | 857 | 该方法接受一个参数,即为新的主板型号,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 858 | 859 | ```python 860 | Mumu().select(1).simulation.brand('Xiaomi') 861 | ``` 862 | 863 | #### 设置模拟器硬件制造商(solution) 864 | 865 | 该方法接受一个参数,即为新的硬件型号,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 866 | 867 | ```python 868 | Mumu().select(1).simulation.solution('qcom') 869 | ``` 870 | 871 | #### 设置模拟器手机号码(phone_number) 872 | 873 | 安卓12不允许应用获取手机号码 874 | 875 | 该方法接受一个参数,即为新的手机号码,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 876 | 877 | ```python 878 | Mumu().select(1).simulation.phone_number('18888888888') 879 | ``` 880 | 881 | 举例:随机设置一个手机号码,两种方式均可 882 | 883 | ```python 884 | from mumu.constant import PhoneNumber 885 | 886 | # 第一种:传入一个手机号码 887 | Mumu().select(1).simulation.phone_number(PhoneNumber.random()) 888 | 889 | # 第二种:当不传入参数时,表示随机生成一个手机号码 890 | Mumu().select(1).simulation.phone_number() 891 | ``` 892 | 893 | #### 设置模拟器GPU型号(gpu_model) 894 | 895 | 该方法提供4个参数,选填一个即可。 896 | 897 | 举例:设置索引为1的模拟器的GPU型号为GeForce RTX 3090 898 | 899 | ```python 900 | Mumu().select(1).simulation.gpu_model('GeForce RTX 3090') 901 | ``` 902 | 903 | 举例:设置索引为1的模拟器的GPU型号为高配 904 | 905 | ```python 906 | Mumu().select(1).simulation.gpu_model(top_model=True) 907 | ``` 908 | 909 | 举例:设置索引为1的模拟器的GPU型号为低配 910 | 911 | ```python 912 | Mumu().select(1).simulation.gpu_model(low_model=True) 913 | ``` 914 | 915 | 举例:设置索引为1的模拟器的GPU型号为中配 916 | 917 | ```python 918 | Memu().select(1).simulation.gpu_model(middle_model=True) 919 | ``` 920 | 921 | ### 配置类(setting) 922 | 923 | 该类提供了模拟器配置文件操作。 924 | 925 | 在配置类中,不选择任何模拟器即操作全局配置(默认值) 926 | 927 | #### 获取模拟器配置所有配置项(all) 928 | 929 | 该方法提供一个参数`all_writable`,当传入`True`时,表示获取所有可写的配置项,当传入`False`时,表示获取所有配置项,返回一个字典,表示所有配置项的键值对。 930 | 931 | 举例:获取所有配置项 932 | 933 | ```python 934 | Mumu().select(1).setting.all() 935 | ``` 936 | 937 | 举例:获取所有可写的配置项 938 | 939 | ```python 940 | Mumu().select(1).setting.all(True) 941 | ``` 942 | 943 | #### 获取一个或多个配置项(get) 944 | 945 | 该方法接受一个或多个参数,表示要获取的配置项,当获取单个配置项时,返回一个字符串,当获取多个配置项时,返回一个字典,表示所有配置项的键值对。 946 | 947 | 举例:获取指定一个或多个配置(返回字符串) 948 | 949 | ```python 950 | value = Mumu().select(1).setting.get('window_size_fixed') 951 | ``` 952 | 953 | 举例:获取多个配置项(返回字典) 954 | 955 | ```python 956 | dict_val = Mumu().select(1).setting.get('window_size_fixed', 'window_save_rect') 957 | ``` 958 | 959 | #### 修改一个或多个配置(set) 960 | 961 | 该方法接受一个或多个参数,表示要设置的配置项,返回一个bool值,表示是否设置成功。 962 | 963 | 当传入参数的值为`None`时,表示还原默认值。 964 | 965 | 如果遇到需要写的配置键包含`.`或者`-`时,将`.`替换为两个`_`,`-`替换为三个`_`。 966 | 967 | 举例:修改索引为2的模拟器的配置 window_size_fixed 的值为 true 968 | 969 | ```python 970 | Mumu().select(2).setting.set(window_size_fixed=True) 971 | ``` 972 | 973 | 举例:修改索引为2的模拟器的配置 window_size_fixed 的值为 false,配置 window_save_rect 的值为 true 974 | 975 | ```python 976 | Mumu().select(2).setting.set(window_size_fixed=False, window_save_rect=True) 977 | ``` 978 | 979 | 修改索引为2的模拟器的还原配置 window_size_fixed 的值(将使用默认值) 980 | 981 | ```python 982 | Mumu().select(2).setting.set(window_size_fixed=None) 983 | ``` 984 | 985 | #### 根据JSON文件内容修改配置(set_by_json) 986 | 987 | 该方法接受一个参数,即为JSON文件的路径,返回一个bool值,表示是否设置成功。 988 | 989 | 举例: 990 | 991 | 一个 utf8 格式 test.json 文件在C盘下,文件内容如下: 992 | 993 | ```json 994 | { 995 | "window_save_rect": "true", 996 | "window_size_fixed": "false" 997 | } 998 | ``` 999 | 1000 | 修改索引为2的模拟器的配置,通过JSON文件方式修改。 1001 | 1002 | ```python 1003 | Mumu().select(2).setting.set_by_json(r'C:\test.json') 1004 | ``` 1005 | 1006 | #### *判断某个配置是否等于某个值(equal) 1007 | 1008 | 该方法接受两个参数,分别是配置项和值,返回一个bool值,表示是否相等。 1009 | 1010 | 举例:判断索引为2的模拟器的配置 window_size_fixed 是否等于 true 1011 | 1012 | ```python 1013 | if Mumu().select(2).setting.equal('window_size_fixed', True): 1014 | print('相等') 1015 | else: 1016 | print('不相等') 1017 | ``` 1018 | 1019 | #### *判断某个配置是否不等于某个值(not_equal) 1020 | 1021 | 该方法接受两个参数,分别是配置项和值,返回一个bool值,表示是否不相等。 1022 | 1023 | 举例:判断索引为2的模拟器的配置 window_size_fixed 是否不等于 true 1024 | 1025 | ```python 1026 | if Mumu().select(2).setting.not_equal('window_size_fixed', True): 1027 | print('不相等') 1028 | else: 1029 | print('相等') 1030 | ``` 1031 | 1032 | #### *判断某个配置等于某个值时,修改为另一个值(equal_then_set) 1033 | 1034 | 该方法接受三个参数,分别是配置项、值和新值,返回一个bool值,表示是否修改成功。 1035 | 1036 | 举例:判断索引为2的模拟器的配置 window_size_fixed 是否等于 true,如果相等则修改为 false 1037 | 1038 | ```python 1039 | Mumu().select(2).setting.equal_then_set('window_size_fixed', True, False) 1040 | ``` 1041 | 1042 | #### *判断某个配置不等于某个值时,修改为另一个值(not_equal_then_set) 1043 | 1044 | 该方法接受三个参数,分别是配置项、值和新值,返回一个bool值,表示是否修改成功。 1045 | 1046 | 当不传入新值时,将自动设置`值` 1047 | 1048 | 举例:判断索引为2的模拟器的配置 window_size_fixed 是否不等于 true,如果不相等则修改为 true 1049 | 1050 | ```python 1051 | Mumu().select(2).setting.not_equal_then_set('window_size_fixed', True, True) 1052 | 1053 | # or 1054 | 1055 | Mumu().select(2).setting.not_equal_then_set('window_size_fixed', True) 1056 | ``` 1057 | 1058 | 1059 | 1060 | ### *屏幕类(screen) 1061 | 1062 | 该类提供了模拟器屏幕操作。 1063 | 1064 | #### 调整模拟器分辨率(resolution) 1065 | 1066 | 该方法接受两个参数,分别是宽度和高度,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1067 | 1068 | 举例:修改索引为2的模拟器的分辨率为 800x600 1069 | 1070 | ```python 1071 | Mumu().select(2).screen.resolution(800, 600) 1072 | ``` 1073 | 1074 | #### 设置为手机分辨率(resolution_mobile) 1075 | 1076 | 该方法不需要传入参数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1077 | 1078 | 该方法调整的分辨率为 1080x1920 DPI 为 480 1079 | 1080 | 举例:设置索引为2的模拟器的分辨率为手机分辨率 1081 | 1082 | ```python 1083 | Mumu().select(2).screen.resolution_mobile() 1084 | ``` 1085 | 1086 | #### 设置为平板分辨率(resolution_tablet) 1087 | 1088 | 该方法不需要传入参数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1089 | 1090 | 该方法调整的分辨率为 1920x1080 DPI 为 280 1091 | 1092 | 举例:设置索引为2的模拟器的分辨率为平板分辨率 1093 | 1094 | ```python 1095 | Mumu().select(2).screen.resolution_tablet() 1096 | ``` 1097 | 1098 | #### 设置为超宽屏分辨率(resolution_ultrawide) 1099 | 1100 | 该方法不需要传入参数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1101 | 1102 | 该方法调整的分辨率为 3200x1440 DPI 为 400 1103 | 1104 | 举例:设置索引为2的模拟器的分辨率为超宽屏分辨率 1105 | 1106 | ```python 1107 | Mumu().select(2).screen.resolution_ultrawide() 1108 | ``` 1109 | 1110 | #### 调整模拟器DPI(dpi) 1111 | 1112 | 该方法接受一个参数,即为新的DPI,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1113 | 1114 | 举例:修改索引为2的模拟器的DPI为 240 1115 | 1116 | ```python 1117 | Mumu().select(2).screen.dpi(240) 1118 | ``` 1119 | 1120 | #### 调整模拟器亮度(brightness) 1121 | 1122 | 该方法接受一个参数,即为新的亮度,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1123 | 1124 | 参数范围为0-100 1125 | 1126 | 举例:修改索引为2的模拟器的亮度为 50 1127 | 1128 | ```python 1129 | Mumu().select(2).screen.brightness(50) 1130 | ``` 1131 | 1132 | #### 调整模拟器最大帧率(max_frame_rate) 1133 | 1134 | 该方法接受一个参数,即为新的最大帧率,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1135 | 1136 | 参数范围为1-240 1137 | 1138 | 举例:修改索引为2的模拟器的最大帧率为 60 1139 | 1140 | ```python 1141 | Mumu().select(2).screen.max_frame_rate(60) 1142 | # or 1143 | Mumu().select(2).screen.max_frame_rate() # 缺省值为60 1144 | ``` 1145 | 1146 | #### 设置动态调整帧率(dynamic_adjust_frame_rate) 1147 | 1148 | 该方法接受两个参数,分别是`enable`和`dynamic_low_frame_rate_limit`,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1149 | 1150 | ---- 1151 | 1152 | | 参数 | 类型 | 说明 | 1153 | |------------------------------|------|--------------------------| 1154 | | enable | bool | 是否启用动态调整帧率 | 1155 | | dynamic_low_frame_rate_limit | int | 模拟器不是操作主窗口时,降低至的帧率,默认:15 | 1156 | 1157 | ---- 1158 | 1159 | 举例:启用索引为2的模拟器的动态调整帧率,降低至15帧 1160 | 1161 | ```python 1162 | Mumu().select(2).screen.dynamic_adjust_frame_rate(True, 15) 1163 | ``` 1164 | 1165 | 举例:禁用索引为2的模拟器的动态调整帧率 1166 | 1167 | ```python 1168 | Mumu().select(2).screen.dynamic_adjust_frame_rate(False) 1169 | ``` 1170 | 1171 | #### 设置垂直同步(vertical_sync) 1172 | 1173 | 该方法接受一个参数,即为`enable`,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1174 | 1175 | 举例:启用索引为2的模拟器的垂直同步 1176 | 1177 | ```python 1178 | Mumu().select(2).screen.vertical_sync(True) 1179 | ``` 1180 | 1181 | 举例:禁用索引为2的模拟器的垂直同步 1182 | 1183 | ```python 1184 | Mumu().select(2).screen.vertical_sync(False) 1185 | ``` 1186 | 1187 | #### 显示帧率(show_frame_rate) 1188 | 1189 | 该方法接受一个参数,即为`enable`,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1190 | 1191 | 举例:启用索引为2的模拟器的显示帧率 1192 | 1193 | ```python 1194 | Mumu().select(2).screen.show_frame_rate(True) 1195 | ``` 1196 | 1197 | 举例:禁用索引为2的模拟器的显示帧率 1198 | 1199 | ```python 1200 | Mumu().select(2).screen.show_frame_rate(False) 1201 | ``` 1202 | 1203 | #### 设置窗口自动旋转(window_auto_rotate) 1204 | 1205 | 该方法接受一个参数,即为`enable`,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1206 | 1207 | 设置后,模拟器窗口会根据运行的应用自动旋转 1208 | 1209 | 举例:启用索引为2的模拟器的窗口自动旋转 1210 | 1211 | ```python 1212 | Mumu().select(2).screen.window_auto_rotate(True) 1213 | ``` 1214 | 1215 | 举例:禁用索引为2的模拟器的窗口自动旋转 1216 | 1217 | ```python 1218 | Mumu().select(2).screen.window_auto_rotate(False) 1219 | ``` 1220 | 1221 | ### 性能类(performance) 1222 | 1223 | 该类提供了模拟器性能操作。 1224 | 1225 | 该类的所有操作需要重启模拟器后生效。 1226 | 1227 | #### 设置CPU和内存(set) 1228 | 1229 | 该方法接受两个参数,分别是CPU个数和内存大小,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1230 | 1231 | CPU个数范围为1-16 1232 | 1233 | 内存大小范围为1-16,单位为GB,不支持小数 1234 | 1235 | 举例:设置索引为2的模拟器的CPU为4核,内存为4GB 1236 | 1237 | ```python 1238 | Mumu().select(2).performance.set(4, 4) 1239 | ``` 1240 | 1241 | #### 设置CPU个数(cpu) 1242 | 1243 | 该方法接受一个参数,即为CPU个数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1244 | 1245 | CPU个数范围为1-16 1246 | 1247 | 举例:设置索引为2的模拟器的CPU为4核 1248 | 1249 | ```python 1250 | Mumu().select(2).performance.cpu(4) 1251 | ``` 1252 | 1253 | #### 设置内存大小(memory) 1254 | 1255 | 该方法接受一个参数,即为内存大小,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1256 | 1257 | 内存大小范围为1-16,单位为GB,不支持小数 1258 | 1259 | 举例:设置索引为2的模拟器的内存为4GB 1260 | 1261 | ```python 1262 | Mumu().select(2).performance.memory(4) 1263 | ``` 1264 | 1265 | #### 设置强制使用独立显卡(force_discrete_graphics) 1266 | 1267 | 该方法接受一个参数,即为`enable`,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1268 | 1269 | 举例:启用索引为2的模拟器的强制使用独立显卡 1270 | 1271 | ```python 1272 | Mumu().select(2).performance.force_discrete_graphics(True) 1273 | ``` 1274 | 1275 | 举例:禁用索引为2的模拟器的强制使用独立显卡 1276 | 1277 | ```python 1278 | Mumu().select(2).performance.force_discrete_graphics(False) 1279 | ``` 1280 | 1281 | #### 显存使用策略(renderer_strategy) 1282 | 1283 | 该方法接受三个参数,选择一个即可。 1284 | 1285 | 举例:设置索引为2的模拟器的显存使用策略为自动 1286 | 1287 | ```python 1288 | Mumu().select(2).performance.renderer_strategy(auto=True) 1289 | # or 1290 | Mumu().select(2).performance.renderer_strategy() 1291 | ``` 1292 | 1293 | 举例:设置索引为2的模拟器的显存使用策略为资源占用更小 1294 | 1295 | ```python 1296 | Mumu().select(2).performance.renderer_strategy(perf=True) 1297 | ``` 1298 | 1299 | 举例:设置索引为2的模拟器的显存使用策略为画面表现更好 1300 | 1301 | ```python 1302 | Mumu().select(2).performance.renderer_strategy(dis=True) 1303 | ``` 1304 | 1305 | #### 设置磁盘类型(disk_readonly) 1306 | 1307 | 该方法接受一个参数,即为`enable`,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1308 | 1309 | `enable`缺省值为`True`,开启时,磁盘类型为(只读系统盘,官方推荐),关闭时,磁盘类型为(可写系统盘) 1310 | 1311 | 本方法提供一个助手方法`disk_writable`,用于设置磁盘类型为可写系统盘。 1312 | 1313 | 举例:设置索引为2的模拟器的磁盘类型为只读系统盘。 1314 | 1315 | ```python 1316 | Mumu().select(2).performance.disk_readonly(True) 1317 | 1318 | # or 1319 | 1320 | Mumu().select(2).performance.disk_readonly() 1321 | ``` 1322 | 1323 | 举例:设置索引为2的模拟器的磁盘类型为可写系统盘 1324 | 1325 | ```python 1326 | Mumu().select(2).performance.disk_readonly(False) 1327 | 1328 | # or 1329 | 1330 | Mumu().select(2).performance.disk_writable() 1331 | ``` 1332 | 1333 | ### *网络类(network) 1334 | 1335 | 该类提供了模拟器网络操作。 1336 | 1337 | #### 获取所有可被桥接的网卡(get_bridge_card) 1338 | 1339 | 该方法返回一个列表,表示所有可被桥接的网卡。 1340 | 1341 | 举例:获取所有可被桥接的网卡 1342 | 1343 | ```python 1344 | Mumu().select(1).network.get_bridge_card() 1345 | ``` 1346 | 1347 | 示例返回: 1348 | 1349 | ```python 1350 | ['Realtek Gaming GbE Family Controller', 'Sangfor SSL VPN CS Support System VNIC', 'Microsoft KM-TEST 环回适配器', 1351 | 'Intel(R) Wi-Fi 6E AX211 160MHz'] 1352 | ``` 1353 | 1354 | #### 设置网络桥接模式(bridge) 1355 | 1356 | 该方法接受两个参数,分别代表是否启用和网卡名称,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1357 | 1358 | 该方法需要安装“桥接驱动”才能使用。 1359 | 1360 | 举例:启用索引为2的模拟器的网络桥接模式,网卡名称为`Realtek Gaming GbE Family Controller` 1361 | 1362 | ```python 1363 | Mumu().select(2).network.bridge(True, 'Realtek Gaming GbE Family Controller') 1364 | ``` 1365 | 1366 | 举例:禁用索引为2的模拟器的网络桥接模式 1367 | 1368 | ```python 1369 | Mumu().select(2).network.bridge(False) 1370 | ``` 1371 | 1372 | #### 设置网络为NAT模式(nat) 1373 | 1374 | 该方法无需传入参数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1375 | 1376 | 举例:设置索引为2的模拟器的网络为NAT模式 1377 | 1378 | ```python 1379 | Mumu().select(2).network.nat() 1380 | ``` 1381 | 1382 | #### 设置桥接模式IP设置方式为DHCP(bridge_dhcp) 1383 | 1384 | 该方法无需传入参数,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1385 | 1386 | 举例:设置索引为2的模拟器的桥接模式IP设置方式为DHCP 1387 | 1388 | ```python 1389 | Mumu().select(2).network.bridge_dhcp() 1390 | ``` 1391 | 1392 | #### 设置桥接模式IP设置方式为静态(bridge_static) 1393 | 1394 | 该方法接受5个参数,分别是IP地址、子网掩码、网关、DNS1、DNS2,调用该方法前需要先选择一个模拟器,返回bool值,表示是否修改成功。 1395 | 1396 | 当不填写DNS时,默认DNS1为`8.8.8.8`,DNS2为`114.114.114.114` 1397 | 1398 | 举例:设置索引为2的模拟器的桥接模式IP设置方式为静态,IP地址为`192.168.10.10`,子网掩码为`255.255.255.0` 1399 | ,网关为`192.168.10.1` 1400 | 1401 | ```python 1402 | Mumu().select(2).network.bridge_static('192.168.10.10', '255.255.255.0', '192.168.10.1') 1403 | ``` 1404 | 1405 | ### ADB类(adb) 1406 | 1407 | 该类提供了ADB操作。 1408 | 1409 | #### 获取模拟器的ADB连接信息(get_connect_info) 1410 | 1411 | 该无需传入参数,调用该方法前需要先选择一个模拟器,当选择一个模拟器时返回一个元组,包含IP和端口,当选择多个模拟器时返回一个字典,键为索引,值为IP和端口。 1412 | 1413 | 如果选择的模拟器无法获取到ADB连接信息,将返回`None,None` 1414 | 1415 | 举例:获取索引为2的模拟器的ADB连接信息 1416 | 1417 | ```python 1418 | adb_ipaddr, adb_port = Mumu().select(2).adb.get_connect_info() 1419 | ``` 1420 | 1421 | 举例:获取索引为2,4,6的模拟器的ADB连接信息 1422 | 1423 | ```python 1424 | adb_info = Mumu().select(2, 4, 6).adb.get_connect_info() 1425 | ``` 1426 | 1427 | 返回示例 1428 | 1429 | ```python 1430 | {'2': ('172.30.20.123', 16416), '4': (None, None), '6': (None, None)} 1431 | ``` 1432 | 1433 | #### 点击屏幕(click) 1434 | 1435 | 该方法接受两个参数,分别是X坐标和Y坐标,调用该方法前需要先选择一个模拟器,返回bool值,表示是否点击成功。 1436 | 1437 | 举例:点击索引为2的模拟器的坐标为(100, 100)的位置 1438 | 1439 | ```python 1440 | Mumu().select(2).adb.click(100, 100) 1441 | ``` 1442 | 1443 | 举例:点击索引为2,4,6的模拟器的坐标为(100, 100)的位置 1444 | 1445 | ```python 1446 | Mumu().select(2, 4, 6).adb.click(100, 100) 1447 | ``` 1448 | 1449 | #### 滑动屏幕(swipe) 1450 | 1451 | 该方法提供5个参数,分别是起始X坐标、起始Y坐标、结束X坐标、结束Y坐标和滑动时间,调用该方法前需要先选择一个模拟器,返回bool值,表示是否滑动成功。 1452 | 1453 | 举例:滑动索引为2的模拟器的坐标为(100, 100)到(200, 200)的位置,时间为500ms 1454 | 1455 | ```python 1456 | Mumu().select(2).adb.swipe(100, 100, 200, 200, 500) 1457 | # or 1458 | Mumu().select(2).adb.swipe(100, 100, 200, 200) # 缺省值为500ms 1459 | ``` 1460 | 1461 | 举例:滑动索引为2,4,6的模拟器的坐标为(100, 100)到(200, 200)的位置,时间为500ms 1462 | 1463 | ```python 1464 | Mumu().select(2, 4, 6).adb.swipe(100, 100, 200, 200, 500) 1465 | ``` 1466 | 1467 | #### 文本输入(input_text) 1468 | 1469 | 该方法接受一个参数,即为要输入的文本,调用该方法前需要先选择一个模拟器,返回bool值,表示是否输入成功。 1470 | 1471 | 举例:在索引为2的模拟器中输入文本`Hello World` 1472 | 1473 | ```python 1474 | Mumu().select(2).adb.input_text('Hello World') 1475 | ``` 1476 | 1477 | 举例:在索引为2,4,6的模拟器中输入文本`Hello World` 1478 | 1479 | ```python 1480 | Mumu().select(2, 4, 6).adb.input_text('Hello World') 1481 | ``` 1482 | 1483 | #### 模拟按键(key_event) 1484 | 1485 | 该方法接受一个参数,即为要模拟的按键,调用该方法前需要先选择一个模拟器,返回bool值,表示是否模拟成功。 1486 | 1487 | 举例:在索引为2的模拟器中模拟按键`HOME` 1488 | 1489 | ```python 1490 | Mumu().select(2).adb.key_event(3) 1491 | ``` 1492 | 1493 | 举例:通过本项目提供的按键常量模拟按键`HOME` 1494 | 1495 | ```python 1496 | from mumu.constant import AndroidKey 1497 | 1498 | Mumu().select(2).adb.key_event(AndroidKey.KEYCODE_HOME) 1499 | ``` 1500 | 1501 | 举例:在索引为2,4,6的模拟器中模拟音量+ 1502 | 1503 | ```python 1504 | from mumu.constant import AndroidKey 1505 | 1506 | Mumu().select(2, 4, 6).adb.key_event(AndroidKey.KEYCODE_VOLUME_UP) 1507 | ``` 1508 | 1509 | #### 上传文件(push) 1510 | 1511 | 该方法接受两个参数,分别是本地文件路径和模拟器文件路径,调用该方法前需要先选择一个模拟器,返回bool值,表示是否上传成功。 1512 | 1513 | 当上传失败时,会触发一个`warning`警告。 1514 | 1515 | 举例:上传索引为2的模拟器的本地文件`C:\test.txt`到模拟器的`/sdcard/test.txt` 1516 | 1517 | ```python 1518 | Mumu().select(2).adb.push(r'C:\test.txt', '/sdcard/test.txt') 1519 | ``` 1520 | 1521 | 举例:上传索引为2,4,6的模拟器的本地文件`C:\test.txt`到模拟器的`/sdcard/test.txt` 1522 | 1523 | ```python 1524 | Mumu().select(2, 4, 6).adb.push(r'C:\test.txt', '/sdcard/test.txt') 1525 | ``` 1526 | 1527 | #### 上传文件到Download目录(push_download) 1528 | 1529 | 该方法提供两个参数,分别是本地文件路径和模拟器文件名,调用该方法前需要先选择一个模拟器,返回bool值,表示是否上传成功。 1530 | 1531 | 当`模拟器文件名`为`None`时,将自动使用本地文件名。 1532 | 1533 | 举例:上传索引为2的模拟器的本地文件`C:\test.txt`到模拟器的`Download`目录 1534 | 1535 | ```python 1536 | Mumu().select(2).adb.push_download(r'C:\test.txt') 1537 | ``` 1538 | 1539 | 举例:上传索引为2,4,6的模拟器的本地文件`C:\test.txt`到模拟器的`Download`目录,重命名为:`test1.txt` 1540 | 1541 | ```python 1542 | Mumu().select(2, 4, 6).adb.push_download(r'C:\test.txt', 'test1.txt') 1543 | ``` 1544 | 1545 | #### 下载文件(pull) 1546 | 1547 | 该方法提供两个参数,分别是模拟器文件路径和本地文件路径,调用该方法前需要先选择一个模拟器,返回bool值,表示是否下载成功。 1548 | 1549 | 当下载失败时,会触发一个`warning`警告。 1550 | 1551 | 举例:下载索引为2的模拟器的模拟器文件`/sdcard/test.txt`到本地的`C:\test.txt` 1552 | 1553 | ```python 1554 | Mumu().select(2).adb.pull('/sdcard/test.txt', r'C:\test.txt') 1555 | ``` 1556 | 1557 | #### 清理应用数据(clear) 1558 | 1559 | 该方法接受一个参数,即为应用包名,调用该方法前需要先选择一个模拟器,返回bool值,表示是否清理成功。 1560 | 1561 | 如果包名错误会抛出异常 1562 | 1563 | 举例:清理索引为2的模拟器的应用数据 1564 | 1565 | ```python 1566 | Mumu().select(2).adb.clear('com.miHoYo.Yuanshen') 1567 | ``` 1568 | 1569 | ### GUI自动化类(auto) 1570 | 1571 | 本类提供了模拟器的GUI自动化操作,例如:通过图片定位、坐标定位、文字定位实现自动化操作。 1572 | 1573 | 先决条件:需要安装`opencv-python`库,可以通过`pip install opencv-python`安装。 1574 | 1575 | 如果希望使用本项目自带的投屏功能,需要安装`scrcpy`,可以通过`pip install scrcpy-client`安装。 1576 | 1577 | #### 处理模拟器实时帧(create_handle) 1578 | 1579 | 该方法接受一个参数`handle`,传入一个方法,用于处理模拟器的帧。 1580 | 1581 | 该方法基于`scrcpy`实现,如果您的应用对该软件有限制,请勿使用。 1582 | 1583 | 使用该方法,会自动创建`2`个子线程,一个用于接收模拟器的帧,一个用于处理帧。 1584 | 1585 | `handle`方法包含两个参数`frame`和`mumu`,分别表示帧和当前模拟器对象。 1586 | 1587 | `frame`为`numpy`数组,可以直接使用`opencv`的方法处理。 1588 | `mumu`为当前模拟器对象,仅选中了当前模拟器。 1589 | 1590 | 举例:处理索引为2的模拟器的帧 1591 | 1592 | ```python 1593 | def handle(frame, mumu): 1594 | # do something 1595 | print('接收到模拟器:', mumu.core.utils.get_vm_id(), '的帧') 1596 | 1597 | 1598 | Mumu().select(2).auto.create_handle(handle) 1599 | ``` 1600 | 1601 | 举例:处理索引为2,4,6的模拟器的帧 1602 | 1603 | ```python 1604 | def handle(frame, mumu): 1605 | # do something 1606 | print('接收到模拟器:', mumu.core.utils.get_vm_id(), '的帧') 1607 | 1608 | 1609 | Mumu().select(2, 4, 6).auto.create_handle(handle) 1610 | ``` 1611 | 1612 | #### 保存模拟器实时帧(save) 1613 | 1614 | 该方法接受两个参数,分别为`frame`和`path`,用于保存模拟器的帧。 1615 | 1616 | 举例:保存帧到`C:\test.png` 1617 | 1618 | ```python 1619 | def handle(frame, mumu): 1620 | mumu.auto.save(frame, r'C:\test.png') 1621 | 1622 | 1623 | Mumu().select(2).auto.create_handle(handle) 1624 | ``` 1625 | 1626 | 举例:保存索引为3的模拟器的帧到`C:\test.png` 1627 | 1628 | ```python 1629 | def handle(frame, mumu): 1630 | if mumu.core.utils.get_vm_id() == '3': 1631 | mumu.auto.save(frame, r'C:\test.png') 1632 | 1633 | 1634 | Mumu().select(2, 3).auto.create_handle(handle) 1635 | ``` 1636 | 1637 | #### 在帧中查找第一个图片(locateOnScreen) 1638 | 1639 | 该方法接受4个参数,分别为`haystack`、`needle`、`confidence`和`grayscale`,用于在帧中查找图片。 1640 | 1641 | `confidence`为相似度,范围为0-1,缺省值为0.8 1642 | 1643 | `grayscale`为是否使用灰度查找,缺省值为`False 1644 | 1645 | 举例:在帧中查找图片`C:\test.png` 1646 | 1647 | ```python 1648 | def handle(frame, mumu): 1649 | # do something 1650 | print('接收到模拟器:', mumu.core.utils.get_vm_id(), '的帧') 1651 | pos = mumu.auto.locateOnScreen(frame, r'C:\test.png') 1652 | if pos: 1653 | print('找到图片:', pos) 1654 | else: 1655 | print('未找到图片') 1656 | ``` 1657 | 1658 | #### 在帧中查找第一张图片的中心点(locateCenterOnScreen) 1659 | 1660 | 该方法接受4个参数,分别为`haystack`、`needle`、`confidence`和`grayscale`,用于在帧中查找图片。 1661 | 1662 | `confidence`为相似度,范围为0-1,缺省值为0.8 1663 | 1664 | `grayscale`为是否使用灰度查找,缺省值为`False 1665 | 1666 | ```python 1667 | def handle(frame, mumu): 1668 | # do something 1669 | print('接收到模拟器:', mumu.core.utils.get_vm_id(), '的帧') 1670 | pos = mumu.auto.locateCenterOnScreen(frame, r'C:\test.png') 1671 | if pos: 1672 | print('找到图片中心点:', pos) 1673 | else: 1674 | print('未找到图片') 1675 | 1676 | ``` 1677 | 1678 | #### 在帧中查找所有图片(locateAllOnScreen) 1679 | 1680 | 该方法接受4个参数,分别为`haystack`、`needle`、`confidence`和`grayscale`,用于在帧中查找图片。 1681 | 1682 | `confidence`为相似度,范围为0-1,缺省值为0.8 1683 | `grayscale`为是否使用灰度查找,缺省值为`False 1684 | 1685 | ```python 1686 | def handle(frame, mumu): 1687 | # do something 1688 | print('接收到模拟器:', mumu.core.utils.get_vm_id(), '的帧') 1689 | pos = mumu.auto.locateAllOnScreen(frame, r'C:\test.png') 1690 | if pos: 1691 | print('找到图片:', pos) 1692 | else: 1693 | print('未找到图片') 1694 | 1695 | ``` 1696 | 1697 | #### 获取Box的中心点(center) 1698 | 1699 | 该方法接受一个参数,即为`box`,用于获取Box的中心点,返回x和y 1700 | 1701 | `box`为一个元组,包含4个元素,分别为左上角x、左上角y、右下角x、右下角y 1702 | 1703 | ```python 1704 | def handle(frame, mumu): 1705 | # do something 1706 | print('接收到模拟器:', mumu.core.utils.get_vm_id(), '的帧') 1707 | pos = mumu.auto.locateOnScreen(frame, r'C:\test.png') 1708 | if pos: 1709 | print('找到图片:', pos) 1710 | x, y = mumu.auto.center(pos) 1711 | print('中心点:', x, y) 1712 | else: 1713 | print('未找到图片') 1714 | 1715 | ``` 1716 | 1717 | #### 实战举例 1718 | 1719 | 监听模拟器3的帧,当帧中出现`test.png`时,返回`找到了`,并返回当前时间和模拟器ID,然后返回到桌面。 1720 | 1721 | ```python 1722 | def handle(frame, mumu: Mumu): 1723 | if mumu.auto.locateOnScreen(frame, './test.png', confidence=0.75, grayscale=True): 1724 | print("找到了", time.time(), '在模拟器', mumu.core.utils.get_vm_id()) 1725 | mumu.androidEvent.go_home() 1726 | 1727 | 1728 | Mumu().select(3).auto.create_handle(handle) 1729 | 1730 | ``` 1731 | 1732 | ## 支持本项目 1733 | 1734 | 作者:wlkjyy 1735 | 1736 | Mail: 1737 | 1738 | WeCHAT:laravel_debug -------------------------------------------------------------------------------- /mumu/__pycache__/config.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/__pycache__/config.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/__pycache__/constant.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/__pycache__/constant.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/__pycache__/control.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/__pycache__/control.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/__pycache__/mumu.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/__pycache__/mumu.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/__pycache__/utils.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/__pycache__/utils.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/adb/Adb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/30 上午8:40 4 | # @Author : wlkjyy 5 | # @File : Adb.py 6 | # @Software: PyCharm 7 | import json 8 | import os.path 9 | import time 10 | import warnings 11 | from typing import Union 12 | 13 | 14 | import mumu.config as config 15 | 16 | 17 | class Adb: 18 | 19 | def __init__(self, utils): 20 | self.utils = utils 21 | 22 | # __connect_list = None 23 | 24 | def get_connect_info(self): 25 | """ 26 | 获取连接信息 27 | :return: 返回一个包含 ADB 连接信息的字典,或者 (None, None) 如果没有获取到信息。 28 | """ 29 | self.utils.set_operate("adb") 30 | ret_code, retval = self.utils.run_command(['']) 31 | 32 | if ret_code != 0: 33 | return None, None 34 | 35 | try: 36 | data = json.loads(retval) 37 | except json.JSONDecodeError: 38 | return None, None 39 | 40 | adb_info = {} 41 | for key, value in data.items(): 42 | if key == "adb_host" and "adb_port" in data: 43 | return data["adb_host"], data["adb_port"] 44 | if 'errcode' in value: 45 | adb_info[key] = (None, None) 46 | else: 47 | adb_info[key] = (value.get("adb_host"), value.get("adb_port")) 48 | 49 | return adb_info if adb_info else (None, None) 50 | 51 | def click(self, x: int, y: int): 52 | """ 53 | 点击(click) 54 | :param x: 横坐标 55 | :param y: 纵坐标 56 | :return: 57 | """ 58 | self.utils.set_operate('adb') 59 | ret_code, retval = self.utils.run_command(['-c', 'input', 'tap', str(x), str(y)]) 60 | 61 | if ret_code == 0: 62 | return True 63 | 64 | raise RuntimeError(retval) 65 | 66 | def swipe(self, from_x: int, from_y: int, to_x: int, to_y: int, duration: int = 500): 67 | """ 68 | 滑动(swipe) 69 | :param from_x: 起始横坐标 70 | :param from_y: 起始纵坐标 71 | :param to_x: 终点横坐标 72 | :param to_y: 终点纵坐标 73 | :param duration: 滑动时间 74 | :return: 75 | """ 76 | self.utils.set_operate('adb') 77 | ret_code, retval = self.utils.run_command( 78 | ['-c', 'input', 'swipe', str(from_x), str(from_y), str(to_x), str(to_y), str(duration)]) 79 | 80 | if ret_code == 0: 81 | return True 82 | 83 | raise RuntimeError(retval) 84 | 85 | def input_text(self, text: str): 86 | """ 87 | 输入(input) 88 | :param text: 输入的文本 89 | :return: 90 | """ 91 | self.utils.set_operate('adb') 92 | ret_code, retval = self.utils.run_command(['-c', 'input', 'text', text]) 93 | 94 | if ret_code == 0: 95 | return True 96 | 97 | raise RuntimeError(retval) 98 | 99 | def key_event(self, key: Union[int, str]): 100 | """ 101 | 按键(keyevent) 102 | :param key: 键值 103 | :return: 104 | """ 105 | self.utils.set_operate('adb') 106 | ret_code, retval = self.utils.run_command(['-c', 'input', 'keyevent', str(key)]) 107 | 108 | if ret_code == 0: 109 | return True 110 | 111 | raise RuntimeError(retval) 112 | 113 | def __connect(self): 114 | """ 115 | 获取可用的连接 116 | :return: 117 | """ 118 | 119 | self.utils.set_operate("adb") 120 | ret_code, retval = self.utils.run_command(['']) 121 | 122 | if ret_code != 0: 123 | return self 124 | 125 | try: 126 | data = json.loads(retval) 127 | except json.JSONDecodeError: 128 | return None, None 129 | 130 | for key, value in data.items(): 131 | if key == "adb_host" and "adb_port" in data: 132 | yield data["adb_host"], data["adb_port"] 133 | return 134 | 135 | if 'errcode' in value: 136 | continue 137 | else: 138 | # connect_list.append((value.get("adb_host"), value.get("adb_port"))) 139 | yield value.get("adb_host"), value.get("adb_port") 140 | 141 | return 142 | 143 | def push(self, src: str, path: str): 144 | """ 145 | 传输文件(push) 146 | :param src: 源文件 147 | :param path: 目标路径 148 | :return: 149 | """ 150 | 151 | if not os.path.exists(src): 152 | raise FileNotFoundError(f"File not found: {src}") 153 | 154 | if not os.path.exists(config.ADB_PATH): 155 | raise FileNotFoundError(f"adb not found in {config.ADB_PATH}") 156 | 157 | for (host, port) in self.__connect(): 158 | ret_code, retval = self.utils.run_command([config.ADB_PATH, '-s', f"{host}:{port}", 'push', src, path], 159 | mumu=False) 160 | 161 | if ret_code != 0: 162 | warnings.warn(retval) 163 | 164 | return True 165 | 166 | def push_download(self, src: str, new_name: str = None): 167 | """ 168 | 传输文件到Download文件夹(push) 169 | :param new_name: 170 | :param src: 源文件 171 | :param path: 目标路径 172 | :return: 173 | """ 174 | if new_name: 175 | filename = new_name 176 | else: 177 | filename = os.path.basename(src) 178 | 179 | return self.push(src, f"/sdcard/Download/{filename}") 180 | 181 | def pull(self, src: str, path: str): 182 | """ 183 | 传输文件(pull) 184 | :param src: 源文件 185 | :param path: 目标路径 186 | :return: 187 | """ 188 | 189 | if not os.path.exists(config.ADB_PATH): 190 | raise FileNotFoundError(f"adb not found in {config.ADB_PATH}") 191 | 192 | for (host, port) in self.__connect(): 193 | ret_code, retval = self.utils.run_command([config.ADB_PATH, '-s', f"{host}:{port}", 'pull', src, path], 194 | mumu=False) 195 | 196 | if ret_code != 0: 197 | warnings.warn(retval) 198 | 199 | return True 200 | 201 | def clear(self, package: str): 202 | """ 203 | 清除应用数据(clear) 204 | :param package: 应用包名 205 | :return: 206 | """ 207 | self.utils.set_operate('adb') 208 | ret_code, retval = self.utils.run_command(['-c', 'pm', 'clear', package]) 209 | 210 | if ret_code == 0: 211 | return True 212 | 213 | raise RuntimeError(retval) 214 | -------------------------------------------------------------------------------- /mumu/api/core/Core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午2:40 4 | # @Author : wlkjyy 5 | # @File : Core.py 6 | # @Software: PyCharm 7 | import json 8 | import warnings 9 | from typing import Union 10 | 11 | 12 | class Core: 13 | 14 | def __init__(self, utils): 15 | self.utils = utils 16 | 17 | def create(self, number: int = 1) -> list: 18 | """ 19 | 创建模拟器 20 | :param number: 创建数量 21 | :return: 22 | """ 23 | if number <= 0: 24 | warnings.warn("The number of simulators created is less than 1") 25 | number = 1 26 | 27 | self.utils.set_operate("create") 28 | 29 | ret_code, retval = self.utils.run_command(["-n", str(number)]) 30 | 31 | if ret_code != 0: 32 | raise RuntimeError(retval) 33 | 34 | create_index = [] 35 | 36 | data = json.loads(retval) 37 | 38 | for row in data.keys(): 39 | if data[row]["errcode"] == 0: 40 | create_index.append(int(row)) 41 | 42 | return create_index 43 | 44 | def clone(self, number: int = 1) -> list: 45 | """ 46 | 克隆模拟器 47 | :param number: 克隆数量 48 | :return: 49 | """ 50 | if number <= 0: 51 | warnings.warn("The number of simulators created is less than 1") 52 | number = 1 53 | 54 | self.utils.set_operate("clone") 55 | 56 | ret_code, retval = self.utils.run_command(["-n", str(number)]) 57 | 58 | if ret_code != 0: 59 | raise RuntimeError(retval) 60 | 61 | create_index = [] 62 | 63 | data = json.loads(retval) 64 | 65 | for row in data.keys(): 66 | if data[row]["errcode"] == 0: 67 | create_index.append(int(row)) 68 | 69 | return create_index 70 | 71 | def delete(self) -> bool: 72 | """ 73 | 删除模拟器 74 | :return: 75 | """ 76 | self.utils.set_operate("delete") 77 | 78 | ret_code, retval = self.utils.run_command([]) 79 | 80 | if ret_code == 0: 81 | return True 82 | 83 | raise RuntimeError(retval) 84 | 85 | def rename(self, name: str) -> bool: 86 | """ 87 | 重命名模拟器 88 | :param name: 模拟器名称 89 | :return: 90 | """ 91 | self.utils.set_operate("rename") 92 | 93 | ret_code, retval = self.utils.run_command(["-n", name]) 94 | 95 | if ret_code == 0: 96 | return True 97 | 98 | raise RuntimeError(retval) 99 | 100 | # def export(self): 101 | # pass 102 | 103 | def export(self, dir: str, name: str, zip: bool = False) -> bool: 104 | """ 105 | 导出模拟器 106 | :param dir: 备份的目录 107 | :param name: 备份文件的名称 108 | :param zip: 备份文件是否压缩 109 | :return: 110 | """ 111 | 112 | self.utils.set_operate("export") 113 | args = ["-d", dir, "-n", name] 114 | if zip: 115 | args.append("--zip") 116 | 117 | ret_code, retval = self.utils.run_command(args) 118 | if ret_code == 0: 119 | return True 120 | 121 | raise RuntimeError(retval) 122 | 123 | def import_(self, path: Union[str, list], number: int = 0) -> bool: 124 | """ 125 | 导入模拟器 126 | :param path:要导入的 mumudata 文件路径 127 | :param number:导入次数 128 | :return: 129 | """ 130 | if number <= 0: 131 | warnings.warn("The number of simulators created is less than 1") 132 | number = 1 133 | 134 | self.utils.set_operate("import") 135 | # ret_code, retval = self.self.utils.run_command(["-p", path, "-n", str(number)]) 136 | 137 | if isinstance(path, str): 138 | ret_code, retval = self.utils.run_command(["-p", path, "-n", str(number)]) 139 | 140 | else: 141 | args = [] 142 | for p in path: 143 | args.extend(["-p", p]) 144 | args.extend(["-n", str(number)]) 145 | 146 | ret_code, retval = self.utils.run_command(args) 147 | 148 | if ret_code == 0: 149 | return True 150 | 151 | raise RuntimeError(retval) 152 | 153 | def limit_cpu(self, cap: int = 100) -> bool: 154 | """ 155 | 限制CPU 156 | :param cap: CPU限制,范围0-100 157 | :return: 158 | """ 159 | if cap < 0 or cap > 100: 160 | raise ValueError("The value of cap must be between 0 and 100") 161 | 162 | self.utils.set_operate("control") 163 | ret_code, retval = self.utils.run_command(["tool", "downcpu", "-c", str(cap)]) 164 | 165 | if ret_code == 0: 166 | return True 167 | 168 | raise RuntimeError(retval) 169 | -------------------------------------------------------------------------------- /mumu/api/core/__pycache__/Core.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/core/__pycache__/Core.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/core/__pycache__/app.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/core/__pycache__/app.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/core/__pycache__/performance.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/core/__pycache__/performance.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/core/__pycache__/power.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/core/__pycache__/power.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/core/__pycache__/shortcut.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/core/__pycache__/shortcut.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/core/__pycache__/simulation.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/core/__pycache__/simulation.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/core/__pycache__/window.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/core/__pycache__/window.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/core/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午3:26 4 | # @Author : wlkjyy 5 | # @File : app.py 6 | # @Software: PyCharm 7 | import json 8 | import os.path 9 | 10 | 11 | 12 | 13 | class App: 14 | 15 | def __init__(self, utils): 16 | self.utils = utils 17 | 18 | def install(self, apk_path: str = None) -> bool: 19 | """ 20 | 安装应用到模拟器里(install) 21 | :param apk_path: 选择要安装的应用apk文件路径(支持apk/xapk/apks后缀) 22 | :return: 23 | """ 24 | if not os.path.exists(apk_path): 25 | raise FileNotFoundError(f"apk_path:{apk_path} not found") 26 | 27 | if not os.path.isfile(apk_path): 28 | raise FileNotFoundError(f"apk_path:{apk_path} is not a file") 29 | self.utils.set_operate('control') 30 | 31 | ret_code, retval = self.utils.run_command(['app', 'install', '-apk', apk_path]) 32 | 33 | if ret_code == 0: 34 | return True 35 | 36 | raise RuntimeError(retval) 37 | 38 | def uninstall(self,package: str) -> bool: 39 | """ 40 | 卸载应用(uninstall) 41 | :param package: 选择要卸载的应用包名 42 | :return: 43 | """ 44 | self.utils.set_operate('control') 45 | ret_code, retval = self.utils.run_command(['app', 'uninstall', '-pkg', package]) 46 | 47 | if ret_code == 0: 48 | return True 49 | 50 | raise RuntimeError(retval) 51 | 52 | def launch(self,package: str) -> bool: 53 | """ 54 | 启动应用(launch) 55 | :param package: 选择要启动的应用包名 56 | :return: 57 | """ 58 | self.utils.set_operate('control') 59 | ret_code, retval = self.utils.run_command(['app', 'launch', '-pkg', package]) 60 | 61 | if ret_code == 0: 62 | return True 63 | 64 | raise RuntimeError(retval) 65 | 66 | def close(self,package: str) -> bool: 67 | """ 68 | 关闭应用(close) 69 | :param package: 选择要关闭的应用包名 70 | :return: 71 | """ 72 | self.utils.set_operate('control') 73 | ret_code, retval = self.utils.run_command(['app', 'close', '-pkg', package]) 74 | 75 | if ret_code == 0: 76 | return True 77 | 78 | raise RuntimeError(retval) 79 | 80 | def get_installed(self): 81 | """ 82 | 获取已安装的应用(get_installed) 83 | :return: 84 | """ 85 | self.utils.set_operate('control') 86 | ret_code, retval = self.utils.run_command(['app', 'info','-i']) 87 | 88 | if ret_code != 0: 89 | raise RuntimeError(retval) 90 | 91 | data = json.loads(retval) 92 | installed = [] 93 | 94 | for key in data.keys(): 95 | if key != "active": 96 | installed.append({ 97 | "package": key, 98 | "app_name": data[key]['app_name'], 99 | "version": data[key]['version'] 100 | }) 101 | 102 | return installed 103 | 104 | 105 | def exists(self,package: str) -> bool: 106 | """ 107 | 判断应用是否存在(exists) 108 | :param package: 选择要判断的应用包名 109 | :return: 110 | """ 111 | self.utils.set_operate('control') 112 | ret_code, retval = self.utils.run_command(['app', 'info', '-pkg', package]) 113 | 114 | if ret_code != 0: 115 | raise RuntimeError(retval) 116 | 117 | data = json.loads(retval) 118 | 119 | return data['state'] != 'not_installed' 120 | 121 | def doesntExists(self,package: str) -> bool: 122 | """ 123 | 判断应用是否不存在(doesntExists) 124 | :param package: 选择要判断的应用包名 125 | :return: 126 | """ 127 | return not self.exists(package) 128 | 129 | def state(self,package: str) -> str: 130 | """ 131 | 获取应用状态(state) 132 | :param package: 选择要获取的应用包名 133 | :return: 134 | """ 135 | self.utils.set_operate('control') 136 | ret_code, retval = self.utils.run_command(['app', 'info', '-pkg', package]) 137 | 138 | if ret_code != 0: 139 | raise RuntimeError(retval) 140 | 141 | data = json.loads(retval) 142 | 143 | return data['state'] -------------------------------------------------------------------------------- /mumu/api/core/performance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午9:58 4 | # @Author : wlkjyy 5 | # @File : performance.py 6 | # @Software: PyCharm 7 | from mumu.api.setting.setting import Setting 8 | 9 | 10 | class Performance: 11 | 12 | def __init__(self, utils): 13 | self.utils = utils 14 | 15 | def set(self, cpu_num: int = 1, mem_gb: int = 2): 16 | """ 17 | 设置模拟器性能 18 | :param cpu_num: CPU个数 19 | :param mem_gb: 内存大小 20 | :return: 21 | """ 22 | cpu_num = max(1, min(16, cpu_num)) 23 | 24 | return Setting(self.utils).set( 25 | performance_mode='custom', 26 | performance_cpu__custom=cpu_num, 27 | performance_mem__custom=mem_gb 28 | ) 29 | 30 | def cpu(self, cpu_num: int): 31 | """ 32 | 设置模拟器CPU个数 33 | :param cpu_num: CPU个数 34 | :return: 35 | """ 36 | cpu_num = max(1, min(16, cpu_num)) 37 | 38 | return Setting(self.utils).set( 39 | performance_mode='custom', 40 | performance_cpu__custom=cpu_num 41 | ) 42 | 43 | def memory(self, mem_gb: int): 44 | """ 45 | 设置模拟器内存大小 46 | :param mem_gb: 内存大小 47 | :return: 48 | """ 49 | return Setting(self.utils).set( 50 | performance_mode='custom', 51 | performance_mem__custom=mem_gb 52 | ) 53 | 54 | def force_discrete_graphics(self, enable: bool): 55 | """ 56 | 强制使用独立显卡 57 | :param enable: 是否启用 58 | :return: 59 | """ 60 | return Setting(self.utils).set( 61 | force_discrete_graphics=enable 62 | ) 63 | 64 | def renderer_strategy(self, auto=True, dis=False, perf=False): 65 | """ 66 | 显存使用策略 67 | :param auto: 自动调优 68 | :param dis: 画面表现更好 69 | :param perf: 资源占用更小 70 | :return: 71 | """ 72 | if auto: 73 | return Setting(self.utils).set( 74 | renderer_strategy='auto' 75 | ) 76 | elif dis: 77 | return Setting(self.utils).set( 78 | renderer_strategy='dis' 79 | ) 80 | elif perf: 81 | return Setting(self.utils).set( 82 | renderer_strategy='perf' 83 | ) 84 | 85 | def disk_readonly(self, enable: bool = True): 86 | """ 87 | 是否为只读系统盘 88 | :param enable: 是否只读 89 | :return: 90 | """ 91 | return Setting(self.utils).set( 92 | system_disk_readonly=enable 93 | ) 94 | 95 | def disk_writable(self): 96 | """ 97 | 是否为可写系统盘 98 | :param enable: 是否可写 99 | :return: 100 | """ 101 | return self.disk_readonly(False) 102 | -------------------------------------------------------------------------------- /mumu/api/core/power.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午3:05 4 | # @Author : wlkjyy 5 | # @File : power.py 6 | # @Software: PyCharm 7 | 8 | 9 | 10 | class Power: 11 | 12 | def __init__(self, utils): 13 | self.utils = utils 14 | 15 | def start(self, package: str = None) -> bool: 16 | """ 17 | 启动模拟器(start) 18 | :param package: 启动时自动启动应用的应用包名 19 | :return: 20 | """ 21 | self.utils.set_operate('control') 22 | args = ['launch'] 23 | if package is not None: 24 | args.extend(['-pkg', package]) 25 | 26 | ret_code, retval = self.utils.run_command(args) 27 | if ret_code == 0: 28 | return True 29 | 30 | raise RuntimeError(retval) 31 | 32 | def shutdown(self): 33 | """ 34 | 关闭模拟器(shutdown) 35 | :return: 36 | """ 37 | self.utils.set_operate('control') 38 | ret_code, retval = self.utils.run_command(['shutdown']) 39 | if ret_code == 0: 40 | return True 41 | 42 | raise RuntimeError(retval) 43 | 44 | def restart(self): 45 | """ 46 | 重启模拟器(restart) 47 | :return: 48 | """ 49 | self.utils.set_operate('control') 50 | ret_code, retval = self.utils.run_command(['restart']) 51 | if ret_code == 0: 52 | return True 53 | 54 | raise RuntimeError(retval) 55 | 56 | def stop(self): 57 | """ 58 | 关闭一个模拟器 59 | :return: 60 | """ 61 | return self.shutdown() 62 | 63 | def reboot(self): 64 | """ 65 | 重启一个模拟器 66 | :return: 67 | """ 68 | return self.restart() 69 | -------------------------------------------------------------------------------- /mumu/api/core/shortcut.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午7:11 4 | # @Author : wlkjyy 5 | # @File : shortcut.py 6 | # @Software: PyCharm 7 | import os.path 8 | 9 | 10 | 11 | 12 | class Shortcut: 13 | 14 | def __init__(self, utils): 15 | self.utils = utils 16 | 17 | def create(self, name: str, icon: str, package: str) -> bool: 18 | """ 19 | 创建桌面快捷方式 20 | :param name: 创建快捷方式的名称 21 | :param icon: 创建快捷方式的图标路径 22 | :param package: 创建自动启动应用的快捷方式 23 | :return: 24 | """ 25 | self.utils.set_operate('control') 26 | 27 | if not os.path.exists(icon): 28 | raise FileNotFoundError(f'File not found: {icon}') 29 | 30 | ret_code, retval = self.utils.run_command(['shortcut', 'create', '-n', name, '-i', icon, '-pkg', package]) 31 | 32 | if ret_code != 0: 33 | raise RuntimeError(retval) 34 | 35 | return True 36 | 37 | def delete(self) -> bool: 38 | """ 39 | 删除桌面快捷方式 40 | :return: 41 | """ 42 | self.utils.set_operate('control') 43 | ret_code, retval = self.utils.run_command(['shortcut', 'delete']) 44 | 45 | if ret_code != 0: 46 | raise RuntimeError(retval) 47 | 48 | return True 49 | -------------------------------------------------------------------------------- /mumu/api/core/simulation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午7:25 4 | # @Author : wlkjyy 5 | # @File : simulation.py 6 | # @Software: PyCharm 7 | 8 | 9 | 10 | from mumu.constant import MacAddress, IMEI, IMSI, AndroidID, PhoneNumber, GPU 11 | from mumu.api.setting.setting import Setting 12 | 13 | 14 | class Simulation: 15 | 16 | def __init__(self, utils): 17 | self.utils = utils 18 | 19 | def __action(self, sk: str, sv: str) -> bool: 20 | self.utils.set_operate('simulation') 21 | ret_code, retval = self.utils.run_command(['-sk', sk, '-sv', sv]) 22 | if ret_code == 0: 23 | return True 24 | raise RuntimeError(retval) 25 | 26 | def mac_address(self, mac: str = None) -> bool: 27 | """ 28 | 设置模拟器MAC地址 29 | :param mac: 设置模拟器的MAC地址 30 | :return: 31 | """ 32 | if mac is None: 33 | mac = MacAddress.random() 34 | 35 | return self.__action('mac_address', mac) 36 | 37 | def imei(self, imei: str = None) -> bool: 38 | """ 39 | 设置模拟器IMEI 40 | :param imei: 设置模拟器的IMEI 41 | :return: 42 | """ 43 | if imei is None: 44 | imei = IMEI.random() 45 | 46 | return self.__action('imei', imei) 47 | 48 | def imsi(self, imsi: str = None) -> bool: 49 | """ 50 | 设置模拟器IMSI 51 | :param imsi: 设置模拟器的IMSI 52 | :return: 53 | """ 54 | if imsi is None: 55 | imsi = IMSI.random() 56 | 57 | return self.__action('imsi', imsi) 58 | 59 | def android_id(self, android_id: str = None) -> bool: 60 | """ 61 | 设置模拟器Android ID 62 | :param android_id: 设置模拟器的Android ID 63 | :return: 64 | """ 65 | if android_id is None: 66 | android_id = AndroidID.random() 67 | 68 | return self.__action('android_id', android_id) 69 | 70 | def model(self, model: str) -> bool: 71 | """ 72 | 设置模拟器型号 73 | :param model: 设置模拟器的型号 74 | :return: 75 | """ 76 | return self.__action('model', model) 77 | 78 | def brand(self, brand: str) -> bool: 79 | """ 80 | 设置模拟器品牌 81 | :param brand: 设置模拟器的品牌 82 | :return: 83 | """ 84 | return self.__action('brand', brand) 85 | 86 | def solution(self, solution: str) -> bool: 87 | """ 88 | 设置模拟器硬件 89 | :param solution: 设置模拟器的硬件 90 | :return: 91 | """ 92 | return self.__action('solution', solution) 93 | 94 | def phone_number(self, phone_number: str = None) -> bool: 95 | """ 96 | 设置模拟器手机号码 97 | :param phone_number: 设置模拟器的手机号码 98 | :return: 99 | """ 100 | if phone_number is None: 101 | phone_number = PhoneNumber.random() 102 | 103 | return self.__action('phone_number', phone_number) 104 | 105 | def gpu_model(self, gpu_model_name: str = "GeForce GTX 4090 Ti", top_model=False, middle_model=False, 106 | low_model=False) -> bool: 107 | """ 108 | 设置模拟器GPU型号 109 | :param gpu_model_name: 自定义GPU型号 110 | :param top_model: 使用顶级GPU型号 111 | :param middle_model: 使用中级GPU型号 112 | :param low_model: 使用低级GPU型号 113 | :return: 114 | """ 115 | if top_model: 116 | return Setting(self.utils).set( 117 | gpu_mode="high", 118 | gpu_model__custom=GPU.TOP_MODEL 119 | ) 120 | 121 | if middle_model: 122 | return Setting(self.utils).set( 123 | gpu_mode="middle", 124 | gpu_model__custom=GPU.MIDDLE_MODEL 125 | ) 126 | 127 | if low_model: 128 | return Setting(self.utils).set( 129 | gpu_mode="low", 130 | gpu_model__custom=GPU.LOW_MODEL 131 | ) 132 | 133 | return Setting(self.utils).set( 134 | gpu_mode="custom", 135 | gpu_model__custom=gpu_model_name 136 | ) 137 | -------------------------------------------------------------------------------- /mumu/api/core/window.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午3:14 4 | # @Author : wlkjyy 5 | # @File : window.py 6 | # @Software: PyCharm 7 | 8 | 9 | 10 | class Window: 11 | 12 | def __init__(self, utils): 13 | self.utils = utils 14 | 15 | def show(self) -> bool: 16 | """ 17 | 显示模拟器(show_window) 18 | :return: 19 | """ 20 | self.utils.set_operate('control') 21 | ret_code, retval = self.utils.run_command(['show_window']) 22 | 23 | if ret_code == 0: 24 | return True 25 | 26 | raise RuntimeError(retval) 27 | 28 | def hidden(self) -> bool: 29 | """ 30 | 隐藏模拟器(hide_window) 31 | :return: 32 | """ 33 | self.utils.set_operate('control') 34 | ret_code, retval = self.utils.run_command(['hide_window']) 35 | 36 | if ret_code == 0: 37 | return True 38 | 39 | raise RuntimeError(retval) 40 | 41 | def layout(self, x: int=None, y: int=None, width: int=None, height: int=None) -> bool: 42 | """ 43 | 设置模拟器位置和大小(layout) 44 | :param x: 选择修改窗口的X轴位置,以屏幕左上角为原点 45 | :param y: 选择修改窗口的Y轴位置,以屏幕左上角为原点 46 | :param width: 选择修改窗口的宽度 47 | :param height: 选择修改窗口的高度 48 | :return: 49 | """ 50 | self.utils.set_operate('control') 51 | args = ['layout_window'] 52 | 53 | if x: 54 | args.append('-px') 55 | args.append(str(x)) 56 | 57 | if y: 58 | args.append('-py') 59 | args.append(str(y)) 60 | 61 | if width: 62 | args.append('-width') 63 | args.append(str(width)) 64 | 65 | if height: 66 | args.append('-height') 67 | args.append(str(height)) 68 | 69 | if len(args) == 1: 70 | raise RuntimeError('The layout method must have at least one parameter') 71 | 72 | ret_code, retval = self.utils.run_command(args) 73 | if ret_code == 0: 74 | return True 75 | 76 | raise RuntimeError(retval) -------------------------------------------------------------------------------- /mumu/api/develop/__pycache__/androidevent.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/develop/__pycache__/androidevent.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/develop/androidevent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午6:34 4 | # @Author : wlkjyy 5 | # @File : develop.py 6 | # @Software: PyCharm 7 | 8 | 9 | 10 | 11 | class AndroidEvent: 12 | 13 | def __init__(self, utils): 14 | self.utils = utils 15 | 16 | def __action(self, action_name: str) -> bool: 17 | """ 18 | 执行操作 19 | :param action_name: 操作名称 20 | :return: 21 | """ 22 | self.utils.set_operate("control") 23 | ret_code, ret_val = self.utils.run_command(['tool', 'func', '-n', action_name]) 24 | if ret_code == 0: 25 | return True 26 | 27 | raise RuntimeError(ret_val) 28 | 29 | def rotates(self) -> bool: 30 | """ 31 | 屏幕旋转 32 | :return: 33 | """ 34 | return self.__action("rotate") 35 | 36 | def go_home(self) -> bool: 37 | """ 38 | 返回主页 39 | :return: 40 | """ 41 | return self.__action("go_home") 42 | 43 | def go_back(self) -> bool: 44 | """ 45 | 返回 46 | :return: 47 | """ 48 | return self.__action("go_back") 49 | 50 | def top_most(self) -> bool: 51 | """ 52 | 置顶 53 | :return: 54 | """ 55 | return self.__action("top_most") 56 | 57 | def fullscreen(self) -> bool: 58 | """ 59 | 全屏 60 | :return: 61 | """ 62 | return self.__action("fullscreen") 63 | 64 | def shake(self) -> bool: 65 | """ 66 | 摇一摇 67 | :return: 68 | """ 69 | return self.__action("shake") 70 | 71 | def screenshot(self) -> bool: 72 | """ 73 | 截图 74 | :return: 75 | """ 76 | return self.__action("screenshot") 77 | 78 | def volume_up(self) -> bool: 79 | """ 80 | 音量+ 81 | :return: 82 | """ 83 | return self.__action("volume_up") 84 | 85 | def volume_down(self) -> bool: 86 | """ 87 | 音量- 88 | :return: 89 | """ 90 | return self.__action("volume_down") 91 | 92 | def volume_mute(self) -> bool: 93 | """ 94 | 静音 95 | :return: 96 | """ 97 | return self.__action("volume_mute") 98 | 99 | def go_task(self) -> bool: 100 | """ 101 | 按下安卓任务键 102 | :return: 103 | """ 104 | self.utils.set_operate("adb") 105 | ret_code, ret_val = self.utils.run_command(['-c','go_task']) 106 | if ret_code == 0: 107 | return True 108 | 109 | raise RuntimeError(ret_val) 110 | 111 | def location(self, lon: float, lat: float) -> bool: 112 | """ 113 | 修改虚拟定位 114 | :param lon:要修改虚拟定位的经度,-180 ~ 180 之间浮点有效 115 | :param lat: 要修改虚拟定位的纬度,-90 ~ 90 之间浮点有效 116 | :return: 117 | """ 118 | if lon < -180 or lon > 180: 119 | raise ValueError("The longitude range is incorrect") 120 | 121 | if lat < -90 or lat > 90: 122 | raise ValueError("The latitude range is incorrect") 123 | 124 | self.utils.set_operate("control") 125 | ret_code, ret_val = self.utils.run_command(['tool', 'location', '-lon', str(lon), '-lat', str(lat)]) 126 | if ret_code == 0: 127 | return True 128 | 129 | raise RuntimeError(ret_val) 130 | 131 | def gyro(self, x: float, y: float, z: float) -> bool: 132 | """ 133 | 修改虚拟陀螺仪 134 | :param x: x轴 135 | :param y: y轴 136 | :param z: z轴 137 | :return: 138 | """ 139 | self.utils.set_operate("control") 140 | ret_code, ret_val = self.utils.run_command(['tool', 'gyro', '-gx', str(x), '-gy', str(y), '-gz', str(z)]) 141 | if ret_code == 0: 142 | return True 143 | 144 | raise RuntimeError(ret_val) 145 | -------------------------------------------------------------------------------- /mumu/api/driver/Driver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午2:41 4 | # @Author : wlkjyy 5 | # @File : Driver.py 6 | # @Software: PyCharm 7 | from mumu.api.driver.bridge import Bridge 8 | 9 | 10 | class Driver: 11 | 12 | def __init__(self, utils): 13 | self.utils = utils 14 | 15 | """ 16 | 根据官方文档,目前仅支持“网络桥接”驱动 17 | """ 18 | 19 | @property 20 | def bridge(self): 21 | """ 22 | 网络桥接驱动 23 | :return: 24 | """ 25 | 26 | return Bridge(self.utils) 27 | -------------------------------------------------------------------------------- /mumu/api/driver/__pycache__/Driver.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/driver/__pycache__/Driver.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/driver/__pycache__/bridge.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/driver/__pycache__/bridge.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/driver/bridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午2:59 4 | # @Author : wlkjyy 5 | # @File : bridge.py 6 | # @Software: PyCharm 7 | 8 | 9 | 10 | class Bridge: 11 | def __init__(self, utils): 12 | self.utils = utils 13 | 14 | """ 15 | 网络桥接驱动 16 | """ 17 | 18 | def install(self): 19 | """ 20 | 安装网卡桥接驱动 21 | :return: 22 | """ 23 | self.utils.set_operate(['driver', 'install']) 24 | ret_code, ret_val = self.utils.run_command(['-n', 'lwf']) 25 | if ret_code == 0: 26 | return True 27 | 28 | raise RuntimeError(ret_val) 29 | 30 | def uninstall(self): 31 | """ 32 | 卸载网卡桥接驱动 33 | :return: 34 | """ 35 | self.utils.set_operate(['driver', 'uninstall']) 36 | ret_code, ret_val = self.utils.run_command([]) 37 | if ret_code == 0: 38 | return True 39 | 40 | raise RuntimeError(ret_val) 41 | -------------------------------------------------------------------------------- /mumu/api/network/Network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午2:44 4 | # @Author : wlkjyy 5 | # @File : Network.py 6 | # @Software: PyCharm 7 | from mumu.api.setting.setting import Setting 8 | 9 | 10 | class Network: 11 | 12 | def __init__(self, utils): 13 | self.utils = utils 14 | 15 | def get_bridge_card(self): 16 | """ 17 | 获取所有网桥适配器 18 | :return: 19 | """ 20 | card = Setting().get('net_bridge_card.list') 21 | card = card[1:-1] 22 | return card.split(',') 23 | 24 | def nat(self): 25 | """ 26 | 设置为NAT 27 | :return: 28 | """ 29 | return Setting(self.utils).set( 30 | net_bridge_open=False 31 | ) 32 | 33 | def bridge(self, enable: bool = True, net_bridge_card: str = None): 34 | """ 35 | 是否启用网桥 36 | :param enable: 是否启用 37 | :return: 38 | """ 39 | return Setting(self.utils).set( 40 | net_bridge_open=enable, 41 | net_bridge_card=net_bridge_card 42 | ) 43 | 44 | def bridge_dhcp(self): 45 | """ 46 | 设置网桥为DHCP 47 | :return: 48 | """ 49 | return Setting(self.utils).set( 50 | net_bridge_ip_mode='dhcp' 51 | ) 52 | 53 | def bridge_static(self, ip_addr: str, subnet_mask: str, gateway: str, dns1: str = '8.8.8.8', dns2: str='114.114.114.114'): 54 | """ 55 | 设置网桥为静态 56 | :param ip_addr: ip地址 57 | :param subnet_mask: 子网掩码 58 | :param gateway: 网关 59 | :param dns1: DNS1 60 | :param dns2: DNS2 61 | :return: 62 | """ 63 | return Setting(self.utils).set( 64 | net_bridge_ip_mode='static', 65 | net_bridge_ip_addr=ip_addr, 66 | net_bridge_subnet_mask=subnet_mask, 67 | net_bridge_gateway=gateway, 68 | net_bridge_dns1=dns1, 69 | net_bridge_dns2=dns2 70 | ) 71 | -------------------------------------------------------------------------------- /mumu/api/network/__pycache__/Network.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/network/__pycache__/Network.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/permission/Permission.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午2:42 4 | # @Author : wlkjyy 5 | # @File : Permission.py 6 | # @Software: PyCharm 7 | from mumu.api.permission.root import Root 8 | 9 | 10 | class Permission: 11 | 12 | def __init__(self, utils): 13 | self.utils = utils 14 | 15 | @property 16 | def root(self) -> Root: 17 | 18 | return Root(self.utils) -------------------------------------------------------------------------------- /mumu/api/permission/__pycache__/Permission.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/permission/__pycache__/Permission.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/permission/__pycache__/root.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/permission/__pycache__/root.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/permission/root.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午2:17 4 | # @Author : wlkjyy 5 | # @File : root.py 6 | # @Software: PyCharm 7 | import mumu.config as config 8 | 9 | 10 | 11 | class Root: 12 | 13 | def __init__(self, utils): 14 | self.utils = utils 15 | 16 | def disable(self): 17 | """ 18 | 关闭模拟器Root权限 19 | :return: 20 | """ 21 | self.utils.set_operate("setting") 22 | ret_code, ret_val = self.utils.run_command(['-k', 'root_permission', '-val', 'false']) 23 | 24 | if ret_code != 0: 25 | raise RuntimeError(ret_val) 26 | 27 | return True 28 | 29 | def enable(self): 30 | """ 31 | 启用模拟器Root权限 32 | :return: 33 | """ 34 | self.utils.set_operate("setting") 35 | ret_code, ret_val = self.utils.run_command(['-k', 'root_permission', '-val', 'true']) 36 | 37 | if ret_code != 0: 38 | raise RuntimeError(ret_val) 39 | 40 | return True 41 | -------------------------------------------------------------------------------- /mumu/api/screen/__pycache__/screen.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/screen/__pycache__/screen.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/screen/gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/30 下午3:06 4 | # @Author : wlkjyy 5 | # @File : gui.py 6 | # @Software: PyCharm 7 | import copy 8 | import json 9 | import time 10 | 11 | import cv2 12 | import numpy 13 | import collections 14 | from adbutils import adb 15 | import threading 16 | import mumu.config as config 17 | 18 | 19 | class Gui: 20 | 21 | def __init__(self, utils): 22 | self.utils = utils 23 | 24 | def __connect(self): 25 | """ 26 | 获取可用的连接 27 | :return: 28 | """ 29 | 30 | self.utils.set_operate("adb") 31 | ret_code, retval = self.utils.run_command(['']) 32 | if ret_code != 0: 33 | return self 34 | 35 | try: 36 | data = json.loads(retval) 37 | except json.JSONDecodeError: 38 | return None, None 39 | 40 | for key, value in data.items(): 41 | if key == "adb_host" and "adb_port" in data: 42 | yield data["adb_host"], data["adb_port"], self.utils.get_vm_id() 43 | return 44 | 45 | if 'errcode' in value: 46 | continue 47 | else: 48 | # connect_list.append((value.get("adb_host"), value.get("adb_port"))) 49 | yield value.get("adb_host"), value.get("adb_port"), key 50 | 51 | return 52 | 53 | def __vm_handle_frame(self, handle, vm_id): 54 | while True: 55 | if vm_id in config.FRAME_CACHE: 56 | 57 | while True: 58 | frame = config.FRAME_CACHE[vm_id] 59 | 60 | if frame is not None: 61 | handle(frame, self.utils.get_mumu_root_object().select(vm_id)) 62 | time.sleep(0.01) 63 | 64 | else: 65 | continue 66 | 67 | def create_handle(self, handle_func): 68 | """ 69 | 创建帧处理函数 70 | :param handle_func: 71 | :return: 72 | """ 73 | try: 74 | import scrcpy 75 | except: 76 | raise RuntimeError( 77 | "scrcpy is not installed, please install it first by running 'pip install scrcpy-client'") 78 | 79 | client_list = [] 80 | 81 | for (k, v, id) in self.__connect(): 82 | host = str(k) + ":" + str(v) 83 | adb.connect(host) 84 | for i in adb.device_list(): 85 | if i.serial == host: 86 | client = scrcpy.Client(device=i) 87 | 88 | threading.Thread(target=self.__vm_handle_frame, args=(handle_func, id)).start() 89 | 90 | def func(vm_id): 91 | def save_frame(frame): 92 | config.FRAME_CACHE[vm_id] = frame 93 | return 94 | 95 | return save_frame 96 | 97 | client.add_listener(scrcpy.EVENT_FRAME, func(id)) 98 | client_list.append(client) 99 | for cli_row in client_list: 100 | cli_row.start(threaded=True, daemon_threaded=True) 101 | 102 | def locateOnScreen(self, haystack_frame, needle_image, confidence=0.8, grayscale=None): 103 | """ 104 | 在屏幕上查找图片 105 | :param haystack_frame: 屏幕帧 106 | :param needle_image: 图片 107 | :param confidence: 置信度 108 | :param grayscale: 灰度值找图 109 | :return: 110 | """ 111 | needle_image = _load_cv2(needle_image) 112 | haystack_frame = _load_cv2(haystack_frame) 113 | 114 | for r in _locateAll_opencv(needle_image, haystack_frame, confidence=confidence, grayscale=None): 115 | return r 116 | 117 | return False 118 | 119 | def center(self, box): 120 | """ 121 | 获取中心点 122 | :param box: Box 123 | :return: 124 | """ 125 | return int(box.left + box.width / 2), int(box.top + box.height / 2) 126 | 127 | def locateCenterOnScreen(self, haystack_frame, needle_image, confidence=0.8, grayscale=None): 128 | """ 129 | 在屏幕上查找图片 130 | :param haystack_frame: 屏幕帧 131 | :param needle_image: 图片 132 | :param confidence: 置信度 133 | :param grayscale: 灰度值找图 134 | :return: 135 | """ 136 | needle_image = _load_cv2(needle_image) 137 | haystack_frame = _load_cv2(haystack_frame) 138 | 139 | for r in _locateAll_opencv(needle_image, haystack_frame, confidence=confidence, grayscale=None): 140 | return self.center(r) 141 | 142 | return False 143 | 144 | def locateAllOnScreen(self, haystack_frame, needle_image, confidence=0.8, grayscale=None): 145 | """ 146 | 在屏幕上查找所有图片 147 | :param haystack_frame: 屏幕帧 148 | :param needle_image: 图片 149 | :param confidence: 置信度 150 | :param grayscale: 灰度值找图 151 | :return: 152 | """ 153 | arr = [] 154 | needle_image = _load_cv2(needle_image) 155 | haystack_frame = _load_cv2(haystack_frame) 156 | 157 | for r in _locateAll_opencv(needle_image, haystack_frame, confidence=confidence, grayscale=None): 158 | arr.append(r) 159 | 160 | return arr 161 | 162 | 163 | 164 | def save(self, frame, path): 165 | """ 166 | 保存帧 167 | :param path: 路径 168 | :return: 169 | """ 170 | cv2.imwrite(path, frame) 171 | 172 | 173 | GRAYSCALE_DEFAULT = True 174 | USE_IMAGE_NOT_FOUND_EXCEPTION = True 175 | Box = collections.namedtuple('Box', 'left top width height') 176 | 177 | 178 | def _load_cv2(img, grayscale=None): 179 | """ 180 | TODO 181 | """ 182 | # load images if given filename, or convert as needed to opencv 183 | # Alpha layer just causes failures at this point, so flatten to RGB. 184 | # RGBA: load with -1 * cv2.CV_LOAD_IMAGE_COLOR to preserve alpha 185 | # to matchTemplate, need template and image to be the same wrt having alpha 186 | 187 | if grayscale is None: 188 | grayscale = GRAYSCALE_DEFAULT 189 | if isinstance(img, str): 190 | # The function imread loads an image from the specified file and 191 | # returns it. If the image cannot be read (because of missing 192 | # file, improper permissions, unsupported or invalid format), 193 | # the function returns an empty matrix 194 | # http://docs.opencv.org/3.0-beta/modules/imgcodecs/doc/reading_and_writing_images.html 195 | if grayscale: 196 | img_cv = cv2.imread(img, cv2.IMREAD_GRAYSCALE) 197 | else: 198 | img_cv = cv2.imread(img, cv2.IMREAD_COLOR) 199 | if img_cv is None: 200 | raise IOError( 201 | "Failed to read %s because file is missing, " 202 | "has improper permissions, or is an " 203 | "unsupported or invalid format" % img 204 | ) 205 | elif isinstance(img, numpy.ndarray): 206 | # don't try to convert an already-gray image to gray 207 | if grayscale and len(img.shape) == 3: # and img.shape[2] == 3: 208 | img_cv = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 209 | else: 210 | img_cv = img 211 | elif hasattr(img, 'convert'): 212 | # assume its a PIL.Image, convert to cv format 213 | img_array = numpy.array(img.convert('RGB')) 214 | img_cv = img_array[:, :, ::-1].copy() # -1 does RGB -> BGR 215 | if grayscale: 216 | img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY) 217 | else: 218 | raise TypeError('expected an image filename, OpenCV numpy array, or PIL image') 219 | return img_cv 220 | 221 | 222 | def _locateAll_opencv(needleImage, haystackImage, grayscale=None, limit=10000, region=None, step=1, confidence=0.999): 223 | """ 224 | TODO - rewrite this 225 | faster but more memory-intensive than pure python 226 | step 2 skips every other row and column = ~3x faster but prone to miss; 227 | to compensate, the algorithm automatically reduces the confidence 228 | threshold by 5% (which helps but will not avoid all misses). 229 | limitations: 230 | - OpenCV 3.x & python 3.x not tested 231 | - RGBA images are treated as RBG (ignores alpha channel) 232 | """ 233 | if grayscale is None: 234 | grayscale = GRAYSCALE_DEFAULT 235 | 236 | confidence = float(confidence) 237 | 238 | needleImage = _load_cv2(needleImage, grayscale) 239 | needleHeight, needleWidth = needleImage.shape[:2] 240 | haystackImage = _load_cv2(haystackImage, grayscale) 241 | 242 | if region: 243 | haystackImage = haystackImage[region[1]: region[1] + region[3], region[0]: region[0] + region[2]] 244 | else: 245 | region = (0, 0) # full image; these values used in the yield statement 246 | if haystackImage.shape[0] < needleImage.shape[0] or haystackImage.shape[1] < needleImage.shape[1]: 247 | # avoid semi-cryptic OpenCV error below if bad size 248 | raise ValueError('needle dimension(s) exceed the haystack image or region dimensions') 249 | 250 | if step == 2: 251 | confidence *= 0.95 252 | needleImage = needleImage[::step, ::step] 253 | haystackImage = haystackImage[::step, ::step] 254 | else: 255 | step = 1 256 | 257 | # get all matches at once, credit: https://stackoverflow.com/questions/7670112/finding-a-subimage-inside-a-numpy-image/9253805#9253805 258 | result = cv2.matchTemplate(haystackImage, needleImage, cv2.TM_CCOEFF_NORMED) 259 | match_indices = numpy.arange(result.size)[(result > confidence).flatten()] 260 | matches = numpy.unravel_index(match_indices[:limit], result.shape) 261 | 262 | if len(matches[0]) == 0: 263 | if USE_IMAGE_NOT_FOUND_EXCEPTION: 264 | # raise ImageNotFoundException('Could not locate the image (highest confidence = %.3f)' % result.max()) 265 | # print('Could not locate the image (highest confidence = %.3f)' % result.max()) 266 | return 267 | else: 268 | return 269 | 270 | # use a generator for API consistency: 271 | matchx = matches[1] * step + region[0] # vectorized 272 | matchy = matches[0] * step + region[1] 273 | for x, y in zip(matchx, matchy): 274 | yield Box(x, y, needleWidth, needleHeight) 275 | -------------------------------------------------------------------------------- /mumu/api/screen/screen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午8:54 4 | # @Author : wlkjyy 5 | # @File : screen.py.py 6 | # @Software: PyCharm 7 | 8 | 9 | from mumu.api.setting.setting import Setting 10 | 11 | 12 | class Screen: 13 | 14 | def __init__(self, utils): 15 | self.utils = utils 16 | 17 | def resolution(self, width: int, height: int): 18 | """ 19 | 修改分辨率 20 | :param width: 21 | :param height: 22 | :return: 23 | """ 24 | return Setting(self.utils).set( 25 | resolution_height__custom=height, 26 | resolution_width__custom=width 27 | ) 28 | 29 | def resolution_mobile(self): 30 | """ 31 | 设置为手机分辨率 32 | :return: 33 | """ 34 | self.resolution(width=1080, height=1920) 35 | self.dpi(480) 36 | 37 | def resolution_tablet(self): 38 | """ 39 | 设置为平板分辨率 40 | :return: 41 | """ 42 | self.resolution(width=1920, height=1080) 43 | self.dpi(280) 44 | 45 | def resolution_ultrawide(self): 46 | """ 47 | 设置为超宽屏分辨率 48 | :return: 49 | """ 50 | self.resolution(width=3200, height=1440) 51 | self.dpi(400) 52 | 53 | def dpi(self, dpi: int): 54 | """ 55 | 修改dpi 56 | :param dpi: 57 | :return: 58 | """ 59 | return Setting(self.utils).set( 60 | resolution_dpi__custom=dpi 61 | ) 62 | 63 | def brightness(self, brightness: int): 64 | """ 65 | 修改模拟器亮度 66 | :param brightness: 亮度值 1-100 67 | :return: 68 | """ 69 | brightness = max(1, min(100, brightness)) 70 | 71 | return Setting(self.utils).set( 72 | screen_brightness=brightness 73 | ) 74 | 75 | def max_frame_rate(self, frame_rate: int = 60): 76 | """ 77 | 修改模拟器最大帧率 78 | :param frame_rate: 最大帧率 1-240 79 | :return: 80 | """ 81 | frame_rate = max(1, min(240, frame_rate)) 82 | 83 | return Setting(self.utils).set( 84 | max_frame_rate=frame_rate 85 | ) 86 | 87 | def dynamic_adjust_frame_rate(self, enable: bool, dynamic_low_frame_rate_limit: int = 15): 88 | """ 89 | 是否开启动态调整帧率 90 | :param enable: 是否开启 91 | :param dynamic_low_frame_rate_limit: 当主操作窗口不是本模拟器时候,降低帧率到多少 92 | :return: 93 | """ 94 | return Setting(self.utils).set( 95 | dynamic_adjust_frame_rate=enable, 96 | dynamic_low_frame_rate_limit=dynamic_low_frame_rate_limit 97 | ) 98 | 99 | def vertical_sync(self, enable: bool): 100 | """ 101 | 是否开启垂直同步 102 | :param enable: 是否开启 103 | :return: 104 | """ 105 | return Setting(self.utils).set( 106 | vertical_sync=enable 107 | ) 108 | 109 | def show_frame_rate(self, enable: bool): 110 | """ 111 | 是否显示帧率 112 | :param enable: 是否显示 113 | :return: 114 | """ 115 | return Setting(self.utils).set( 116 | show_frame_rate=enable 117 | ) 118 | 119 | def window_auto_rotate(self, enable: bool): 120 | """ 121 | 是否开启窗口自动旋转 122 | 123 | 开启后,模拟器会根据程序自动旋转窗口 124 | :param enable: 是否开启 125 | :return: 126 | """ 127 | return Setting(self.utils).set( 128 | window_auto_rotate=enable 129 | ) 130 | -------------------------------------------------------------------------------- /mumu/api/setting/__pycache__/setting.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-wlkjyy/mumu-python-api/7f861ceeb4bd3a7bc0d6fec73dac5d043a160f88/mumu/api/setting/__pycache__/setting.cpython-38.pyc -------------------------------------------------------------------------------- /mumu/api/setting/setting.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午8:20 4 | # @Author : wlkjyy 5 | # @File : setting.py 6 | # @Software: PyCharm 7 | import json 8 | import os.path 9 | from typing import Union 10 | 11 | 12 | class Setting: 13 | 14 | def __init__(self, utils): 15 | self.utils = utils 16 | 17 | def all(self, all_writable: bool = False) -> dict: 18 | """ 19 | 获取所有配置项 20 | :return: 21 | """ 22 | self.utils.set_operate("setting") 23 | if all_writable: 24 | args = ["-aw"] 25 | else: 26 | args = ["-a"] 27 | 28 | ret_code, retval = self.utils.run_command(args) 29 | if ret_code == 0: 30 | return json.loads(retval) 31 | 32 | raise RuntimeError(retval) 33 | 34 | def get(self, *args) -> Union[dict, str]: 35 | """ 36 | 获取一个或多个配置项 37 | :param args: 38 | :return: 39 | """ 40 | self.utils.set_operate("setting") 41 | command_args = [] 42 | for arg in args: 43 | command_args.extend(["-k", arg]) 44 | 45 | ret_code, retval = self.utils.run_command(command_args) 46 | if ret_code == 0: 47 | ret = json.loads(retval) 48 | for key in ret.keys(): 49 | # 类型转换 50 | val = ret[key] 51 | if val.isdigit(): 52 | ret[key] = int(val) 53 | elif val.lower() == "true": 54 | ret[key] = True 55 | elif val.lower() == "false": 56 | ret[key] = False 57 | 58 | # return ret 59 | if len(args) == 1: 60 | return ret[args[0]] 61 | 62 | return ret 63 | 64 | raise RuntimeError(retval) 65 | 66 | def set(self, **kwargs) -> bool: 67 | """ 68 | 设置一个或多个配置项 69 | :param kwargs: 70 | :return: 71 | """ 72 | self.utils.set_operate("setting") 73 | command_args = [] 74 | for key in kwargs.keys(): 75 | 76 | if isinstance(kwargs[key], bool): 77 | kwargs[key] = str(kwargs[key]).lower() 78 | 79 | if kwargs[key] is None: 80 | kwargs[key] = "__null__" 81 | 82 | new_key = key 83 | if '___' in key: 84 | new_key = key.replace('___', '-') 85 | 86 | if '__' in key: 87 | new_key = key.replace('__', '.') 88 | 89 | command_args.extend(["-k", new_key, "-val", str(kwargs[key])]) 90 | 91 | ret_code, retval = self.utils.run_command(command_args) 92 | if ret_code == 0: 93 | return True 94 | 95 | raise RuntimeError(retval) 96 | 97 | def set_by_json(self, file_path: str) -> bool: 98 | """ 99 | 通过json文件设置配置项 100 | :param file_path: json文件路径 101 | :return: 102 | """ 103 | if not os.path.exists(file_path): 104 | raise FileNotFoundError(file_path) 105 | 106 | self.utils.set_operate("setting") 107 | ret_code, retval = self.utils.run_command(["-p", file_path]) 108 | 109 | if ret_code == 0: 110 | return True 111 | 112 | raise RuntimeError(retval) 113 | 114 | def equal(self, key: str, value) -> bool: 115 | """ 116 | 判断配置项是否等于某个值 117 | :param key: 配置项 118 | :param value: 值 119 | :return: 120 | """ 121 | return self.get(key) == value 122 | 123 | def not_equal(self, key: str, value) -> bool: 124 | """ 125 | 判断配置项是否不等于某个值 126 | :param key: 配置项 127 | :param value: 值 128 | :return: 129 | """ 130 | return self.get(key) != value 131 | 132 | def equal_then_set(self, key: str, value, new_value) -> bool: 133 | """ 134 | 判断配置项是否等于某个值并设置新值 135 | :param key: 配置项 136 | :param value: 值 137 | :param new_value: 新值 138 | :return: 139 | """ 140 | if self.equal(key, value): 141 | return self.set(**{key: new_value}) 142 | 143 | return False 144 | 145 | def not_equal_then_set(self, key: str, value, new_value=None) -> bool: 146 | """ 147 | 判断配置项是否不等于某个值并设置新值 148 | :param key: 配置项 149 | :param value: 值 150 | :param new_value: 新值 151 | :return: 152 | """ 153 | if self.not_equal(key, value): 154 | if new_value is None: 155 | return self.set(**{key: value}) 156 | return self.set(**{key: new_value}) 157 | 158 | return False 159 | -------------------------------------------------------------------------------- /mumu/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午2:24 4 | # @Author : wlkjyy 5 | # @File : config.py 6 | # @Software: PyCharm 7 | 8 | MUMU_PATH = None 9 | VM_INDEX = None 10 | OPERATE = None 11 | ADB_PATH = None 12 | FRAME_CACHE = {} 13 | FRAME_UPDATE_TIME = {} 14 | -------------------------------------------------------------------------------- /mumu/constant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/29 下午7:35 4 | # @Author : wlkjyy 5 | # @File : constant.py 6 | # @Software: PyCharm 7 | import os 8 | import random 9 | 10 | 11 | class MacAddress: 12 | 13 | @staticmethod 14 | def random() -> str: 15 | """ 16 | 生成一个随机的MAC地址 17 | :return: 18 | """ 19 | mac = [random.randint(0x00, 0xff) for _ in range(6)] 20 | return ':'.join(map(lambda x: "%02x" % x, mac)) 21 | 22 | 23 | class IMEI: 24 | 25 | @staticmethod 26 | def random() -> str: 27 | """ 28 | 生成一个随机的IMEI 29 | 30 | 来源:chatgpt-3.5 31 | :return: 32 | """ 33 | # Generate TAC (first 8 digits) 34 | tac = f"{random.randint(10 ** 5, 10 ** 6 - 1):06d}" 35 | 36 | # Generate FAC (next 2 digits) 37 | fac = f"{random.randint(0, 99):02d}" 38 | 39 | # Generate SNR (next 6 digits) 40 | snr = f"{random.randint(10 ** 5, 10 ** 6 - 1):06d}" 41 | 42 | # Concatenate parts 43 | imei_base = tac + fac + snr 44 | 45 | # Calculate Check Digit (SP) 46 | def calculate_check_digit(imei): 47 | digits = list(map(int, imei)) 48 | odd_digits = digits[0::2] 49 | even_digits = [sum(divmod(2 * d, 10)) + 2 * d // 10 for d in digits[1::2]] 50 | total = sum(odd_digits + even_digits) 51 | return (10 - total % 10) % 10 52 | 53 | check_digit = calculate_check_digit(imei_base) 54 | 55 | # Construct full IMEI 56 | imei = imei_base + str(check_digit) 57 | 58 | return imei 59 | 60 | 61 | class IMSI: 62 | 63 | @staticmethod 64 | def random() -> str: 65 | """ 66 | 生成一个随机的IMSI 67 | :return: 68 | """ 69 | mcc = random.choice(['302', '310', '334', '460']) # Example MCCs for demonstration 70 | 71 | # MNC (Mobile Network Code, 2 or 3 digits) 72 | mnc = f"{random.randint(0, 999):03d}"[:2] # Random 2-digit MNC 73 | 74 | # MSIN (Mobile Subscriber Identification Number, 10 digits) 75 | msin = f"{random.randint(0, 10 ** 10 - 1):010d}" 76 | 77 | # Concatenate parts 78 | imsi = mcc + mnc + msin 79 | 80 | return imsi 81 | 82 | 83 | class AndroidID: 84 | 85 | @staticmethod 86 | def random() -> str: 87 | """ 88 | 生成一个随机的AndroidId 89 | :return: 90 | """ 91 | return ''.join(random.choices('0123456789abcdef', k=16)) 92 | 93 | 94 | class PhoneNumber: 95 | 96 | @staticmethod 97 | def random() -> str: 98 | """ 99 | 生成一个随机的手机号 100 | :return: 101 | """ 102 | prefix = random.choice(['130', '131', '132', '133', '134', '135', '136', '137', '138', '139', 103 | '147', '150', '151', '152', '153', '155', '156', '157', '158', '159', 104 | '186', '187', '188', '189', '199']) 105 | 106 | return prefix + ''.join(random.choices('0123456789', k=8)) 107 | 108 | 109 | class GPU: 110 | # 顶配型号 111 | TOP_MODEL = 'Adreno (TM) 740' 112 | 113 | # 中配型号 114 | MIDDLE_MODEL = 'Adreno (TM) 640' 115 | 116 | # 低配型号 117 | LOW_MODEL = 'Adreno (TM) 530' 118 | 119 | 120 | class AndroidKey: 121 | # 菜单键 122 | KEYCODE_MENU = 1 123 | 124 | # 回到桌面 125 | KEYCODE_HOME = 3 126 | 127 | # 返回键 128 | KEYCODE_BACK = 4 129 | 130 | # 拨号键 131 | KEYCODE_CALL = 5 132 | 133 | # 挂断键 134 | KEYCODE_ENDCALL = 6 135 | 136 | # 搜索键 137 | KEYCODE_SEARCH = 84 138 | 139 | # 拍照键 140 | KEYCODE_CAMERA = 27 141 | 142 | # 相机对焦键 143 | KEYCODE_FOCUS = 80 144 | 145 | # 电源键 146 | KEYCODE_POWER = 26 147 | 148 | # 通知键 149 | KEYCODE_NOTIFICATION = 83 150 | 151 | # 禁音键 152 | KEYCODE_MUTE = 91 153 | 154 | # 扬声器静音键 155 | KEYCODE_VOLUME_MUTE = 164 156 | 157 | # 音量增加键 158 | KEYCODE_VOLUME_UP = 24 159 | 160 | # 音量减小键 161 | KEYCODE_VOLUME_DOWN = 25 162 | 163 | # 回车键 164 | KEYCODE_ENTER = 66 165 | 166 | # ESC键 167 | KEYCODE_ESCAPE = 111 168 | 169 | # 导航键 170 | KEYCODE_DPAD_CENTER = 23 171 | 172 | # 向上导航键 173 | KEYCODE_DPAD_UP = 19 174 | 175 | # 向下导航键 176 | KEYCODE_DPAD_DOWN = 20 177 | 178 | # 向左导航键 179 | KEYCODE_DPAD_LEFT = 21 180 | 181 | # 向右导航键 182 | KEYCODE_DPAD_RIGHT = 22 183 | 184 | # 退格 185 | KEYCODE_DEL = 67 186 | 187 | # TAB 188 | KEYCODE_TAB = 61 189 | 190 | # 放大键 191 | KEYCODE_ZOOM_IN = 168 192 | 193 | -------------------------------------------------------------------------------- /mumu/control.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/28 下午10:22 4 | # @Author : wlkjyy 5 | # @File : control.py 6 | # @Software: PyCharm 7 | import os.path 8 | 9 | from mumu.utils import run_command 10 | from mumu.api.permission.root import Root 11 | 12 | 13 | class Control: 14 | 15 | __mumu_path = None 16 | __vm_index = None 17 | def __init__(self, mumu, vm_index): 18 | self.__mumu_path = mumu 19 | self.__vm_index = vm_index 20 | 21 | def start(self, package=None) -> bool: 22 | """ 23 | 启动模拟器 24 | :param package: 启动完成后打开指定包名的应用 25 | :return: 26 | """ 27 | 28 | if isinstance(self.__vm_index, int): 29 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 30 | else: 31 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 32 | 33 | args.append("launch") 34 | if package is not None: 35 | args.append("-pkg") 36 | args.append(package) 37 | 38 | ret_code, retval = run_command(args) 39 | 40 | if ret_code != 0: 41 | raise RuntimeError(retval) 42 | 43 | return True 44 | 45 | def shutdown(self): 46 | """ 47 | 关闭模拟器 48 | :return: 49 | """ 50 | if isinstance(self.__vm_index, int): 51 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 52 | else: 53 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 54 | 55 | args.append("shutdown") 56 | 57 | ret_code, retval = run_command(args) 58 | 59 | if ret_code != 0: 60 | raise RuntimeError(retval) 61 | 62 | return True 63 | 64 | def restart(self): 65 | """ 66 | 重启模拟器 67 | :param package: 重启完成后打开指定包名的应用 68 | :return: 69 | """ 70 | if isinstance(self.__vm_index, int): 71 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 72 | else: 73 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 74 | 75 | args.append("restart") 76 | 77 | ret_code, retval = run_command(args) 78 | 79 | if ret_code != 0: 80 | raise RuntimeError(retval) 81 | 82 | return True 83 | 84 | def show_window(self): 85 | """ 86 | 显示窗口 87 | :return: 88 | """ 89 | if isinstance(self.__vm_index, int): 90 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 91 | else: 92 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 93 | 94 | args.append("show_window") 95 | 96 | ret_code, retval = run_command(args) 97 | 98 | if ret_code != 0: 99 | raise RuntimeError(retval) 100 | 101 | return True 102 | 103 | def hide_window(self): 104 | """ 105 | 隐藏窗口 106 | 相当于后台运行 107 | :return: 108 | """ 109 | if isinstance(self.__vm_index, int): 110 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 111 | else: 112 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 113 | 114 | args.append("hide_window") 115 | 116 | ret_code, retval = run_command(args) 117 | 118 | if ret_code != 0: 119 | raise RuntimeError(retval) 120 | 121 | return True 122 | 123 | def install(self, apk: str) -> bool: 124 | """ 125 | 安装应用 126 | :param apk: apk文件路径 127 | :return: 128 | """ 129 | if not os.path.exists(apk): 130 | raise FileNotFoundError(apk) 131 | if isinstance(self.__vm_index, int): 132 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 133 | else: 134 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 135 | 136 | args.append("app") 137 | args.append("install") 138 | args.append("-apk") 139 | args.append(apk) 140 | 141 | ret_code, retval = run_command(args) 142 | 143 | if ret_code != 0: 144 | raise RuntimeError(retval) 145 | 146 | return True 147 | 148 | def uninstall(self, package: str) -> bool: 149 | """ 150 | 卸载应用 151 | :param package: 包名 152 | :return: 153 | """ 154 | if isinstance(self.__vm_index, int): 155 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 156 | else: 157 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 158 | 159 | args.append("app") 160 | args.append("uninstall") 161 | args.append("-pkg") 162 | args.append(package) 163 | 164 | ret_code, retval = run_command(args) 165 | 166 | if ret_code != 0: 167 | raise RuntimeError(retval) 168 | 169 | return True 170 | 171 | def launch(self, package: str) -> bool: 172 | """ 173 | 打开应用 174 | :param package: 包名 175 | :return: 176 | """ 177 | if isinstance(self.__vm_index, int): 178 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 179 | else: 180 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 181 | 182 | args.append("app") 183 | args.append("launch") 184 | args.append("-pkg") 185 | args.append(package) 186 | 187 | ret_code, retval = run_command(args) 188 | 189 | if ret_code != 0: 190 | raise RuntimeError(retval) 191 | 192 | return True 193 | 194 | def close(self, package: str) -> bool: 195 | """ 196 | 关闭应用 197 | :param package: 包名 198 | :return: 199 | """ 200 | if isinstance(self.__vm_index, int): 201 | args = [self.__mumu_path, "control", "-v", str(self.__vm_index)] 202 | else: 203 | args = [self.__mumu_path, "control", "-v", ",".join([str(i) for i in self.__vm_index])] 204 | 205 | args.append("app") 206 | args.append("close") 207 | args.append("-pkg") 208 | args.append(package) 209 | 210 | ret_code, retval = run_command(args) 211 | 212 | if ret_code != 0: 213 | raise RuntimeError(retval) 214 | 215 | return True 216 | 217 | def root(self): 218 | return Root(self.__mumu_path, self.__vm_index) 219 | 220 | def enable_root_permission(self): 221 | """ 222 | 开启ROOT权限 223 | :return: 224 | """ 225 | if isinstance(self.__vm_index, int): 226 | args = [self.__mumu_path, "setting", "-v", str(self.__vm_index)] 227 | else: 228 | args = [self.__mumu_path, "setting", "-v", ",".join([str(i) for i in self.__vm_index])] 229 | 230 | args.append("-k") 231 | args.append("root_permission") 232 | args.append("-val") 233 | args.append("true") 234 | ret_code, retval = run_command(args) 235 | 236 | if ret_code != 0: 237 | raise RuntimeError(retval) 238 | 239 | return True 240 | 241 | def disable_root_permission(self): 242 | """ 243 | 关闭ROOT权限 244 | :return: 245 | """ 246 | if isinstance(self.__vm_index, int): 247 | args = [self.__mumu_path, "setting", "-v", str(self.__vm_index)] 248 | else: 249 | args = [self.__mumu_path, "setting", "-v", ",".join([str(i) for i in self.__vm_index])] 250 | 251 | args.append("-k") 252 | args.append("root_permission") 253 | args.append("-val") 254 | args.append("false") 255 | ret_code, retval = run_command(args) 256 | 257 | if ret_code != 0: 258 | raise RuntimeError(retval) 259 | 260 | return True 261 | -------------------------------------------------------------------------------- /mumu/mumu.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/28 下午9:36 4 | # @Author : wlkjyy 5 | # @File : mumu.py 6 | # @Software: PyCharm 7 | import os.path 8 | from typing import Union 9 | 10 | from mumu.api.adb.Adb import Adb 11 | from mumu.api.core.Core import Core 12 | from mumu.api.core.app import App 13 | from mumu.api.core.performance import Performance 14 | from mumu.api.core.power import Power 15 | from mumu.api.core.shortcut import Shortcut 16 | from mumu.api.core.simulation import Simulation 17 | from mumu.api.core.window import Window 18 | from mumu.api.develop.androidevent import AndroidEvent 19 | from mumu.api.driver.Driver import Driver 20 | import mumu.config as config 21 | from mumu.api.network.Network import Network 22 | from mumu.api.permission.Permission import Permission 23 | from mumu.api.screen.gui import Gui 24 | from mumu.api.screen.screen import Screen 25 | from mumu.api.setting.setting import Setting 26 | from mumu.utils import utils 27 | 28 | 29 | class Mumu: 30 | __mumu_manager = r"D:\Program Files\Netease\MuMu Player 12\shell\MuMuManager.exe" 31 | 32 | __vm_index = None 33 | 34 | def __init__(self, mumu_manager_path=None): 35 | if mumu_manager_path is not None: 36 | # 后续创建的实例未设置时使用之前设置的路径 37 | if config.MUMU_PATH is None: 38 | self.__mumu_manager = mumu_manager_path 39 | else: 40 | self.__mumu_manager = config.MUMU_PATH 41 | 42 | if not os.path.exists(self.__mumu_manager): 43 | raise RuntimeError(f"MuMuManager.exe not found in {self.__mumu_manager}") 44 | 45 | base_path = os.path.dirname(self.__mumu_manager) 46 | 47 | config.MUMU_PATH = self.__mumu_manager 48 | config.ADB_PATH = os.path.join(base_path, "adb.exe") 49 | 50 | def select(self, vm_index: Union[int, list, tuple] = None, *args): 51 | """ 52 | 选择要操作的模拟器索引 53 | :param vm_index: 模拟器索引 54 | :param args: 更多的模拟器索引 55 | :return: 56 | 57 | Example: 58 | Mumu().select(1) 59 | Mumu().select(1, 2, 3) 60 | Mumu().select([1, 2, 3]) 61 | Mumu().select((1, 2, 3)) 62 | """ 63 | 64 | if vm_index is None: 65 | self.__vm_index = 'all' 66 | return self 67 | 68 | if len(args) > 0: 69 | if isinstance(vm_index, int): 70 | vm_index = [vm_index] 71 | else: 72 | vm_index = list(vm_index) 73 | 74 | vm_index.extend(args) 75 | 76 | if isinstance(vm_index, int): 77 | self.__vm_index = str(vm_index) 78 | else: 79 | vm_index = list(set(vm_index)) 80 | self.__vm_index = ",".join([str(i) for i in vm_index]) 81 | 82 | return self 83 | 84 | def generate_utils(self) -> utils: 85 | return utils().set_vm_index(self.__vm_index).set_mumu_root_object(self) 86 | 87 | def all(self): 88 | """ 89 | 选择所有模拟器 90 | :return: 91 | """ 92 | self.__vm_index = 'all' 93 | return self 94 | 95 | @property 96 | def core(self) -> Core: 97 | """ 98 | 模拟器类 99 | :return: 100 | """ 101 | return Core(self.generate_utils()) 102 | 103 | @property 104 | def driver(self) -> Driver: 105 | """ 106 | 驱动类 107 | 108 | 已完成 109 | :return: 110 | """ 111 | 112 | return Driver(self.generate_utils()) 113 | 114 | @property 115 | def permission(self) -> Permission: 116 | """ 117 | 权限类 118 | 119 | 已完成 120 | :return: 121 | """ 122 | return Permission(self.generate_utils()) 123 | 124 | @property 125 | def power(self): 126 | """ 127 | 电源类 128 | 129 | 已完成 130 | :return: 131 | """ 132 | return Power(self.generate_utils()) 133 | 134 | @property 135 | def window(self) -> Window: 136 | """ 137 | 窗口类 138 | 139 | 已完成 140 | :return: 141 | """ 142 | 143 | return Window(self.generate_utils()) 144 | 145 | @property 146 | def app(self) -> App: 147 | """ 148 | app类 149 | 150 | 已完成 151 | :return: 152 | """ 153 | 154 | return App(self.generate_utils()) 155 | 156 | @property 157 | def androidEvent(self) -> AndroidEvent: 158 | """ 159 | 安卓事件类 160 | 161 | 已完成 162 | :return: 163 | """ 164 | return AndroidEvent(self.generate_utils()) 165 | 166 | @property 167 | def shortcut(self) -> Shortcut: 168 | """ 169 | 快捷方式类 170 | 171 | 已完成 172 | :return: 173 | """ 174 | return Shortcut(self.generate_utils()) 175 | 176 | @property 177 | def simulation(self) -> Simulation: 178 | """ 179 | 机型类(这玩意很鸡肋,没什么用) 180 | 181 | 已完成 182 | :return: 183 | """ 184 | return Simulation(self.generate_utils()) 185 | 186 | @property 187 | def setting(self) -> Setting: 188 | """ 189 | 配置类 190 | :return: 191 | """ 192 | 193 | return Setting(self.generate_utils()) 194 | 195 | @property 196 | def screen(self) -> Screen: 197 | """ 198 | 屏幕类 199 | :return: 200 | """ 201 | return Screen(self.generate_utils()) 202 | 203 | @property 204 | def performance(self) -> Performance: 205 | """ 206 | 性能类 207 | :return: 208 | """ 209 | return Performance(self.generate_utils()) 210 | 211 | @property 212 | def network(self): 213 | """ 214 | 网路操作类 215 | :return: 216 | """ 217 | 218 | return Network(self.generate_utils()) 219 | 220 | @property 221 | def adb(self) -> Adb: 222 | """ 223 | ADB类 224 | :return: 225 | """ 226 | return Adb(self.generate_utils()) 227 | 228 | @property 229 | def auto(self) -> Gui: 230 | """ 231 | GUI自动化类 232 | :return: 233 | """ 234 | 235 | try: 236 | import cv2 237 | except ImportError: 238 | raise ImportError("if you want to use autoGui class, you should install opencv-python") 239 | 240 | return Gui(self.generate_utils()) 241 | -------------------------------------------------------------------------------- /mumu/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2024/7/28 下午10:25 4 | # @Author : wlkjyy 5 | # @File : utils.py 6 | # @Software: PyCharm 7 | 8 | import subprocess 9 | from typing import Union, List 10 | 11 | import mumu.config as config 12 | 13 | 14 | class utils: 15 | __VM_INDEX = None 16 | __OPERATE = None 17 | __MUMU_ROOT_OBJECT = None 18 | 19 | def set_vm_index(self, vm_index): 20 | """ 21 | 设置虚拟机索引 22 | :param vm_index: 23 | :return: 24 | """ 25 | self.__VM_INDEX = vm_index 26 | 27 | return self 28 | 29 | def set_operate(self, operate: Union[str, list]): 30 | """ 31 | 设置操作 32 | :param operate: 33 | :return: 34 | """ 35 | self.__OPERATE = operate 36 | 37 | def set_mumu_root_object(self, mumu_root_object): 38 | """ 39 | 设置mumu_root_object 40 | :param mumu_root_object: 41 | :return: 42 | """ 43 | self.__MUMU_ROOT_OBJECT = mumu_root_object 44 | return self 45 | 46 | def get_mumu_root_object(self): 47 | return self.__MUMU_ROOT_OBJECT 48 | 49 | def get_vm_id(self): 50 | return self.__VM_INDEX 51 | 52 | def run_command(self, command, mumu=True): 53 | """ 54 | 执行命令 55 | :param mumu: 56 | :param command: 57 | :return: 58 | """ 59 | try: 60 | if mumu: 61 | command_extend = [config.MUMU_PATH] 62 | 63 | if self.__OPERATE is not None: 64 | if isinstance(self.__OPERATE, list): 65 | command_extend.extend(self.__OPERATE) 66 | else: 67 | command_extend.append(self.__OPERATE) 68 | 69 | if self.__VM_INDEX is not None: 70 | command_extend.extend(['-v', self.__VM_INDEX]) 71 | 72 | command_extend.extend(command) 73 | else: 74 | command_extend = command 75 | 76 | result = subprocess.run(command_extend, shell=True, check=False, stdout=subprocess.PIPE, 77 | stderr=subprocess.PIPE, 78 | encoding='utf-8') 79 | 80 | ret_code = result.returncode 81 | retval = result.stdout 82 | 83 | # print(retval) 84 | 85 | return ret_code, retval 86 | 87 | except subprocess.CalledProcessError as e: 88 | ret_code = e.returncode 89 | retval = e.stderr 90 | 91 | return ret_code, retval 92 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python 2 | numpy==1.24.4 3 | adbutils==0.4.7 4 | adbutils==1.2.15 -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from mumu.mumu import Mumu 4 | import cv2 5 | 6 | Mumu().select(3).power.restart() --------------------------------------------------------------------------------