kotonebot 0.1.0__py3-none-any.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.
- kotonebot/__init__.py +40 -0
- kotonebot/backend/__init__.py +0 -0
- kotonebot/backend/bot.py +302 -0
- kotonebot/backend/color.py +525 -0
- kotonebot/backend/context/__init__.py +3 -0
- kotonebot/backend/context/context.py +1001 -0
- kotonebot/backend/context/task_action.py +176 -0
- kotonebot/backend/core.py +126 -0
- kotonebot/backend/debug/__init__.py +1 -0
- kotonebot/backend/debug/entry.py +89 -0
- kotonebot/backend/debug/mock.py +79 -0
- kotonebot/backend/debug/server.py +223 -0
- kotonebot/backend/debug/vars.py +346 -0
- kotonebot/backend/dispatch.py +228 -0
- kotonebot/backend/flow_controller.py +197 -0
- kotonebot/backend/image.py +748 -0
- kotonebot/backend/loop.py +277 -0
- kotonebot/backend/ocr.py +511 -0
- kotonebot/backend/preprocessor.py +103 -0
- kotonebot/client/__init__.py +10 -0
- kotonebot/client/device.py +500 -0
- kotonebot/client/fast_screenshot.py +378 -0
- kotonebot/client/host/__init__.py +12 -0
- kotonebot/client/host/adb_common.py +94 -0
- kotonebot/client/host/custom.py +114 -0
- kotonebot/client/host/leidian_host.py +202 -0
- kotonebot/client/host/mumu12_host.py +245 -0
- kotonebot/client/host/protocol.py +213 -0
- kotonebot/client/host/windows_common.py +55 -0
- kotonebot/client/implements/__init__.py +7 -0
- kotonebot/client/implements/adb.py +85 -0
- kotonebot/client/implements/adb_raw.py +159 -0
- kotonebot/client/implements/nemu_ipc/__init__.py +8 -0
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +280 -0
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -0
- kotonebot/client/implements/remote_windows.py +193 -0
- kotonebot/client/implements/uiautomator2.py +82 -0
- kotonebot/client/implements/windows.py +168 -0
- kotonebot/client/protocol.py +69 -0
- kotonebot/client/registration.py +24 -0
- kotonebot/config/__init__.py +1 -0
- kotonebot/config/base_config.py +96 -0
- kotonebot/config/manager.py +36 -0
- kotonebot/errors.py +72 -0
- kotonebot/interop/win/__init__.py +0 -0
- kotonebot/interop/win/message_box.py +314 -0
- kotonebot/interop/win/reg.py +37 -0
- kotonebot/interop/win/shortcut.py +43 -0
- kotonebot/interop/win/task_dialog.py +469 -0
- kotonebot/logging/__init__.py +2 -0
- kotonebot/logging/log.py +18 -0
- kotonebot/primitives/__init__.py +17 -0
- kotonebot/primitives/geometry.py +290 -0
- kotonebot/primitives/visual.py +63 -0
- kotonebot/tools/__init__.py +0 -0
- kotonebot/tools/mirror.py +354 -0
- kotonebot/ui/__init__.py +0 -0
- kotonebot/ui/file_host/sensio.py +36 -0
- kotonebot/ui/file_host/tmp_send.py +54 -0
- kotonebot/ui/pushkit/__init__.py +3 -0
- kotonebot/ui/pushkit/image_host.py +87 -0
- kotonebot/ui/pushkit/protocol.py +13 -0
- kotonebot/ui/pushkit/wxpusher.py +53 -0
- kotonebot/ui/user.py +144 -0
- kotonebot/util.py +409 -0
- kotonebot-0.1.0.dist-info/METADATA +204 -0
- kotonebot-0.1.0.dist-info/RECORD +70 -0
- kotonebot-0.1.0.dist-info/WHEEL +5 -0
- kotonebot-0.1.0.dist-info/licenses/LICENSE +674 -0
- kotonebot-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from typing import Generic, TypeVar, TypeGuard, overload
|
|
2
|
+
|
|
3
|
+
T = TypeVar('T')
|
|
4
|
+
|
|
5
|
+
class Vector2D(Generic[T]):
|
|
6
|
+
"""2D 坐标类"""
|
|
7
|
+
def __init__(self, x: T, y: T, *, name: str | None = None):
|
|
8
|
+
self.x = x
|
|
9
|
+
self.y = y
|
|
10
|
+
self.name: str | None = name
|
|
11
|
+
"""坐标的名称。"""
|
|
12
|
+
|
|
13
|
+
def __getitem__(self, item: int):
|
|
14
|
+
if item == 0:
|
|
15
|
+
return self.x
|
|
16
|
+
elif item == 1:
|
|
17
|
+
return self.y
|
|
18
|
+
else:
|
|
19
|
+
raise IndexError
|
|
20
|
+
|
|
21
|
+
def __repr__(self) -> str:
|
|
22
|
+
return f'Point<"{self.name}" at ({self.x}, {self.y})>'
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return f'({self.x}, {self.y})'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Vector3D(Generic[T]):
|
|
29
|
+
"""三元组类。"""
|
|
30
|
+
def __init__(self, x: T, y: T, z: T, *, name: str | None = None):
|
|
31
|
+
self.x = x
|
|
32
|
+
self.y = y
|
|
33
|
+
self.z = z
|
|
34
|
+
self.name: str | None = name
|
|
35
|
+
"""坐标的名称。"""
|
|
36
|
+
|
|
37
|
+
def __getitem__(self, item: int):
|
|
38
|
+
if item == 0:
|
|
39
|
+
return self.x
|
|
40
|
+
elif item == 1:
|
|
41
|
+
return self.y
|
|
42
|
+
elif item == 2:
|
|
43
|
+
return self.z
|
|
44
|
+
else:
|
|
45
|
+
raise IndexError
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def xyz(self) -> tuple[T, T, T]:
|
|
49
|
+
"""
|
|
50
|
+
三元组 (x, y, z)。OpenCV 格式的坐标。
|
|
51
|
+
"""
|
|
52
|
+
return self.x, self.y, self.z
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def xy(self) -> tuple[T, T]:
|
|
56
|
+
"""
|
|
57
|
+
二元组 (x, y)。OpenCV 格式的坐标。
|
|
58
|
+
"""
|
|
59
|
+
return self.x, self.y
|
|
60
|
+
|
|
61
|
+
class Vector4D(Generic[T]):
|
|
62
|
+
"""四元组类。"""
|
|
63
|
+
def __init__(self, x: T, y: T, z: T, w: T, *, name: str | None = None):
|
|
64
|
+
self.x = x
|
|
65
|
+
self.y = y
|
|
66
|
+
self.z = z
|
|
67
|
+
self.w = w
|
|
68
|
+
self.name: str | None = name
|
|
69
|
+
"""坐标的名称。"""
|
|
70
|
+
|
|
71
|
+
def __getitem__(self, item: int):
|
|
72
|
+
if item == 0:
|
|
73
|
+
return self.x
|
|
74
|
+
elif item == 1:
|
|
75
|
+
return self.y
|
|
76
|
+
elif item == 2:
|
|
77
|
+
return self.z
|
|
78
|
+
elif item == 3:
|
|
79
|
+
return self.w
|
|
80
|
+
else:
|
|
81
|
+
raise IndexError
|
|
82
|
+
|
|
83
|
+
Size = Vector2D[int]
|
|
84
|
+
"""尺寸。相当于 Vector2D[int]"""
|
|
85
|
+
RectTuple = tuple[int, int, int, int]
|
|
86
|
+
"""矩形。(x, y, w, h)"""
|
|
87
|
+
PointTuple = tuple[int, int]
|
|
88
|
+
"""点。(x, y)"""
|
|
89
|
+
|
|
90
|
+
class Point(Vector2D[int]):
|
|
91
|
+
"""点。"""
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def xy(self) -> PointTuple:
|
|
95
|
+
"""
|
|
96
|
+
二元组 (x, y)。OpenCV 格式的坐标。
|
|
97
|
+
"""
|
|
98
|
+
return self.x, self.y
|
|
99
|
+
|
|
100
|
+
def offset(self, dx: int, dy: int) -> 'Point':
|
|
101
|
+
"""
|
|
102
|
+
偏移坐标。
|
|
103
|
+
|
|
104
|
+
:param dx: 偏移量。
|
|
105
|
+
:param dy: 偏移量。
|
|
106
|
+
:return: 偏移后的坐标。
|
|
107
|
+
"""
|
|
108
|
+
return Point(self.x + dx, self.y + dy, name=self.name)
|
|
109
|
+
|
|
110
|
+
def __add__(self, other: 'Point | PointTuple') -> 'Point':
|
|
111
|
+
"""
|
|
112
|
+
相加。
|
|
113
|
+
|
|
114
|
+
:param other: 另一个 Point 对象或二元组 (x: int, y: int)。
|
|
115
|
+
:return: 相加后的点。
|
|
116
|
+
"""
|
|
117
|
+
if isinstance(other, Point):
|
|
118
|
+
return Point(self.x + other.x, self.y + other.y, name=self.name)
|
|
119
|
+
else:
|
|
120
|
+
return Point(self.x + other[0], self.y + other[1], name=self.name)
|
|
121
|
+
|
|
122
|
+
def __sub__(self, other: 'Point | PointTuple') -> 'Point':
|
|
123
|
+
"""
|
|
124
|
+
相减。
|
|
125
|
+
|
|
126
|
+
:param other: 另一个 Point 对象或二元组 (x: int, y: int)。
|
|
127
|
+
:return: 相减后的点。
|
|
128
|
+
"""
|
|
129
|
+
if isinstance(other, Point):
|
|
130
|
+
return Point(self.x - other.x, self.y - other.y, name=self.name)
|
|
131
|
+
else:
|
|
132
|
+
return Point(self.x - other[0], self.y - other[1], name=self.name)
|
|
133
|
+
|
|
134
|
+
class Rect:
|
|
135
|
+
"""
|
|
136
|
+
矩形类。
|
|
137
|
+
"""
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
x: int | None = None,
|
|
141
|
+
y: int | None = None,
|
|
142
|
+
w: int | None = None,
|
|
143
|
+
h: int | None = None,
|
|
144
|
+
*,
|
|
145
|
+
xywh: RectTuple | None = None,
|
|
146
|
+
name: str | None = None,
|
|
147
|
+
):
|
|
148
|
+
"""
|
|
149
|
+
从给定的坐标信息创建矩形。
|
|
150
|
+
|
|
151
|
+
参数 `x`, `y`, `w`, `h` 和 `xywh` 必须至少指定一组。
|
|
152
|
+
|
|
153
|
+
:param x: 矩形左上角的 X 坐标。
|
|
154
|
+
:param y: 矩形左上角的 Y 坐标。
|
|
155
|
+
:param w: 矩形的宽度。
|
|
156
|
+
:param h: 矩形的高度。
|
|
157
|
+
:param xywh: 四元组 (x, y, w, h)。
|
|
158
|
+
:param name: 矩形的名称。
|
|
159
|
+
:raises ValueError: 提供的坐标参数不完整时抛出。
|
|
160
|
+
"""
|
|
161
|
+
if xywh is not None:
|
|
162
|
+
x, y, w, h = xywh
|
|
163
|
+
elif (
|
|
164
|
+
x is not None and
|
|
165
|
+
y is not None and
|
|
166
|
+
w is not None and
|
|
167
|
+
h is not None
|
|
168
|
+
):
|
|
169
|
+
pass
|
|
170
|
+
else:
|
|
171
|
+
raise ValueError('Either xywh or x, y, w, h must be provided.')
|
|
172
|
+
|
|
173
|
+
self.x1 = x
|
|
174
|
+
"""矩形左上角的 X 坐标。"""
|
|
175
|
+
self.y1 = y
|
|
176
|
+
"""矩形左上角的 Y 坐标。"""
|
|
177
|
+
self.w = w
|
|
178
|
+
"""矩形的宽度。"""
|
|
179
|
+
self.h = h
|
|
180
|
+
"""矩形的高度。"""
|
|
181
|
+
self.name: str | None = name
|
|
182
|
+
"""矩形的名称。"""
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def from_xyxy(cls, x1: int, y1: int, x2: int, y2: int) -> 'Rect':
|
|
186
|
+
"""
|
|
187
|
+
从 (x1, y1, x2, y2) 创建矩形。
|
|
188
|
+
:return: 创建结果。
|
|
189
|
+
"""
|
|
190
|
+
return cls(x1, y1, x2 - x1, y2 - y1)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def x2(self) -> int:
|
|
194
|
+
"""矩形右下角的 X 坐标。"""
|
|
195
|
+
return self.x1 + self.w
|
|
196
|
+
|
|
197
|
+
@x2.setter
|
|
198
|
+
def x2(self, value: int):
|
|
199
|
+
self.w = value - self.x1
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def y2(self) -> int:
|
|
203
|
+
"""矩形右下角的 Y 坐标。"""
|
|
204
|
+
return self.y1 + self.h
|
|
205
|
+
|
|
206
|
+
@y2.setter
|
|
207
|
+
def y2(self, value: int):
|
|
208
|
+
self.h = value - self.y1
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def xywh(self) -> RectTuple:
|
|
212
|
+
"""
|
|
213
|
+
四元组 (x1, y1, w, h)。OpenCV 格式的坐标。
|
|
214
|
+
"""
|
|
215
|
+
return self.x1, self.y1, self.w, self.h
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def xyxy(self) -> RectTuple:
|
|
219
|
+
"""
|
|
220
|
+
四元组 (x1, y1, x2, y2)。
|
|
221
|
+
"""
|
|
222
|
+
return self.x1, self.y1, self.x2, self.y2
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def top_left(self) -> Point:
|
|
226
|
+
"""
|
|
227
|
+
矩形的左上角点。
|
|
228
|
+
"""
|
|
229
|
+
if self.name:
|
|
230
|
+
name = "Left-top of rect "+ self.name
|
|
231
|
+
else:
|
|
232
|
+
name = None
|
|
233
|
+
return Point(self.x1, self.y1, name=name)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def bottom_right(self) -> Point:
|
|
237
|
+
"""
|
|
238
|
+
矩形的右下角点。
|
|
239
|
+
"""
|
|
240
|
+
if self.name:
|
|
241
|
+
name = "Right-bottom of rect "+ self.name
|
|
242
|
+
else:
|
|
243
|
+
name = None
|
|
244
|
+
return Point(self.x2, self.y2, name=name)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def left_bottom(self) -> Point:
|
|
248
|
+
"""
|
|
249
|
+
矩形的左下角点。
|
|
250
|
+
"""
|
|
251
|
+
if self.name:
|
|
252
|
+
name = "Left-bottom of rect "+ self.name
|
|
253
|
+
else:
|
|
254
|
+
name = None
|
|
255
|
+
return Point(self.x1, self.y2, name=name)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def right_top(self) -> Point:
|
|
259
|
+
"""
|
|
260
|
+
矩形的右上角点。
|
|
261
|
+
"""
|
|
262
|
+
if self.name:
|
|
263
|
+
name = "Right-top of rect "+ self.name
|
|
264
|
+
else:
|
|
265
|
+
name = None
|
|
266
|
+
return Point(self.x2, self.y1, name=name)
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def center(self) -> Point:
|
|
270
|
+
"""
|
|
271
|
+
矩形的中心点。
|
|
272
|
+
"""
|
|
273
|
+
if self.name:
|
|
274
|
+
name = "Center of rect "+ self.name
|
|
275
|
+
else:
|
|
276
|
+
name = None
|
|
277
|
+
return Point(self.x1 + self.w // 2, self.y1 + self.h // 2, name=name)
|
|
278
|
+
|
|
279
|
+
def __repr__(self) -> str:
|
|
280
|
+
return f'Rect<"{self.name}" at (x={self.x1}, y={self.y1}, w={self.w}, h={self.h})>'
|
|
281
|
+
|
|
282
|
+
def __str__(self) -> str:
|
|
283
|
+
return f'(x={self.x1}, y={self.y1}, w={self.w}, h={self.h})'
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def is_point(obj: object) -> TypeGuard[Point]:
|
|
287
|
+
return isinstance(obj, Point)
|
|
288
|
+
|
|
289
|
+
def is_rect(obj: object) -> TypeGuard[Rect]:
|
|
290
|
+
return isinstance(obj, Rect)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from cv2.typing import MatLike
|
|
4
|
+
|
|
5
|
+
from .geometry import Size
|
|
6
|
+
from kotonebot.util import cv2_imread
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class Image:
|
|
11
|
+
"""
|
|
12
|
+
图像类。
|
|
13
|
+
"""
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
pixels: MatLike | None = None,
|
|
17
|
+
file_path: str | None = None,
|
|
18
|
+
lazy_load: bool = False,
|
|
19
|
+
name: str | None = None,
|
|
20
|
+
description: str | None = None
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
从内存数据或图像文件创建图像类。
|
|
24
|
+
|
|
25
|
+
:param pixels: 图像数据。格式必须为 BGR。
|
|
26
|
+
:param file_path: 图像文件路径。
|
|
27
|
+
:param lazy_load: 是否延迟加载图像数据。
|
|
28
|
+
若为 False,立即载入,否则仅当访问图像数据时才载入。仅当从文件创建图像类时生效。
|
|
29
|
+
:param name: 图像名称。
|
|
30
|
+
:param description: 图像描述。
|
|
31
|
+
"""
|
|
32
|
+
self.name: str | None = name
|
|
33
|
+
"""图像名称。"""
|
|
34
|
+
self.description: str | None = description
|
|
35
|
+
"""图像描述。"""
|
|
36
|
+
self.file_path: str | None = file_path
|
|
37
|
+
"""图像的文件路径。"""
|
|
38
|
+
self.__pixels: MatLike | None = None
|
|
39
|
+
# 立即加载
|
|
40
|
+
if not lazy_load and self.file_path:
|
|
41
|
+
_ = self.pixels
|
|
42
|
+
# 传入像素数据而不是文件
|
|
43
|
+
if pixels is not None:
|
|
44
|
+
self.__pixels = pixels
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def pixels(self) -> MatLike:
|
|
48
|
+
"""图像的像素数据。"""
|
|
49
|
+
if self.__pixels is None:
|
|
50
|
+
if not self.file_path:
|
|
51
|
+
raise ValueError('Either pixels or file_path must be provided.')
|
|
52
|
+
logger.debug('Loading image "%s" from %s...', self.name or '(unnamed)', self.file_path)
|
|
53
|
+
self.__pixels = cv2_imread(self.file_path)
|
|
54
|
+
return self.__pixels
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def size(self) -> Size:
|
|
58
|
+
return Size(self.pixels.shape[1], self.pixels.shape[0])
|
|
59
|
+
|
|
60
|
+
class Template(Image):
|
|
61
|
+
"""
|
|
62
|
+
模板图像类。
|
|
63
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import wx
|
|
2
|
+
import cv2
|
|
3
|
+
import numpy as np
|
|
4
|
+
import time
|
|
5
|
+
from typing import Optional, Tuple, Callable
|
|
6
|
+
from threading import Thread, Lock
|
|
7
|
+
from cv2.typing import MatLike
|
|
8
|
+
from queue import Queue
|
|
9
|
+
|
|
10
|
+
from kotonebot.client.device import Device
|
|
11
|
+
|
|
12
|
+
class DeviceMirrorPanel(wx.Panel):
|
|
13
|
+
def __init__(self, parent, device: Device, log_callback=None):
|
|
14
|
+
super().__init__(parent)
|
|
15
|
+
self.device = device
|
|
16
|
+
self.screen_bitmap: Optional[wx.Bitmap] = None
|
|
17
|
+
self.fps = 0
|
|
18
|
+
self.last_frame_time = time.time()
|
|
19
|
+
self.frame_count = 0
|
|
20
|
+
self.is_running = True
|
|
21
|
+
self.lock = Lock()
|
|
22
|
+
self.last_mouse_pos = (0, 0)
|
|
23
|
+
self.is_dragging = False
|
|
24
|
+
self.screenshot_interval = 0 # 截图耗时(ms)
|
|
25
|
+
self.log_callback = log_callback
|
|
26
|
+
self.operation_queue = Queue()
|
|
27
|
+
|
|
28
|
+
# 设置背景色为黑色
|
|
29
|
+
self.SetBackgroundColour(wx.BLACK)
|
|
30
|
+
|
|
31
|
+
# 双缓冲,减少闪烁
|
|
32
|
+
self.SetDoubleBuffered(True)
|
|
33
|
+
|
|
34
|
+
# 绑定事件
|
|
35
|
+
self.Bind(wx.EVT_PAINT, self.on_paint)
|
|
36
|
+
self.Bind(wx.EVT_SIZE, self.on_size)
|
|
37
|
+
self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
|
|
38
|
+
self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
|
|
39
|
+
self.Bind(wx.EVT_MOTION, self.on_motion)
|
|
40
|
+
|
|
41
|
+
# 启动刷新线程
|
|
42
|
+
self.update_thread = Thread(target=self.update_screen, daemon=True)
|
|
43
|
+
self.update_thread.start()
|
|
44
|
+
|
|
45
|
+
# 启动操作处理线程
|
|
46
|
+
self.operation_thread = Thread(target=self.process_operations, daemon=True)
|
|
47
|
+
self.operation_thread.start()
|
|
48
|
+
|
|
49
|
+
def process_operations(self):
|
|
50
|
+
"""处理设备操作的线程"""
|
|
51
|
+
while self.is_running:
|
|
52
|
+
try:
|
|
53
|
+
operation = self.operation_queue.get()
|
|
54
|
+
if operation is not None:
|
|
55
|
+
operation()
|
|
56
|
+
self.operation_queue.task_done()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
if self.log_callback:
|
|
59
|
+
self.log_callback(f"操作执行错误: {e}")
|
|
60
|
+
|
|
61
|
+
def execute_device_operation(self, operation: Callable):
|
|
62
|
+
"""将设备操作添加到队列"""
|
|
63
|
+
self.operation_queue.put(operation)
|
|
64
|
+
|
|
65
|
+
def update_screen(self):
|
|
66
|
+
while self.is_running:
|
|
67
|
+
try:
|
|
68
|
+
# 获取设备截图并计时
|
|
69
|
+
start_time = time.time()
|
|
70
|
+
frame = self.device.screenshot()
|
|
71
|
+
end_time = time.time()
|
|
72
|
+
self.screenshot_interval = int((end_time - start_time) * 1000)
|
|
73
|
+
|
|
74
|
+
if frame is None:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
# 计算FPS
|
|
78
|
+
current_time = time.time()
|
|
79
|
+
self.frame_count += 1
|
|
80
|
+
if current_time - self.last_frame_time >= 1.0:
|
|
81
|
+
self.fps = self.frame_count
|
|
82
|
+
self.frame_count = 0
|
|
83
|
+
self.last_frame_time = current_time
|
|
84
|
+
|
|
85
|
+
# 转换为wx.Bitmap
|
|
86
|
+
height, width = frame.shape[:2]
|
|
87
|
+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
88
|
+
wximage = wx.Bitmap.FromBuffer(width, height, frame)
|
|
89
|
+
|
|
90
|
+
with self.lock:
|
|
91
|
+
self.screen_bitmap = wximage
|
|
92
|
+
|
|
93
|
+
# 请求重绘
|
|
94
|
+
wx.CallAfter(self.Refresh)
|
|
95
|
+
|
|
96
|
+
# 控制刷新率
|
|
97
|
+
time.sleep(1/60)
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"Error updating screen: {e}")
|
|
101
|
+
time.sleep(1)
|
|
102
|
+
|
|
103
|
+
def on_paint(self, event):
|
|
104
|
+
dc = wx.BufferedPaintDC(self)
|
|
105
|
+
|
|
106
|
+
# 清空背景
|
|
107
|
+
dc.SetBackground(wx.Brush(wx.BLACK))
|
|
108
|
+
dc.Clear()
|
|
109
|
+
|
|
110
|
+
if not self.screen_bitmap:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# 绘制设备画面
|
|
114
|
+
with self.lock:
|
|
115
|
+
# 计算缩放比例,保持宽高比
|
|
116
|
+
panel_width, panel_height = self.GetSize()
|
|
117
|
+
bitmap_width = self.screen_bitmap.GetWidth()
|
|
118
|
+
bitmap_height = self.screen_bitmap.GetHeight()
|
|
119
|
+
|
|
120
|
+
scale = min(panel_width/bitmap_width, panel_height/bitmap_height)
|
|
121
|
+
scaled_width = int(bitmap_width * scale)
|
|
122
|
+
scaled_height = int(bitmap_height * scale)
|
|
123
|
+
|
|
124
|
+
# 居中显示
|
|
125
|
+
x = (panel_width - scaled_width) // 2
|
|
126
|
+
y = (panel_height - scaled_height) // 2
|
|
127
|
+
|
|
128
|
+
if scale != 1:
|
|
129
|
+
img = self.screen_bitmap.ConvertToImage()
|
|
130
|
+
img = img.Scale(scaled_width, scaled_height, wx.IMAGE_QUALITY_HIGH)
|
|
131
|
+
bitmap = wx.Bitmap(img)
|
|
132
|
+
else:
|
|
133
|
+
bitmap = self.screen_bitmap
|
|
134
|
+
|
|
135
|
+
dc.DrawBitmap(bitmap, x, y)
|
|
136
|
+
|
|
137
|
+
# 绘制FPS和截图时间
|
|
138
|
+
dc.SetTextForeground(wx.GREEN)
|
|
139
|
+
dc.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
|
|
140
|
+
dc.DrawText(f"FPS: {self.fps}", 10, 10)
|
|
141
|
+
dc.DrawText(f"Interval: {self.screenshot_interval}ms", 10, 30)
|
|
142
|
+
|
|
143
|
+
def on_size(self, event):
|
|
144
|
+
self.Refresh()
|
|
145
|
+
event.Skip()
|
|
146
|
+
|
|
147
|
+
def get_device_coordinates(self, x: int, y: int) -> Tuple[int, int]:
|
|
148
|
+
"""将面板坐标转换为设备坐标"""
|
|
149
|
+
if not self.screen_bitmap:
|
|
150
|
+
return (0, 0)
|
|
151
|
+
|
|
152
|
+
panel_width, panel_height = self.GetSize()
|
|
153
|
+
bitmap_width = self.screen_bitmap.GetWidth()
|
|
154
|
+
bitmap_height = self.screen_bitmap.GetHeight()
|
|
155
|
+
|
|
156
|
+
scale = min(panel_width/bitmap_width, panel_height/bitmap_height)
|
|
157
|
+
scaled_width = int(bitmap_width * scale)
|
|
158
|
+
scaled_height = int(bitmap_height * scale)
|
|
159
|
+
|
|
160
|
+
# 计算显示区域的偏移
|
|
161
|
+
x_offset = (panel_width - scaled_width) // 2
|
|
162
|
+
y_offset = (panel_height - scaled_height) // 2
|
|
163
|
+
|
|
164
|
+
# 转换坐标
|
|
165
|
+
device_x = int((x - x_offset) / scale)
|
|
166
|
+
device_y = int((y - y_offset) / scale)
|
|
167
|
+
|
|
168
|
+
# 确保坐标在设备范围内
|
|
169
|
+
device_x = max(0, min(device_x, bitmap_width-1))
|
|
170
|
+
device_y = max(0, min(device_y, bitmap_height-1))
|
|
171
|
+
|
|
172
|
+
return (device_x, device_y)
|
|
173
|
+
|
|
174
|
+
def on_left_down(self, event):
|
|
175
|
+
self.last_mouse_pos = event.GetPosition()
|
|
176
|
+
self.is_dragging = True
|
|
177
|
+
event.Skip()
|
|
178
|
+
|
|
179
|
+
def on_left_up(self, event):
|
|
180
|
+
if not self.is_dragging:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
self.is_dragging = False
|
|
184
|
+
pos = event.GetPosition()
|
|
185
|
+
|
|
186
|
+
# 如果鼠标位置没有明显变化,执行点击
|
|
187
|
+
if abs(pos[0] - self.last_mouse_pos[0]) < 5 and abs(pos[1] - self.last_mouse_pos[1]) < 5:
|
|
188
|
+
device_x, device_y = self.get_device_coordinates(*pos)
|
|
189
|
+
self.execute_device_operation(lambda: self.device.click(device_x, device_y))
|
|
190
|
+
if self.log_callback:
|
|
191
|
+
self.log_callback(f"点击: ({device_x}, {device_y})")
|
|
192
|
+
else:
|
|
193
|
+
# 执行滑动
|
|
194
|
+
start_x, start_y = self.get_device_coordinates(*self.last_mouse_pos)
|
|
195
|
+
end_x, end_y = self.get_device_coordinates(*pos)
|
|
196
|
+
self.execute_device_operation(lambda: self.device.swipe(start_x, start_y, end_x, end_y))
|
|
197
|
+
if self.log_callback:
|
|
198
|
+
self.log_callback(f"滑动: ({start_x}, {start_y}) -> ({end_x}, {end_y})")
|
|
199
|
+
|
|
200
|
+
event.Skip()
|
|
201
|
+
|
|
202
|
+
def on_motion(self, event):
|
|
203
|
+
if not self.is_dragging:
|
|
204
|
+
event.Skip()
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
event.Skip()
|
|
208
|
+
|
|
209
|
+
class DeviceMirrorFrame(wx.Frame):
|
|
210
|
+
def __init__(self, device: Device):
|
|
211
|
+
super().__init__(None, title="设备镜像", size=(800, 600))
|
|
212
|
+
|
|
213
|
+
# 创建分割窗口
|
|
214
|
+
self.splitter = wx.SplitterWindow(self)
|
|
215
|
+
|
|
216
|
+
# 创建左侧面板(包含控制区域和日志区域)
|
|
217
|
+
self.left_panel = wx.Panel(self.splitter)
|
|
218
|
+
left_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
219
|
+
|
|
220
|
+
# 控制区域
|
|
221
|
+
self.control_panel = wx.Panel(self.left_panel)
|
|
222
|
+
self.init_control_panel()
|
|
223
|
+
left_sizer.Add(self.control_panel, 0, wx.EXPAND | wx.ALL, 5)
|
|
224
|
+
|
|
225
|
+
# 日志区域
|
|
226
|
+
self.log_text = wx.TextCtrl(self.left_panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
|
|
227
|
+
self.log_text.SetBackgroundColour(wx.BLACK)
|
|
228
|
+
self.log_text.SetForegroundColour(wx.GREEN)
|
|
229
|
+
self.log_text.SetFont(wx.Font(9, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
|
|
230
|
+
left_sizer.Add(self.log_text, 1, wx.EXPAND | wx.ALL, 5)
|
|
231
|
+
|
|
232
|
+
self.left_panel.SetSizer(left_sizer)
|
|
233
|
+
|
|
234
|
+
# 创建设备画面
|
|
235
|
+
self.device_panel = DeviceMirrorPanel(self.splitter, device, self.log)
|
|
236
|
+
|
|
237
|
+
# 设置分割
|
|
238
|
+
self.splitter.SplitVertically(self.left_panel, self.device_panel)
|
|
239
|
+
self.splitter.SetMinimumPaneSize(200)
|
|
240
|
+
|
|
241
|
+
# 保存设备引用
|
|
242
|
+
self.device = device
|
|
243
|
+
|
|
244
|
+
def log(self, message: str):
|
|
245
|
+
"""添加日志"""
|
|
246
|
+
timestamp = time.strftime("%H:%M:%S", time.localtime())
|
|
247
|
+
wx.CallAfter(self.log_text.AppendText, f"[{timestamp}] {message}\n")
|
|
248
|
+
|
|
249
|
+
def init_control_panel(self):
|
|
250
|
+
vbox = wx.BoxSizer(wx.VERTICAL)
|
|
251
|
+
|
|
252
|
+
# 添加控制按钮
|
|
253
|
+
btn_get_resolution = wx.Button(self.control_panel, label="获取分辨率")
|
|
254
|
+
btn_get_resolution.Bind(wx.EVT_BUTTON, self.on_get_resolution)
|
|
255
|
+
vbox.Add(btn_get_resolution, 0, wx.EXPAND | wx.ALL, 5)
|
|
256
|
+
|
|
257
|
+
btn_get_orientation = wx.Button(self.control_panel, label="获取设备方向")
|
|
258
|
+
btn_get_orientation.Bind(wx.EVT_BUTTON, self.on_get_orientation)
|
|
259
|
+
vbox.Add(btn_get_orientation, 0, wx.EXPAND | wx.ALL, 5)
|
|
260
|
+
|
|
261
|
+
# 启动APP区域
|
|
262
|
+
hbox = wx.BoxSizer(wx.HORIZONTAL)
|
|
263
|
+
self.package_input = wx.TextCtrl(self.control_panel)
|
|
264
|
+
hbox.Add(self.package_input, 1, wx.EXPAND | wx.RIGHT, 5)
|
|
265
|
+
btn_launch_app = wx.Button(self.control_panel, label="启动APP")
|
|
266
|
+
btn_launch_app.Bind(wx.EVT_BUTTON, self.on_launch_app)
|
|
267
|
+
hbox.Add(btn_launch_app, 0)
|
|
268
|
+
vbox.Add(hbox, 0, wx.EXPAND | wx.ALL, 5)
|
|
269
|
+
|
|
270
|
+
btn_get_current_app = wx.Button(self.control_panel, label="获取前台APP")
|
|
271
|
+
btn_get_current_app.Bind(wx.EVT_BUTTON, self.on_get_current_app)
|
|
272
|
+
vbox.Add(btn_get_current_app, 0, wx.EXPAND | wx.ALL, 5)
|
|
273
|
+
|
|
274
|
+
self.control_panel.SetSizer(vbox)
|
|
275
|
+
|
|
276
|
+
def on_get_resolution(self, event):
|
|
277
|
+
"""获取分辨率"""
|
|
278
|
+
try:
|
|
279
|
+
width, height = self.device.screen_size
|
|
280
|
+
self.log(f"设备分辨率: {width}x{height}")
|
|
281
|
+
except Exception as e:
|
|
282
|
+
self.log(f"获取分辨率失败: {e}")
|
|
283
|
+
|
|
284
|
+
def on_get_orientation(self, event):
|
|
285
|
+
"""获取设备方向"""
|
|
286
|
+
try:
|
|
287
|
+
orientation = self.device.detect_orientation()
|
|
288
|
+
orientation_text = "横屏" if orientation == "landscape" else "竖屏"
|
|
289
|
+
self.log(f"设备方向: {orientation_text}")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
self.log(f"获取设备方向失败: {e}")
|
|
292
|
+
|
|
293
|
+
def on_launch_app(self, event):
|
|
294
|
+
"""启动APP"""
|
|
295
|
+
package_name = self.package_input.GetValue().strip()
|
|
296
|
+
if not package_name:
|
|
297
|
+
self.log("请输入包名")
|
|
298
|
+
return
|
|
299
|
+
try:
|
|
300
|
+
# 使用新的 API 通过 commands 属性访问平台特定方法
|
|
301
|
+
if hasattr(self.device, 'commands') and hasattr(self.device.commands, 'launch_app'):
|
|
302
|
+
self.device.commands.launch_app(package_name)
|
|
303
|
+
self.log(f"启动APP: {package_name}")
|
|
304
|
+
else:
|
|
305
|
+
self.log("当前设备不支持启动APP功能")
|
|
306
|
+
except Exception as e:
|
|
307
|
+
self.log(f"启动APP失败: {e}")
|
|
308
|
+
|
|
309
|
+
def on_get_current_app(self, event):
|
|
310
|
+
"""获取前台APP"""
|
|
311
|
+
try:
|
|
312
|
+
# 使用新的 API 通过 commands 属性访问平台特定方法
|
|
313
|
+
if hasattr(self.device, 'commands') and hasattr(self.device.commands, 'current_package'):
|
|
314
|
+
package = self.device.commands.current_package()
|
|
315
|
+
if package:
|
|
316
|
+
self.log(f"前台APP: {package}")
|
|
317
|
+
else:
|
|
318
|
+
self.log("未获取到前台APP")
|
|
319
|
+
else:
|
|
320
|
+
self.log("当前设备不支持获取前台APP功能")
|
|
321
|
+
except Exception as e:
|
|
322
|
+
self.log(f"获取前台APP失败: {e}")
|
|
323
|
+
|
|
324
|
+
def on_quit(self, event):
|
|
325
|
+
self.device_panel.is_running = False
|
|
326
|
+
self.Close()
|
|
327
|
+
|
|
328
|
+
def show_device_mirror(device: Device):
|
|
329
|
+
"""显示设备镜像窗口"""
|
|
330
|
+
app = wx.App()
|
|
331
|
+
frame = DeviceMirrorFrame(device)
|
|
332
|
+
frame.Show()
|
|
333
|
+
app.MainLoop()
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
# 测试代码
|
|
337
|
+
from kotonebot.client.device import AndroidDevice
|
|
338
|
+
from kotonebot.client.implements.adb import AdbImpl
|
|
339
|
+
from kotonebot.client.implements.uiautomator2 import UiAutomator2Impl
|
|
340
|
+
from adbutils import adb
|
|
341
|
+
|
|
342
|
+
print("server version:", adb.server_version())
|
|
343
|
+
adb.connect("127.0.0.1:5555")
|
|
344
|
+
print("devices:", adb.device_list())
|
|
345
|
+
d = adb.device_list()[-1]
|
|
346
|
+
|
|
347
|
+
# 使用新的 API
|
|
348
|
+
dd = AndroidDevice(d)
|
|
349
|
+
adb_imp = AdbImpl(d) # 直接传入 adb 连接
|
|
350
|
+
dd._touch = adb_imp
|
|
351
|
+
dd._screenshot = UiAutomator2Impl(dd) # UiAutomator2Impl 可能还需要 device 对象
|
|
352
|
+
dd.commands = adb_imp # 设置 Android 特定命令
|
|
353
|
+
|
|
354
|
+
show_device_mirror(dd)
|
kotonebot/ui/__init__.py
ADDED
|
File without changes
|