⚡瑞莎 Radxa ROCK 5B+(RK3588)单板搭建高性能安卓游戏脚本机
⚡瑞莎 Radxa ROCK 5B+(RK3588)单板搭建高性能安卓游戏脚本机
🧩 瑞莎 Radxa ROCK 5B+(RK3588)单板搭建高性能安卓游戏脚本机
🎯 1. 目标与预算
✅ 最终目标:
- 设备搭载并稳定运行 Android 12 系统
- 设备完整支持 Google 服务框架(GMS),可运行Google Play、Gmail等应用
- 设备能够 流畅运行对图形和资源要求较高的大型安卓游戏,如《ARK: Ultimate Mobile Edition》、《原神》等
- 设备可运行 内置自动化脚本,实现领取登录奖励,刷初始号等复杂任务
- 设备可运行 自定义开机启动脚本,上电即运行预设逻辑或启动自动任务
💰 预算:
总预算上限:约¥2000
🛠️ 2. 硬件选型
为了顺利运行大型安卓手游,系统配置至少需满足以下标准:
- 内存:≥ 8GB(推荐16GB)
- 存储:≥ 250GB,需支持高速读写(读写速度至少每秒千兆级,避免加载卡顿)
🔍 采购配件:
- Radxa ROCK 5B+ 主板套装(RK3588, 16GB RAM + 电源 + 散热风扇 + 金属外壳):¥1332
- Radxa 原厂 8英寸 HD 触摸屏(800×1280 分辨率):¥366
- SanDisk 250GB NVMe SSD(M.2 接口,读取2400MB/s,写入1500MB/s):¥219
- SanDisk 64GB 高速 SD 卡 ×2(读取140MB/s):¥72
💡 小提示:适配 Radxa 安卓系统的触摸屏目前只能选官方出品的型号,亲测其它品牌基本无法兼容,基本没有选择空间。
此外,虽然 Radxa 提供了集成 eMMC 的主板套装,但相比之下,自行搭配高性能 SD 卡在价格和灵活性上更具优势。
至于为什么买了两张 SD 卡?后文会揭晓用途
📦 其他配件(若无则需额外采购):
由于系统从零开始部署,以下配件几乎必不可少:
- SD 卡读卡器:用于烧录系统镜像,市场估价¥10-30
- USB-Type A / Type C 数据线:用于连接主机进行 adb 调试,市场估价¥10-¥25
- USB 转串口线(可选):排查启动失败或系统异常时的调试利器,市场估价¥15-60
🧠 3. 软件架构
💡 设计思路
Radxa ROCK 5B+ 支持从 SD 卡、eMMC 和 NVMe(M.2 接口)启动 Android 系统。虽然官方声称支持 NVMe 启动,但经过我的实测与技术咨询,系统实际上只能安装在采用 FAT32 文件系统的 NVMe 存储设备上。
但遗憾的是,市面上主流的大容量 SSD 通常预设为 NTFS 或 ext4 格式,不支持直接安装 Android 系统,也无法从中启动。因此,我采取以下方案:
- 使用一张 64GB SD 卡 安装 Android 系统,作为启动盘;
- 系统启动后再挂载一块 250GB NVMe SSD,作为游戏数据等大文件的存储盘。
此外,原计划是在系统内部运行自动化脚本。但经过测试发现,Radxa 官方提供的 Android 镜像为开发者调试版,仅支持 adb root
,不具备系统级 root 权限,因此无法单独通过单板运行自动化脚本。
因此,我采用外部自动化控制架构:通过 PC 上的脚本远程控制 Android 系统完成任务,具体如下:
- PC 端:Python + OpenCV 图像识别 + Tesseract OCR 文字识别
- 连接方式:ADB 调试连接
- Android 端(Radxa ROCK 5B+):
- SD 卡(64GB):用于安装 Android 系统
- SSD 固态硬盘(250GB):用于存储游戏大数据包及频繁读写文件,以提升性能
🔧 性能优化策略:我将游戏的资源文件、贴图包等读写频繁的内容移至 SSD,以最大化运行效率。而用户数据、APK 文件等读取频率较低的数据保留在 SD 卡,兼顾性能与稳定性。
📊 架构图示意
🧱 4. 脚本平台搭建流程
🧩 4.1 烧录 Android 12 镜像至 SD 卡
-
下载 Radxa 官方推荐的烧录工具:
balenaEtcher-Setup-1.18.11.exe
-
下载 适用于 SD 卡启动的 Android 12 镜像压缩包:
Rock5BPlus-Android12-rkr14-SD-or-eMMC-20240705-gpt.zip
(解压后得到.img
镜像文件)。 -
将 SD 卡插入SD卡读卡器,并连接到 PC。
-
打开 balenaEtcher,按照界面提示选择
.img
文件和目标 SD 卡,开始烧录。 -
烧录完成后,不要格式化系统弹出的任何新磁盘分区(如 I:、K: 等)。
⚠️ 注意:部分 SD 卡烧录完成后,Windows 会弹出多个格式化提示框。这些分区是 Android 系统的必要组成部分,
若误格式化其中任何一个分区,将导致系统无法正常启动。
🔧 4.2 搭建 ADB 调试环境
-
将烧录好的 SD 卡插入 Radxa Rock 5B+。
-
将Nvme固态一段插入m.2接口另一端用螺丝固定
-
按照瑞莎官方的参考将单板和触摸屏通过排线链接
-
将单板和电源线链接,接通电源,系统将自动从 SD 卡启动进入 Android。
-
将单板用 USB Type-A 转 Type-C 数据线连接至 PC。
-
打开 PowerShell(或终端),执行:
adb devices
如果设备列表中出现 Rock 5B+ 的设备识别码(通常是一串随机的数字和字母组合),说明连接成功。
-
在 PC 上执行:
adb shell
然后尝试获取 root 权限:
su
成功后将进入 adb root 模式,可查看系统参数和配置。
-
(可选)若有串口转 USB 数据线,可以按照瑞莎官方提供的接线方式将单板链接到 PC ,使用 PuTTY 或其他串口工具连接串口并查看启动日志(参考官方设置波特率)。
💽 4.3 SSD 固态硬盘挂载
1. 确认系统识别到 SSD
使用 adb 连接至设备终端,获取 root 权限后执行以下命令:
ls /dev/block
若输出中包含 nvme0n1
,说明系统已成功识别到 NVMe 固态硬盘。
2. 检查是否已有分区表
由于官方镜像中未集成 vim
或 fdisk
,需借助 busybox
工具来查看磁盘状态:
busybox fdisk -l /dev/block/nvme0n1
若返回信息中含有:
Disk /dev/block/nvme0n1 doesn't contain a valid partition table
则说明该硬盘尚未创建分区,需要手动分区并格式化。
3. 使用 fdisk 创建分区
执行以下命令进入分区交互模式:
busybox fdisk /dev/nvme0n1
根据提示依次输入以下指令:
o ← 新建空的 DOS 分区表
n ← 新建分区
p ← 创建主分区
1 ← 分区号
[Enter] ← 默认起始位置
[Enter] ← 默认终止位置(使用整个磁盘)
w ← 写入并保存分区表
4. 格式化分区为 ext4 文件系统
执行以下命令格式化固态硬盘:
mkfs.ext4 /dev/block/nvme0n1p1
格式化成功后,在 /dev/block/
下应能看到新的分区节点 nvme0n1p1
。
5. 手动挂载 SSD 到指定目录
执行以下命令挂载固态硬盘:
mkdir -p /mnt/nvme
mount -t ext4 /dev/block/nvme0n1p1 /mnt/nvme
可通过访问 /mnt/nvme
目录检查挂载状态与文件系统结构是否正常。
说明:格式化后显示容量约为 228GB 属正常现象。厂商标称的 “250GB” 使用的是十进制(1GB = 1,000,000,000 字节),而操作系统采用二进制(1GiB = 1,073,741,824 字节)进行换算,二者存在单位差异。
🧃 4.4 部署应用(以手游《The Wolf》为例)
这里轻量手游《The Wolf》为例,记录游戏部署流程
1. 下载安装包
一般游戏安装包都是.apk
文件,可以直接使用adb install
安装
但我这里下载的是.xapk
文件,需要先进行解压处理:
- 将
.xapk
后缀修改为.zip
。 - 使用 WinRAR 或其他压缩工具解压。
解压后的文件通常包含:
- 主 APK 包(例如:
com.swiftappskom.thewolfrpg.apk
); - 配置 APK 包(例如:
config.arm64_v8a.apk
); - 其他文件如
icon.png
和manifest.json
(可忽略)。
2. 使用 ADB 安装游戏
如果是.apk
文件,直接通过adb install
安装
对于我下载的.xapk
文件,解压后通过以下命令安装:
adb install-multiple com.swiftappskom.thewolfrpg.apk config.arm64_v8a.apk
注意这条命令的参数要先跟主 APK 包,再跟配置 APK 包。
3. 理解游戏安装路径结构
安装完成后,游戏相关文件通常分布在以下几个目录:
- /data/app/:APK 安装位置;
- /data/data/:应用运行时数据(如存档、账号信息);
- /data/media/0/Android/data/:游戏下载的缓存和资源;
- /data/media/0/Android/obb/:大型资源包(如地图、贴图);
- /user_de/0/:部分延迟加载或用户解密数据。
重点:我们通常只迁移 /data/data 与 /data/media 目录的数据,因为其他目录里的文件不会影响游戏运行速度
4. 迁移游戏数据
通过以下命令将/data/data和/data/media目录的数据迁移到我们的SSD固态盘上(目标路径可以自己定):
cp -rp /data/data/com.swiftappskom.thewolfrpg /mnt/nvme/data
cp -rp /data/media/0/Android/data/com.swiftappskom.thewolfrpg /mnt/nvme/media/0/Android/data
cp -rp /data/media/0/Android/obb/com.swiftappskom.thewolfrpg /mnt/nvme/media/0/Android/obb
5. 通过 mount --bind 实现游戏数据迁移
编写 nvme_bind.sh 脚本,先挂载nvme然后再将游戏数据目录绑定到外部硬盘(如 NVMe):
#!/system/bin/sh
# mount nvme
mkdir -p /mnt/nvme
mount -t ext4 /dev/block/nvme0n1p1 /mnt/nvme
# bind game data
rm -rf /data/data/com.swiftappskom.thewolfrpg
mkdir /data/data/com.swiftappskom.thewolfrpg
mount --bind /mnt/nvme/data/com.swiftappskom.thewolfrpg /data/data/com.swiftappskom.thewolfrpg
rm -rf /data/media/0/Android/data/com.swiftappskom.thewolfrpg
mkdir /data/media/0/Android/data/com.swiftappskom.thewolfrpg
mount --bind /mnt/nvme/media/0/Android/data/com.swiftappskom.thewolfrpg /data/media/0/Android/data/com.swiftappskom.thewolfrpg
rm -rf /data/media/0/Android/obb/com.swiftappskom.thewolfrpg
mkdir /data/media/0/Android/obb/com.swiftappskom.thewolfrpg
mount --bind /mnt/nvme/media/0/Android/obb/com.swiftappskom.thewolfrpg /data/media/0/Android/obb/com.swiftappskom.thewolfrpg
将脚本保存到 /data/local/nvme_bind.sh 并赋予执行权限:
chmod +x /data/local/nvme_bind.sh
每次开机后手动运行此脚本,以挂载游戏数据目录。之后就可以正常运行游戏。
⚠️ 注意:这里不能直接挂载整个/data/media目录,否则会导致挂载点被覆盖,游戏找不到相应数据包的路径
🌼 4.5 安卓环境优化
1. 设置系统语言为中文
打开系统设置 → 【System】(系统)→ 【Languages & input】(语言和输入法)→【Languages】(语言)。
点击【Add a language】,选择中文(简体)。
添加后,长按“中文(简体)”并拖动至列表最上方,使其成为系统默认语言。
2. 设置系统时间与时区
进入系统设置 → 系统 → 日期与时间
点掉自动设置时区
点击选择时区,点击区域选择“中国”,点开时区选择“上海”
3. 更换 Launcher(启动器)
默认启动器功能简陋,建议替换为功能完整的 Nora Launcher。
在 PC 端下载 Nora Launcher APK,并使用 ADB 安装:
adb install NoraLauncher.apk
安装完成后,进入系统设置 → 应用 → 默认应用 → 主屏幕应用,将其更改为 “Nora Launcher”。
启动 Nora Launcher 后,从屏幕右侧的边栏(或底部应用抽屉)打开应用列表,长按应用图标,即可将其拖动至主屏幕以创建快捷方式。
👓 4.6 脚本编写
1. 手动触摸屏检查
在设备上用手进行点按、滑动等操作,同时在 PC 端执行如下命令实时查看输入事件:
adb shell getevent -lt
若能观察到 /dev/input/event*
的时间戳与触摸事件,说明触摸屏事件系统正常工作。
2. 使用 ADB 命令模拟输入操作
常用 ADB 输入命令:
- 点按指定坐标:
adb shell input tap <x> <y>
- 滑动操作:
adb shell input swipe <x1> <y1> <x2> <y2> <duration_ms>
- 输入文本:
adb shell input text "hello"
- 返回键 / Home 键等控制:
adb shell input keyevent KEYCODE_BACK
、adb shell input keyevent KEYCODE_HOME
若执行后屏幕有对应操作反馈,说明 ADB 控制功能正常。
3. 编写Python工具包
在确认触控和adb调试命令都没有问题以后,就可以开始Python工具包的编写。
因为不是什么很复杂的架构,所以就不写继承类特性类之类的py文件了。
先编写 adb_debug.py(英文注释防止乱码):
import subprocess
import re
import sys
import os
import datetime
import argparse
import adb_util
# Maximum touch coordinates (based on `getevent -p`)
touch_x_max = 800
touch_y_max = 1280
def get_screen_size():
"""
Get the physical screen resolution of the connected Android device.
Returns:
tuple[int, int]: Width and height of the screen in pixels.
"""
result = subprocess.check_output(['adb', 'shell', 'wm', 'size']).decode()
for line in result.splitlines():
if 'Physical size:' in line:
res = line.split(':')[1].strip()
width, height = map(int, res.split('x'))
return width, height
raise RuntimeError("Unable to get screen resolution")
def to_physical(x, y):
"""
Convert touch coordinates from getevent to physical screen coordinates.
Args:
x (int): Raw touch X coordinate.
y (int): Raw touch Y coordinate.
Returns:
tuple[int, int]: Mapped screen coordinates.
"""
screen_w, screen_h = get_screen_size()
physical_x = int(x / touch_x_max * screen_w)
physical_y = int(y / touch_y_max * screen_h)
return physical_x, physical_y
def start_touch_probe():
"""
Listen to raw touchscreen events and print translated screen coordinates.
"""
pattern = re.compile(r"ABS_MT_POSITION_(X|Y)\s+([0-9a-f]+)")
proc = subprocess.Popen(
["adb", "shell", "getevent", "-lt", "/dev/input/event1"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True
)
print("Listening for touch coordinates... (Ctrl+C to stop)")
current_x = None
current_y = None
try:
for line in proc.stdout:
match = pattern.search(line)
if match:
axis = match.group(1)
hex_value = match.group(2)
dec_value = int(hex_value, 16)
if axis == "X":
current_x = dec_value
elif axis == "Y":
current_y = dec_value
if current_x is not None and current_y is not None:
phys_x, phys_y = to_physical(current_x, current_y)
print(f"X={phys_x}, Y={phys_y}")
except KeyboardInterrupt:
print("Stopped.")
proc.terminate()
def take_screenshot(save_dir="."):
"""
Capture the device screen and save the image to the local machine.
Args:
save_dir (str): Local directory to store the screenshot (default: current directory).
"""
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
local_filename = f"screenshot_{timestamp}.png"
local_path = os.path.join(save_dir, local_filename)
try:
subprocess.run(["adb", "shell", "screencap", "-p", "/sdcard/screen.png"], check=True)
subprocess.run(["adb", "pull", "/sdcard/screen.png", local_path], check=True)
subprocess.run(["adb", "shell", "rm", "/sdcard/screen.png"], check=True)
print(f"Screenshot saved to: {local_path}")
except subprocess.CalledProcessError as e:
print(f"Error taking screenshot: {e}")
def get_foreground_app(target_package=None):
"""
List all current window activities or filter by a specific package name.
"""
try:
output = subprocess.check_output(
["adb", "shell", "dumpsys", "window", "windows"],
universal_newlines=True
)
# Match every style which like Window{... com.package.name/com.activity.Name}
pattern = re.compile(r"Window\{[^\}]*?\s+(\S+)/(\S+)}")
found = set()
for match in pattern.finditer(output):
pkg, act = match.groups()
if (target_package is None) or (pkg == target_package):
entry = f"{pkg}/{act}"
if entry not in found:
found.add(entry)
if found:
print("Found activity windows:")
for entry in sorted(found):
print(f" {entry}")
else:
print("No matching activity found.")
except subprocess.CalledProcessError as e:
print(f"Failed to execute adb command: {e}")
def screen_freeze():
adb_util.adb_keyevent("power")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="ADB Debug Tools")
parser.add_argument(
"mode",
choices=["touch", "shot", "fore", "freeze"],
help=(
"Select the operation mode: touch, shot, fore or freeze\n"
)
)
args = parser.parse_args()
if args.mode == "touch":
start_touch_probe()
elif args.mode == "shot":
take_screenshot()
elif args.mode == "fore":
get_foreground_app()
elif args.mode == "freeze":
screen_freeze()
说明:
get_screen_size: 用于获取屏幕真实分辨率的函数
to_physical: 用于点击坐标转换的函数
start_touch_probe: 用于开启监听记录屏幕点按坐标的函数
take_screenshot: 用于游戏截图并保存到PC端的函数
get_foreground_app: 用于获取游戏包名和活动名的函数,配合adb_util.py中的start_app可以做到利用脚本打开游戏
screen_freeze: 用于锁频的函数
再编写 adb_util.py(英文注释防止乱码):
import subprocess
import time
import cv2
import numpy as np
touch_x_max = 1920
touch_y_max = 1080
touch_y_min_valid = 350
touch_y_max_valid = 730
def adb_delay(seconds):
"""
Wait for a given number of seconds.
Args:
seconds (float): Number of seconds to sleep.
"""
print(f"Sleeping for {seconds} seconds...")
time.sleep(seconds)
def adb_tap_vertical(x, y):
"""
Simulate a tap on screen coordinates for portrait mode.
Converts portrait (x, y) to landscape raw input.
Args:
x (int): X coordinate in portrait mode (0-1080).
y (int): Y coordinate in portrait mode (0-1920).
"""
print(f"Tapping at: ({x}, {y})")
phys_x = int((x / 1920) * 1080)
phys_y = int((y / 1080) * 1920)
subprocess.run(['adb', 'shell', 'input', 'tap', str(phys_x), str(phys_y)])
def adb_tap(x, y):
"""
Simulate a tap on screen coordinates.
Args:
x (int): X coordinate.
y (int): Y coordinate (mapped to a valid screen range).
"""
print(f"Tapping at: ({x}, {y})")
y_ratio = (y - touch_y_min_valid) / (touch_y_max_valid - touch_y_min_valid)
mapped_y = int(y_ratio * touch_y_max)
subprocess.run(['adb', 'shell', 'input', 'tap', str(x), str(mapped_y)])
def adb_keyevent(key_name):
"""
Send a key event to the device.
Args:
key_name (str): One of the supported key names like 'home', 'back', etc.
"""
key_map = {
"home": "KEYCODE_HOME",
"back": "KEYCODE_BACK",
"recent": "KEYCODE_APP_SWITCH",
"power": "KEYCODE_POWER",
"volume_up": "KEYCODE_VOLUME_UP",
"volume_down": "KEYCODE_VOLUME_DOWN"
}
if key_name not in key_map:
raise ValueError(f"Unknown key name: {key_name}")
subprocess.run(['adb', 'shell', 'input', 'keyevent', key_map[key_name]])
def tap_sequence_vertical(coord_list, delay=3):
"""
Perform a sequence of taps with delay in between.
Args:
coord_list (list of tuple): List of (x, y) coordinates.
delay (float): Delay in seconds between each tap.
"""
for x, y in coord_list:
adb_tap_vertical(x, y)
adb_delay(delay)
def tap_sequence(coord_list, delay=3):
"""
Perform a sequence of taps with delay in between.
Args:
coord_list (list of tuple): List of (x, y) coordinates.
delay (float): Delay in seconds between each tap.
"""
for x, y in coord_list:
adb_tap(x, y)
adb_delay(delay)
def start_app(package_name, activity_name):
"""
Start an Android app using ADB with am start.
Args:
package_name (str): Package name of the app.
activity_name (str): Full activity name (e.g., .MainActivity or com.package/.MainActivity).
"""
cmd = ['adb', 'shell', 'am', 'start', '-n', f'{package_name}/{activity_name}']
try:
result = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print("Output:\n", result.stdout)
except subprocess.CalledProcessError as e:
print("Error:\n", e.stderr)
def force_stop(package_name):
"""
Force-stop the given Android app via ADB.
Args:
package_name (str): The package name of the app to stop.
"""
print(f"Force-stopping app: {package_name}")
subprocess.run(['adb', 'shell', 'am', 'force-stop', package_name])
def adb_swipe_vertical(start_x, start_y, end_x, end_y, duration_ms=300):
"""
Perform a swipe gesture from start point to end point, mapping Y values.
Args:
start_x (int): Start X coordinate.
start_y (int): Start Y coordinate (to be mapped).
end_x (int): End X coordinate.
end_y (int): End Y coordinate (to be mapped).
duration_ms (int): Duration of the swipe in milliseconds.
"""
mapped_start_x = int((start_x / 1920) * 1080)
mapped_start_y = int((start_y / 1080) * 1920)
mapped_end_x = int((end_x / 1920) * 1080)
mapped_end_y = int((end_y / 1080) * 1920)
print(f"Swiping from ({mapped_start_x}, {mapped_start_y}) to ({mapped_end_x}, {mapped_end_y})")
subprocess.run([
'adb', 'shell', 'input', 'swipe',
str(mapped_start_x), str(mapped_start_y),
str(mapped_end_x), str(mapped_end_y),
str(duration_ms)
])
adb_delay(3)
def adb_swipe(start_x, start_y, end_x, end_y, duration_ms=300):
"""
Perform a swipe gesture from start point to end point, mapping Y values.
Args:
start_x (int): Start X coordinate.
start_y (int): Start Y coordinate (to be mapped).
end_x (int): End X coordinate.
end_y (int): End Y coordinate (to be mapped).
duration_ms (int): Duration of the swipe in milliseconds.
"""
start_y_ratio = (start_y - touch_y_min_valid) / (touch_y_max_valid - touch_y_min_valid)
end_y_ratio = (end_y - touch_y_min_valid) / (touch_y_max_valid - touch_y_min_valid)
mapped_start_y = int(start_y_ratio * touch_y_max)
mapped_end_y = int(end_y_ratio * touch_y_max)
print(f"Swiping from ({start_x}, {mapped_start_y}) to ({end_x}, {mapped_end_y})")
subprocess.run([
'adb', 'shell', 'input', 'swipe',
str(start_x), str(mapped_start_y),
str(end_x), str(mapped_end_y),
str(duration_ms)
])
adb_delay(3)
def adb_input_text(text):
"""
Input text on the connected Android device.
Limitations:
- Cannot contain: " or '
- Spaces will be replaced with '%s'
- &, |, (, ) will be escaped to prevent shell errors
Args:
text (str): Text string to input.
"""
print(f"Inputting text: {text}")
# Escape problematic characters for ADB shell input
safe_text = text.replace(' ', '%s').replace('&', '\&').replace('|', '\|').replace('(', '\(').replace(')', '\)')
subprocess.run(['adb', 'shell', 'input', 'text', safe_text])
def match_template_on_screen(template_path, threshold=0.94):
"""
Capture the device screen and check if a template image exists on it.
Args:
template_path (str): Path to the local template image file (e.g., PNG).
threshold (float): Match threshold between 0 and 1. Default is 0.95.
Returns:
bool: True if template is found with sufficient match, False otherwise.
"""
print("Capturing screen...")
try:
# Get screenshot as bytes
result = subprocess.run(['adb', 'exec-out', 'screencap', '-p'], stdout=subprocess.PIPE, check=True)
screenshot_bytes = result.stdout
# Convert bytes to numpy array
image_array = np.frombuffer(screenshot_bytes, np.uint8)
screenshot = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
template = cv2.imread(template_path, cv2.IMREAD_COLOR)
if template is None:
raise FileNotFoundError(f"Template image not found: {template_path}")
# Perform template matching
res = cv2.matchTemplate(screenshot, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
print(f"Match value: {max_val:.4f}")
return max_val >= threshold
except Exception as e:
print(f"Template matching failed: {e}")
return False
说明:
adb_delay: 加了打印信息的延时函数
adb_tap_vertical: 用于竖屏游戏的点按函数,配合adb_debug.py的start_touch_probe使用,映射关系从start_touch_probe到adb_tap_vertical为X:(0-1920)->(0-1080), y:(0-1080)->(0-1920)
adb_tap: 用于大多数横屏游戏的点按函数,配合adb_debug.py的start_touch_probe使用,映射关系从start_touch_probe到adb_tap_vertical为X:(0-1920)->(0-1920), y:(350-710)->(0-1080)
adb_keyevent: 用来调用返回键,锁屏键等系统按键函数
tap_sequence_vertical: 用于竖屏游戏连续点击函数
tap_sequence: 用于大多数横屏游戏连续点击函数
start_app: 用来打开游戏的函数
force_stop: 用于强制停止游戏的函数
adb_swipe_vertical: 用于竖屏游戏滑动操作的函数
adb_swipe: 用于大多数横屏游戏滑动操作的函数
adb_input_text: 用于切换账号时输入文本的函数
match_template_on_screen: 用于对比游戏界面和截图的函数
4. 编写python脚本
有了之前的两个工具包之后,就能够编写python脚本了。
但在这之前需要先搭建工程的目录结构,毕竟所有文件堆在一起很难维护:
game
├── __pycache__
│ └── adb_util.cpython-310.pyc
├── adb_debug.py
├── adb_util.py
└── wolf
├── reward.png
└── wolf.py
然后再一边调试一边编写脚本,这里拿一个最简单的《The Wolf》登录领取奖励的脚本为例:
import subprocess
import argparse
import inspect
import os
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(current_dir, ".."))
import adb_util
package_name="com.swiftappskom.thewolfrpg"
activity_name="com.google.firebase.MessagingUnityPlayerActivity"
def start_game():
# start game and wait for loading
adb_util.start_app(package_name, activity_name)
adb_util.adb_delay(35)
adb_util.adb_tap(998,701)
adb_util.adb_delay(7)
def end_game():
# end game
adb_util.force_stop(package_name)
adb_util.adb_delay(5)
def check_reward():
# check if daily reward is appear
img_path = os.path.join(current_dir, "reward.png")
res= adb_util.match_template_on_screen(img_path)
if res:
adb_util.adb_tap(1564, 387)
adb_util.adb_delay(3)
adb_util.adb_tap(1276, 426)
adb_util.adb_delay(3)
adb_util.adb_tap(420, 648)
adb_util.adb_delay(3)
adb_util.adb_tap(986, 675)
adb_util.adb_delay(25)
adb_util.adb_tap(1825, 373)
adb_util.adb_delay(3)
adb_util.adb_tap(1825, 373)
adb_util.adb_delay(5)
else:
adb_util.adb_delay(5)
def do_daily():
start_game()
check_reward()
end_game()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Automatically game task runner")
parser.add_argument(
"mode",
choices=["daily"],
help="choose task mode:daily"
)
args = parser.parse_args()
if args.mode == "daily":
do_daily()
start_game和check_reward中的延时是通过自己的游戏经验判断网络可能造成的干扰,估算出来的,需要后期不断的调试来达到最佳值
start_game和check_reward中的点按坐标是根据adb_debug.py工具包的start_touch_probe获取的
start_game和end_game中的游戏包名和应用名是根据adb_debug.py工具包的get_foreground_app获取的
脚本可以通过python命令直接运行:
python3 wolf.py daily
⚠️ 注意:要成功运行python脚本进行自动化控制需要确保 PC 中有安装了 OpenCV 插件的 Python 环境
⚠️ 5. 注意事项
🔁 绑定顺序限制
Android 系统在安装应用时不会读取提前绑定的目录。如果你在执行 adb install
前已通过 mount --bind
映射路径,系统会中断绑定并将 APK 安装到默认目录,导致绑定失效。因此,在迁移应用数据的时候必须遵循以下顺序:
- 使用系统原生目录完成应用安装;
- 安装完成后再使用
mount --bind
映射到外部存储路径。
尽管这样的部署流程十分繁琐,但目前没有别的办法。若能在 Android 镜像 源码中修改应用安装的默认路径,将更好的处理这个问题。
🧠 游戏设备识别异常
某些游戏(如《The Wolf》)在运行时会将 Radxa Rock 5B+ 识别为模拟器,并强制账号进入“模拟器服”。这是由于开发板缺乏标准手机硬件标识(如 IMEI、基带信息等)所致。
要避免这种识别行为,必须修改 Android 镜像源码 ,为设备添加完整的硬件信息模拟。
🚫 无法获取 Root 权限
我尝试使用 Magisk 替换 boot.img
获取 Root 权限,但最后失败,导致系统无法正常启动。个人认为问题主要在于:
- Magisk 无法识别开发板提供的 bootloader 文件格式;
- 系统启动引导失败,需重新刷写镜像恢复。
这说明当前设备并不完全兼容 Magisk 的常规 Root 方案。
⚙️ 自定义开机脚本无法运行
经过我测试,/system, /etc, /vendor 这三个系统文件夹默认全是只读形式,即使是adb root也无法修改。
这直接导致了自定义开机脚本无法运行。
虽然可以利用adb root重新挂载vendor为可读写,但是依然存在种种限制,只能运行功能有限的脚本(比如挂载nvme)
只有修改 Android 镜像源码 重新编译后在烧写镜像才能实现自定义脚本开机运行的功能
📵 无法安装谷歌服务
我尝试以下多种方式安装 Google 服务均失败:
- 使用 Go 安装器;
- 手动提取并安装 Open GApps 中的 APK 包;
初步判断为镜像未提供必要依赖或权限配置,可能需要重新编译 Android 镜像源码 以支持 Google 框架
🤖 自动化全局脚本无法内部运行
我在尝试部署自动化控制脚本时发现,仅能通过 PC 端 adb root
的方式运行全局脚本,而无法在单板机内部独立运行全局脚本。
个人认为主要原因是 Android 的沙箱机制限制了应用之间的交互和全局文件访问权限;
因此,暂时只能使用外部主机通过 adb
接口远程调度任务。
若要在本地独立执行自动化脚本,需在 Android 镜像源码 中构建专属执行环境,并赋予必要的系统权限。
💻 触摸屏显示异常
Radxa 的单板只能适配 Radxa 提供的触摸屏,但是 Radxa 的触摸屏连接装了 Radxa 官方安卓镜像的 Radxa Rock 5B+ 单板后会出现显示异常,横屏只能显示1/3个屏幕。
可以通过 设置->显示->屏幕旋转->90 的方法转换成竖屏全屏显示,但打开横屏游戏后又会强制变回1/3个屏幕。
Radxa 提供的设置命令 adb shell setprop persist.sys.rotation.einit-1 1
也无法实现横屏全屏显示
在和 Radxa 客服进行多轮沟通后,基本上确定 Radxa 的技术人员把这个横屏只显示1/3个屏幕的 BUG 写死在了 Android 镜像源码 中,需要修改镜像源码才能实现横屏的全屏显示。另外这个BUG还搞得实际坐标和相对坐标很乱,XY坐标都是反的,导致脚本非常不好写,我直到现在还没搞清楚这个坐标转换的规律
💔 存储空间异常无法打开应用
有的游戏在完成数据迁移到SSD固态以后再次打开会出现 Error: Not enough storage space to install required resources 的报错。
如果出现这种情况,需检查是否绑定了/data/media
或者/data/data
整个目录。
如果绑定了整个目录,会导致覆盖系统原始挂载点,所以需要把挂载点精确到每个数据包,比如按照/data/media/0/Android/data/com.nhnent.SKQUEST
这样一个一个数据包来挂载
💥 登录即闪退
经过我测试有个别游戏在完成mount --bind后打开登录即闪退,暂时不清楚是什么原因,估计是应用的代码里添加了对目录的特殊校验。
经过我进一步测试发现这些游戏只要不去mount --bind /data/data
里面的数据包就不会触发校验,其他的目录里的数据可以正常迁移。/data/data
里面的数据只能暂时让他在SD卡上运行了(暂时没找到什么好的解决方法)
🧩 6. 后记
我之前一直接触的都是 Linux 系统,这是我第一次接触 Android 系统,给我的整体感觉就是两个字“高贵”。
本来以为开发板一定会有系统的最高权限,系统的所有文件都能修改,但是 Android 12 的系统就是特别“高贵”。
什么都不能改,什么都不能动,不仅是系统文件不能改,哪怕是放应用文件的 /data
目录,也会被沙箱隔离。
据说谷歌这么做是为了安全,很好,你往 Android 普通版里放这些可以理解,但你往 Android 开发调试版里塞这些 🐔🎱 玩意这不是欠揍吗?
我前后调试了半个月,越调越💢,越改越😡。不得不承认 Android 12 系统很好的锻炼了我的心态。
还有那个模拟器识别,《The Wolf》还算好了还让你进模拟器服,像《Nikke》和《Action Taimanin》识别到模拟器直接不让进游戏。
人家花2000多买个高配平板,上手直接用,我花2000多买个组装开发板,组装完还被识别成模拟器,我可真 🌿🍋🐴🌩️
最后还有那个屏幕的坐标映射也搞得我很💥🛡️,他要是全部应用都是1/3屏幕显示还好,他竖屏应用能全屏显示,横屏应用1/3屏显示,而且全屏显示和1/3屏显示用的是两套不同的映射方案。而且这两个映射方案都非常奇葩。竖屏的映射方案是把屏幕窄的那条边映射到1920像素,长的那条边映射到1080像素,XY轴直接反转;横屏的映射方案是在竖屏的基础上,Y轴映射再以中心为原点,“缩水”1/3(从0-1080缩到350-710),我是测的差点就摔板子了,真的是大💥特💥了。
就算是在我的极力挽救下,也只是成功让这块开发板把 Android 12 系统运行起来,能通过adb链接PC来跑Python自动化脚本任务而已,写在开头的预期的目标几乎全军覆没。
回顾第五节总结的问题,大多数都和镜像源码有关。Android 12 (也可能是 Radxa 的技术人员)把太多的权限都写死在镜像源码中了,如果不能修改镜像源码,那就无法拥有 Android 12 系统的所有权限。
但是镜像源码修改了以后还得编译,而谷歌官方给出的编译环境最低条件就是16GB内存+250GB空间的 Ubuntu20.04 版本,我的PC完全满足不了这个要求(谁 🗼🐴 PC用Ubuntu系统?还16G内存?)。
不过这时我恰巧发现,我的 Rock 5B+ 不就是16GB内存+256GB空间的吗?于是我有了一个大胆的想法:如果我再买一张SD卡,烧写Ubuntu的镜像,放到 Rock 5B+ 里让他跑 Ubuntu 系统,那 Rock 5B+ 不就是一台小型编译服务器了吗?(开头的第二张SD卡就是这么来的)
而且 Rock 5B+ 自己编译出来的镜像给自己用,连交叉编译都省下了。
所以接下来我计划安装 Rock 5B+ 的 Linux 系统,在 Linux 系统中搭建安卓镜像的编译环境, 修改并编译瑞莎官方提供的 Android 镜像源码,最后将镜像装回 Rock 5B+ 进行调试
如果成功了会另开一篇博客来记录,失败了大家就当无事发生吧。
📚 参考资料
更多推荐
所有评论(0)