autoxkit 2.4.6__cp313-cp313-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
autoxkit/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .constants import Hex_Key_Code, Hex_Mouse_Code, Hex_Hook_Code
2
+
3
+ __all__ = ["Hex_Key_Code", "Hex_Mouse_Code", "Hex_Hook_Code"]
@@ -0,0 +1,208 @@
1
+ """Android 设备控制模块。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import threading
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .client import ScrcpyClient, ScrcpyOptions
12
+ from .adb import AdbServerLauncher
13
+ from .models import AudioVideoEvent, StreamKind
14
+
15
+ __all__ = [
16
+ "AndroidDevice",
17
+ "ScrcpyClient",
18
+ "ScrcpyOptions",
19
+ "AdbServerLauncher",
20
+ "AudioVideoEvent",
21
+ "StreamKind",
22
+ ]
23
+
24
+
25
+ class AndroidDevice:
26
+ """同步 Android 设备控制器。
27
+
28
+ 将异步的 ``ScrcpyClient`` 封装在后台线程中,使所有公共方法都成为
29
+ 普通的同步调用,适合在钩子回调中使用(``key_down``、``key_up`` 等)。
30
+
31
+ 用法::
32
+
33
+ from autoxkit.android import AndroidDevice
34
+
35
+ device = AndroidDevice()
36
+ device.connect("192.168.1.23:5555")
37
+
38
+ device.tap(500, 300)
39
+ device.touch(500, 300, "down")
40
+ device.keycode(4, "up")
41
+ device.disconnect()
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ adb_path: str | Path = Path("scrcpy-win64-v4.0") / "adb.exe",
47
+ server_path: str | Path = Path("scrcpy-win64-v4.0") / "scrcpy-server",
48
+ ) -> None:
49
+ self._adb_path = Path(adb_path)
50
+ self._server_path = Path(server_path)
51
+ self._loop = asyncio.new_event_loop()
52
+ self._thread = threading.Thread(target=self._run_loop, daemon=True)
53
+ self._thread.start()
54
+ self._client: ScrcpyClient | None = None
55
+ self._session_size: tuple[int, int] | None = None
56
+
57
+ # ── 连接管理 ────────────────────────────────
58
+
59
+ def connect(
60
+ self,
61
+ serial: str | None = None,
62
+ *,
63
+ video: bool = True,
64
+ audio: bool = True,
65
+ control: bool = True,
66
+ **kwargs: Any,
67
+ ) -> None:
68
+ """通过 scrcpy-server 连接到 Android 设备。
69
+
70
+ 参数将传递给 :class:`ScrcpyOptions`。常用额外参数:
71
+
72
+ - ``tunnel_forward`` (bool): 使用 ``adb forward`` 而不是
73
+ ``adb reverse``。
74
+ - ``tcpip`` (bool): 启动前启用 ADB over TCP/IP。
75
+ - ``tcpip_dst`` (str): 已知的无线 ADB 地址。
76
+ """
77
+ self._submit(
78
+ self._connect_async(serial, video=video, audio=audio, control=control, **kwargs)
79
+ )
80
+
81
+ def disconnect(self) -> None:
82
+ """断开与设备的连接并停止 scrcpy-server。"""
83
+ self._submit(self._disconnect_async())
84
+
85
+ @property
86
+ def connected(self) -> bool:
87
+ return self._client is not None
88
+
89
+ # ── 触摸 ────────────────────────────────────────────────
90
+
91
+ def touch(self, x: int, y: int, action: str) -> None:
92
+ """发送触摸事件。
93
+
94
+ Args:
95
+ x: 屏幕 X 坐标。
96
+ y: 屏幕 Y 坐标。
97
+ action: ``"down"``、``"up"`` 或 ``"move"``。
98
+ """
99
+ action_code = {"down": 0, "up": 1, "move": 2}.get(action)
100
+ if action_code is None:
101
+ raise ValueError(f"无效的触摸动作:{action!r}(请使用 down/up/move)")
102
+ self._submit(self._touch_async(action_code, x, y))
103
+
104
+ def tap(self, x: int, y: int, delay: float = 0.05) -> None:
105
+ """在屏幕坐标上执行点击(触摸按下然后抬起)。"""
106
+ self.touch(x, y, "down")
107
+ time.sleep(delay)
108
+ self.touch(x, y, "up")
109
+
110
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, steps: int = 10) -> None:
111
+ """执行从 (x1, y1) 到 (x2, y2) 的滑动手势。"""
112
+ self.touch(x1, y1, "down")
113
+ for i in range(1, steps + 1):
114
+ t = i / steps
115
+ cx = int(x1 + (x2 - x1) * t)
116
+ cy = int(y1 + (y2 - y1) * t)
117
+ self.touch(cx, cy, "move")
118
+ self.touch(x2, y2, "up")
119
+
120
+ # ── 按键码 ──────────────────────────────────────────────
121
+
122
+ def keycode(self, keycode: int, action: str = "down") -> None:
123
+ """发送 Android 按键码事件。
124
+
125
+ Args:
126
+ keycode: Android 按键码(例如 4 = 返回,3 = 主页)。
127
+ action: ``"down"`` 或 ``"up"``。
128
+ """
129
+ action_code = {"down": 0, "up": 1}.get(action)
130
+ if action_code is None:
131
+ raise ValueError(f"无效的按键动作:{action!r}(请使用 down/up)")
132
+ self._submit(self._keycode_async(action_code, keycode))
133
+
134
+ def key_press(self, keycode: int) -> None:
135
+ """按下并释放按键(模拟单次点击)。"""
136
+ self.keycode(keycode, "down")
137
+ time.sleep(0.02)
138
+ self.keycode(keycode, "up")
139
+
140
+ # ── 剪贴板 ────────────────────────────────────────────
141
+
142
+ def set_clipboard(self, text: str, paste: bool = False) -> None:
143
+ """设置设备剪贴板(可选同时粘贴)。"""
144
+ self._submit(self._clipboard_async(text, paste))
145
+
146
+ # ── 内部异步辅助方法 ───────────────────────────────
147
+
148
+ async def _connect_async(
149
+ self,
150
+ serial: str | None,
151
+ video: bool,
152
+ audio: bool,
153
+ control: bool,
154
+ **kwargs: Any,
155
+ ) -> None:
156
+ if self._client is not None:
157
+ return
158
+ opts = ScrcpyOptions(
159
+ serial=serial,
160
+ adb_path=self._adb_path,
161
+ server_path=self._server_path,
162
+ video=video,
163
+ audio=audio,
164
+ control=control,
165
+ **kwargs,
166
+ )
167
+ client = ScrcpyClient(opts)
168
+ await client.start()
169
+ self._client = client
170
+
171
+ async def _disconnect_async(self) -> None:
172
+ if self._client is None:
173
+ return
174
+ await self._client.stop()
175
+ self._client = None
176
+ self._session_size = None
177
+
178
+ async def _touch_async(self, action: int, x: int, y: int) -> None:
179
+ if self._client is None or self._client.control is None:
180
+ raise RuntimeError("未连接到设备")
181
+ width, height = self._session_size or (1, 1)
182
+ await self._client.control.send_touch(action, x, y, width, height)
183
+
184
+ async def _keycode_async(self, action: int, keycode: int) -> None:
185
+ if self._client is None or self._client.control is None:
186
+ raise RuntimeError("未连接到设备")
187
+ await self._client.control.send_keycode(action, keycode)
188
+
189
+ async def _clipboard_async(self, text: str, paste: bool) -> None:
190
+ if self._client is None or self._client.control is None:
191
+ raise RuntimeError("未连接到设备")
192
+ await self._client.control.set_clipboard(text, sequence=1, paste=paste)
193
+
194
+ # ── 异步循环桥接 ─────────────────────────────────────
195
+
196
+ def _submit(self, coro: Any) -> Any:
197
+ future = asyncio.run_coroutine_threadsafe(coro, self._loop)
198
+ return future.result()
199
+
200
+ def _run_loop(self) -> None:
201
+ asyncio.set_event_loop(self._loop)
202
+ self._loop.run_forever()
203
+
204
+ def __enter__(self) -> AndroidDevice:
205
+ return self
206
+
207
+ def __exit__(self, *args: Any) -> None:
208
+ self.disconnect()
@@ -0,0 +1,195 @@
1
+ """ADB 启动器,用于 scrcpy-server v4.0."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import re
8
+ import subprocess
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ if os.name == "nt":
13
+ _CREATE_NO_WINDOW = subprocess.CREATE_NO_WINDOW
14
+ else:
15
+ _CREATE_NO_WINDOW = 0
16
+
17
+
18
+ class AdbError(RuntimeError):
19
+ pass
20
+
21
+
22
+ @dataclass(slots=True)
23
+ class AdbServerLauncher:
24
+ adb_path: Path
25
+ server_path: Path
26
+ serial: str | None = None
27
+ device_server_path: str = "/data/local/tmp/scrcpy-server.jar"
28
+
29
+ async def connect_tcpip(self, address: str) -> None:
30
+ stdout, stderr = await self._adb_global_output("connect", address)
31
+ message = f"{stdout}\n{stderr}".lower()
32
+ if "failed" in message or "unable" in message or "cannot" in message:
33
+ raise AdbError((stderr or stdout).strip())
34
+ self.serial = address
35
+
36
+ async def disconnect_tcpip(self, address: str) -> None:
37
+ await self._adb_global("disconnect", address, check=False)
38
+
39
+ async def enable_tcpip(self, port: int = 5555) -> None:
40
+ await self._adb("tcpip", str(port))
41
+
42
+ async def get_device_ip(self) -> str:
43
+ """从 `adb shell ip route` 解析 Wi-Fi IP 地址。
44
+
45
+ 仅考虑接口名称以 ``wlan`` 开头的行,
46
+ 与官方 scrcpy 行为一致,跳过非 Wi-Fi 接口,
47
+ 例如 rmnet_data(移动数据)或 usb0(USB 共享网络)。
48
+ """
49
+ route = await self.shell_output("ip", "route")
50
+ for line in route.splitlines():
51
+ # 查找类似如下的行:
52
+ # 192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x
53
+ fields = line.split()
54
+ for i, field in enumerate(fields):
55
+ if field == "dev" and i + 1 < len(fields) and fields[i + 1].startswith("wlan"):
56
+ # 找到 wlan 行,提取源 IP
57
+ for j, f in enumerate(fields):
58
+ if f == "src" and j + 1 < len(fields):
59
+ ip = fields[j + 1]
60
+ if re.match(r"^\d{1,3}(\.\d{1,3}){3}$", ip):
61
+ return ip
62
+ raise AdbError(
63
+ f"在 `adb shell ip route` 中找不到 Wi-Fi(wlan)设备的 IP: {route.strip()}"
64
+ )
65
+
66
+ async def discover_usb_serial(self) -> str | None:
67
+ """发现单个 USB 连接设备的序列号。
68
+
69
+ 如果没有 USB 设备或有多个设备,则返回 None。
70
+ """
71
+ proc = await asyncio.create_subprocess_exec(
72
+ str(self.adb_path),
73
+ "devices", "-l",
74
+ stdout=asyncio.subprocess.PIPE,
75
+ stderr=asyncio.subprocess.PIPE,
76
+ creationflags=_CREATE_NO_WINDOW,
77
+ )
78
+ stdout, _ = await proc.communicate()
79
+ out = stdout.decode(errors="replace")
80
+
81
+ usb_serials = []
82
+ for line in out.splitlines():
83
+ # 跳过空行和 "List of devices attached" 标题行
84
+ if not line.strip() or not line[0].isalnum():
85
+ continue
86
+ parts = line.split()
87
+ if len(parts) < 2:
88
+ continue
89
+ serial = parts[0]
90
+ state = parts[1]
91
+ if state != "device":
92
+ continue
93
+ # USB 设备的序列号不包含 ":"
94
+ # TCP/IP 设备的格式为 ip:port
95
+ if ":" not in serial:
96
+ usb_serials.append(serial)
97
+
98
+ if len(usb_serials) == 1:
99
+ return usb_serials[0]
100
+ return None
101
+
102
+ async def shell_output(self, *args: str) -> str:
103
+ stdout, _ = await self._adb_output("shell", *args)
104
+ return stdout
105
+
106
+ async def push_server(self) -> None:
107
+ await self._adb("push", str(self.server_path), self.device_server_path)
108
+
109
+ async def reverse(self, socket_name: str, port: int) -> None:
110
+ await self._adb("reverse", f"localabstract:{socket_name}", f"tcp:{port}")
111
+
112
+ async def remove_reverse(self, socket_name: str) -> None:
113
+ await self._adb("reverse", "--remove", f"localabstract:{socket_name}", check=False)
114
+
115
+ async def forward(self, socket_name: str, port: int) -> None:
116
+ await self._adb("forward", f"tcp:{port}", f"localabstract:{socket_name}")
117
+
118
+ async def remove_forward(self, port: int) -> None:
119
+ await self._adb("forward", "--remove", f"tcp:{port}", check=False)
120
+
121
+ async def start_server_process(
122
+ self,
123
+ *,
124
+ version: str,
125
+ scid: int,
126
+ tunnel_forward: bool,
127
+ video: bool,
128
+ audio: bool,
129
+ control: bool,
130
+ log_level: str,
131
+ extra_args: list[str] | None = None,
132
+ ) -> asyncio.subprocess.Process:
133
+ args = [
134
+ str(self.adb_path),
135
+ *self._serial_args(),
136
+ "shell",
137
+ f"CLASSPATH={self.device_server_path}",
138
+ "app_process",
139
+ "/",
140
+ "com.genymobile.scrcpy.Server",
141
+ version,
142
+ f"scid={scid:08x}",
143
+ f"log_level={log_level}",
144
+ ]
145
+ if tunnel_forward:
146
+ args.append("tunnel_forward=true")
147
+ if not video:
148
+ args.append("video=false")
149
+ if not audio:
150
+ args.append("audio=false")
151
+ if not control:
152
+ args.append("control=false")
153
+ if extra_args:
154
+ args.extend(extra_args)
155
+
156
+ return await asyncio.create_subprocess_exec(
157
+ *args,
158
+ stdout=asyncio.subprocess.PIPE,
159
+ stderr=asyncio.subprocess.PIPE,
160
+ creationflags=_CREATE_NO_WINDOW,
161
+ )
162
+
163
+ async def _adb(self, *args: str, check: bool = True) -> asyncio.subprocess.Process:
164
+ proc, _, _ = await self._run_adb([*self._serial_args(), *args], check=check)
165
+ return proc
166
+
167
+ async def _adb_global(self, *args: str, check: bool = True) -> asyncio.subprocess.Process:
168
+ proc, _, _ = await self._run_adb([*args], check=check)
169
+ return proc
170
+
171
+ async def _adb_output(self, *args: str, check: bool = True) -> tuple[str, str]:
172
+ _, stdout, stderr = await self._run_adb([*self._serial_args(), *args], check=check)
173
+ return stdout, stderr
174
+
175
+ async def _adb_global_output(self, *args: str, check: bool = True) -> tuple[str, str]:
176
+ _, stdout, stderr = await self._run_adb([*args], check=check)
177
+ return stdout, stderr
178
+
179
+ async def _run_adb(self, args: list[str], check: bool = True) -> tuple[asyncio.subprocess.Process, str, str]:
180
+ proc = await asyncio.create_subprocess_exec(
181
+ str(self.adb_path),
182
+ *args,
183
+ stdout=asyncio.subprocess.PIPE,
184
+ stderr=asyncio.subprocess.PIPE,
185
+ creationflags=_CREATE_NO_WINDOW,
186
+ )
187
+ stdout, stderr = await proc.communicate()
188
+ out = stdout.decode(errors="replace").strip()
189
+ err = stderr.decode(errors="replace").strip()
190
+ if check and proc.returncode:
191
+ raise AdbError(f"adb {' '.join(args)} failed: {err or out}")
192
+ return proc, out, err
193
+
194
+ def _serial_args(self) -> list[str]:
195
+ return ["-s", self.serial] if self.serial else []
@@ -0,0 +1,115 @@
1
+ """scrcpy v4.0 协议的二进制辅助函数。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import Awaitable, Callable
7
+
8
+
9
+ ReadExact = Callable[[int], Awaitable[bytes]]
10
+
11
+
12
+ class ProtocolError(Exception):
13
+ """当网络上的字节不符合预期协议时抛出。"""
14
+
15
+
16
+ class StreamDisabledError(ProtocolError):
17
+ """当 scrcpy-server 显式禁用媒体流时抛出。"""
18
+
19
+
20
+ class StreamConfigurationError(ProtocolError):
21
+ """当 scrcpy-server 报告媒体流配置错误时抛出。"""
22
+
23
+
24
+ def u16be(value: int) -> bytes:
25
+ return int(value).to_bytes(2, "big", signed=False)
26
+
27
+
28
+ def i16be(value: int) -> bytes:
29
+ return int(value).to_bytes(2, "big", signed=True)
30
+
31
+
32
+ def u32be(value: int) -> bytes:
33
+ return int(value).to_bytes(4, "big", signed=False)
34
+
35
+
36
+ def i32be(value: int) -> bytes:
37
+ return int(value).to_bytes(4, "big", signed=True)
38
+
39
+
40
+ def u64be(value: int) -> bytes:
41
+ return int(value).to_bytes(8, "big", signed=False)
42
+
43
+
44
+ def read_u16be(data: bytes | bytearray | memoryview, offset: int = 0) -> int:
45
+ return int.from_bytes(data[offset : offset + 2], "big", signed=False)
46
+
47
+
48
+ def read_i16be(data: bytes | bytearray | memoryview, offset: int = 0) -> int:
49
+ return int.from_bytes(data[offset : offset + 2], "big", signed=True)
50
+
51
+
52
+ def read_u32be(data: bytes | bytearray | memoryview, offset: int = 0) -> int:
53
+ return int.from_bytes(data[offset : offset + 4], "big", signed=False)
54
+
55
+
56
+ def read_i32be(data: bytes | bytearray | memoryview, offset: int = 0) -> int:
57
+ return int.from_bytes(data[offset : offset + 4], "big", signed=True)
58
+
59
+
60
+ def read_u64be(data: bytes | bytearray | memoryview, offset: int = 0) -> int:
61
+ return int.from_bytes(data[offset : offset + 8], "big", signed=False)
62
+
63
+
64
+ async def read_exact(reader: asyncio.StreamReader, size: int) -> bytes:
65
+ try:
66
+ return await reader.readexactly(size)
67
+ except asyncio.IncompleteReadError as exc:
68
+ raise EOFError(f"期望 {size} 字节,实际获取 {len(exc.partial)} 字节") from exc
69
+
70
+
71
+ def utf8_truncate(text: str, max_bytes: int) -> bytes:
72
+ raw = text.encode("utf-8")
73
+ if len(raw) <= max_bytes:
74
+ return raw
75
+ end = max_bytes
76
+ while end > 0:
77
+ try:
78
+ return raw[:end].decode("utf-8").encode("utf-8")
79
+ except UnicodeDecodeError:
80
+ end -= 1
81
+ return b""
82
+
83
+
84
+ def string32(text: str, max_payload_len: int) -> bytes:
85
+ raw = utf8_truncate(text, max_payload_len)
86
+ return u32be(len(raw)) + raw
87
+
88
+
89
+ def string8(text: str, max_payload_len: int = 255) -> bytes:
90
+ if max_payload_len > 255:
91
+ raise ValueError("1字节字符串不能超过255字节")
92
+ raw = utf8_truncate(text, max_payload_len)
93
+ return bytes([len(raw)]) + raw
94
+
95
+
96
+ def float_to_u16fp(value: float) -> int:
97
+ clamped = max(0.0, min(1.0, value))
98
+ return int(round(clamped * 0xFFFF))
99
+
100
+
101
+ def float_to_i16fp(value: float) -> int:
102
+ clamped = max(-1.0, min(1.0, value))
103
+ if clamped == 1.0:
104
+ return 0x7FFF
105
+ return int(round(clamped * 0x8000))
106
+
107
+
108
+ def u16fp_to_float(value: int) -> float:
109
+ return value / 0xFFFF
110
+
111
+
112
+ def i16fp_to_float(value: int) -> float:
113
+ if value & 0x8000:
114
+ value -= 0x10000
115
+ return value / 0x8000