mower-ng/mower/utils/device/method/scrcpy/core.py
2024-12-01 14:50:39 +08:00

314 lines
10 KiB
Python

from __future__ import annotations
import functools
import socket
import threading
import time
from functools import wraps
from typing import Any, Callable, Optional
import numpy as np
from adbutils import AdbConnection, Network
from mower.utils import config
from mower.utils import typealias as tp
from mower.utils.csleep import MowerExit
from mower.utils.device.exception import ADBError, SimulatorError
from mower.utils.device.method.adb import ADB
from mower.utils.device.method.adb.const import KeyCode
from mower.utils.log import logger
from mower.utils.path import get_path
from mower.utils.vector import sm, va
from . import const
from .control import ControlSender
SCR_PATH = "/data/local/tmp/scrcpy-server.jar"
SCR_VER = "3.0"
swipe_update_result = None
def recog_start(func: Callable[[tp.Image], Any] = lambda _: None):
time.sleep(0.1)
config.recog.update()
global swipe_update_result
swipe_update_result = func(config.recog.img)
def retry_scrcpy(func):
@wraps(func)
def retry_wrapper(self, *args, **kwargs):
for try_count in range(2):
try:
return func(self, *args, **kwargs)
except MowerExit:
raise
except (
ConnectionResetError,
BrokenPipeError,
ConnectionAbortedError,
) as e:
logger.exception(e)
self.stop()
time.sleep(1)
self.start()
try_count += 1
# After scrcpy retries fail, escalate to adb error
raise ADBError("Scrcpy failed after 2 attempts.")
return retry_wrapper
class Client:
def __init__(
self,
adb: ADB,
max_width: int = 0,
bitrate: int = 8000000,
max_fps: int = 0,
flip: bool = False,
block_frame: bool = False,
stay_awake: bool = False,
lock_screen_orientation: int = const.LOCK_SCREEN_ORIENTATION_UNLOCKED,
displayid: Optional[int] = None,
connection_timeout: int = 3000,
):
"""
Create a scrcpy client, this client won't be started until you call the start function
Args:
client: ADB client
max_width: frame width that will be broadcast from android server
bitrate: bitrate
max_fps: maximum fps, 0 means not limited (supported after android 10)
flip: flip the video
block_frame: only return nonempty frames, may block cv2 render thread
stay_awake: keep Android device awake
lock_screen_orientation: lock screen orientation, LOCK_SCREEN_ORIENTATION_*
connection_timeout: timeout for connection, unit is ms
"""
# User accessible
self.adb = adb
self.last_frame: Optional[np.ndarray] = None
self.resolution = (1920, 1080)
self.device_name: Optional[str] = None
self.control = ControlSender(self)
# Params
self.flip = flip
self.max_width = max_width
self.bitrate = bitrate
self.max_fps = max_fps
self.block_frame = block_frame
self.stay_awake = stay_awake
self.lock_screen_orientation = lock_screen_orientation
self.connection_timeout = connection_timeout
self.displayid = displayid
# Need to destroy
self.__server_stream: Optional[AdbConnection] = None
self.control_socket: Optional[socket.socket] = None
self.control_socket_lock = threading.Lock()
self.start()
def __del__(self) -> None:
self.stop()
def __start_server(self) -> None:
"""
Start server and get the connection
"""
cmdline = f"CLASSPATH={SCR_PATH} app_process / com.genymobile.scrcpy.Server {SCR_VER} video=false audio=false control=true tunnel_forward=true"
if self.displayid is not None:
cmdline += f" display_id={self.displayid}"
self.__server_stream: AdbConnection = self.adb.adb_shell(cmdline, True)
# Wait for server to start
self.__server_stream.conn.settimeout(3)
logger.info("Create server stream")
response = self.__server_stream.read(10)
logger.debug(response)
if b"[server]" not in response:
raise ConnectionError(
"Failed to start scrcpy-server: " + response.decode("utf-8", "ignore")
)
def __deploy_server(self) -> None:
"""
Deploy server to android device
"""
server_file_path = get_path(f"@install/mower/vendor/scrcpy-server-v{SCR_VER}")
self.adb.adb_push(str(server_file_path), SCR_PATH)
self.__start_server()
def __init_server_connection(self) -> None:
try:
self.control_socket = self.adb.adb.create_connection(
Network.LOCAL_ABSTRACT, "scrcpy"
)
self.control_socket.settimeout(3)
except socket.timeout:
raise ConnectionError("Failed to connect scrcpy-server")
dummy_byte = self.control_socket.recv(1)
if not len(dummy_byte) or dummy_byte != b"\x00":
raise ConnectionError("Did not receive Dummy Byte!")
self.device_name = self.control_socket.recv(64).decode("utf-8")
self.device_name = self.device_name.rstrip("\x00")
if not len(self.device_name):
raise ConnectionError("Did not receive Device Name!")
logger.info(f"scrcpy连接设备:{self.device_name}")
def start(self) -> None:
"""
Start listening video stream
"""
try_count = 0
while try_count < 3:
try:
self.__deploy_server()
time.sleep(0.5)
self.__init_server_connection()
break
except ConnectionError as e:
logger.exception(f"Failed to connect scrcpy-server: {e}")
self.stop()
logger.warning("Try again in 10 seconds...")
time.sleep(10)
try_count += 1
else:
raise RuntimeError("Failed to connect scrcpy-server.")
def stop(self) -> None:
"""
Stop listening (both threaded and blocked)
"""
if self.__server_stream is not None:
self.__server_stream.close()
self.__server_stream = None
if self.control_socket is not None:
self.control_socket.close()
self.control_socket = None
def stable(f):
@functools.wraps(f)
def inner(self: Client, *args, **kwargs):
try_count = 0
while try_count < 3:
try:
return f(self, *args, **kwargs)
except (
ConnectionResetError,
BrokenPipeError,
ConnectionAbortedError,
) as e:
logger.debug_exception(e)
self.stop()
time.sleep(1)
self.start()
try_count += 1
except SimulatorError as e:
logger.exception(e)
else:
raise ADBError("Failed to start scrcpy-server.")
return inner
@retry_scrcpy
def tap(self, x: int, y: int) -> None:
self.control.tap(x, y)
@retry_scrcpy
def back(self):
self.control.send_keyevent(KeyCode.KEYCODE_BACK)
@retry_scrcpy
def swipe(
self,
x0: int,
y0: int,
x1: int,
y1: int,
duration: float = 1,
fall: bool = True,
lift: bool = True,
update: bool = False,
interval: float = 0,
func: Callable[[tp.Image], Any] = lambda _: None,
):
"""滑动
Args:
x0 (int): 起点横坐标
y0 (int): 起点纵坐标
x1 (int): 终点横坐标
y1 (int): 终点纵坐标
duration (float, optional): 拖动时长. Defaults to 1.
fall (bool, optional): 按下. Defaults to True.
lift (bool, optional): 抬起. Defaults to True.
update (bool, optional): 滑动前截图. Defaults to False.
interval (float, optional): 拖动后的等待时间. Defaults to 0.
func (Callable[[tp.Image], Any], optional): 处理截图的函数. Defaults to lambda _: None.
"""
frame_time = 1 / 30
start_time = time.perf_counter()
fall and self.control.touch(x0, y0, const.ACTION_DOWN)
if update and config.recog:
thread = threading.Thread(target=recog_start, args=(func,))
thread.start()
if (down_time := time.perf_counter()) < start_time + frame_time:
time.sleep(start_time + frame_time - down_time)
down_time = start_time + frame_time
while (progress := (time.perf_counter() - down_time) / duration) <= 1:
x, y = va(sm(1 - progress, (x0, y0)), sm(progress, (x1, y1)))
start_time = time.perf_counter()
self.control.touch(x, y, const.ACTION_MOVE)
if (move_time := time.perf_counter() - start_time) < frame_time:
time.sleep(frame_time - move_time)
self.control.touch(x1, y1, const.ACTION_MOVE)
lift and self.control.touch(x1, y1, const.ACTION_UP)
if update and config.recog:
start_time = time.perf_counter()
thread.join()
if (stop_time := time.perf_counter()) < start_time + interval:
time.sleep(start_time + interval - stop_time)
return swipe_update_result
if interval > 0:
time.sleep(interval)
@retry_scrcpy
def swipe_ext(
self,
points: list[tp.Coordinate],
durations: list[int],
update: bool = False,
interval: float = 0,
func: Callable[[tp.Image], Any] = lambda _: None,
):
total = len(durations)
for idx, (S, E, D) in enumerate(zip(points[:-1], points[1:], durations)):
first = idx == 0
last = idx == total - 1
result = self.swipe(
x0=S[0],
y0=S[1],
x1=E[0],
y1=E[1],
duration=D / 1000,
fall=first,
lift=last,
update=last and update,
interval=interval if last else 0,
func=func,
)
return result