kotonebot 0.5.0__py3-none-any.whl → 0.7.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 +39 -39
- kotonebot/backend/bot.py +312 -312
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/context/task_action.py +183 -183
- kotonebot/backend/core.py +86 -129
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +535 -535
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +369 -529
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -43
- kotonebot/client/host/adb_common.py +101 -107
- kotonebot/client/host/custom.py +118 -118
- kotonebot/client/host/leidian_host.py +196 -196
- kotonebot/client/host/mumu12_host.py +353 -353
- kotonebot/client/host/protocol.py +214 -214
- kotonebot/client/host/windows_common.py +73 -58
- kotonebot/client/implements/__init__.py +65 -70
- kotonebot/client/implements/adb.py +89 -89
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -188
- kotonebot/client/implements/uiautomator2.py +85 -85
- kotonebot/client/implements/windows/__init__.py +1 -0
- kotonebot/client/implements/windows/print_window.py +133 -0
- kotonebot/client/implements/windows/send_message.py +324 -0
- kotonebot/client/implements/{windows.py → windows/windows.py} +175 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +103 -96
- kotonebot/config/config.py +61 -0
- kotonebot/config/manager.py +36 -36
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/{tools → devtools}/mirror.py +354 -354
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +76 -76
- kotonebot/interop/win/__init__.py +13 -9
- kotonebot/interop/win/_mouse.py +310 -310
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/interop/win/window.py +89 -0
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +19 -17
- kotonebot/primitives/geometry.py +1067 -862
- kotonebot/primitives/visual.py +143 -63
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -88
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -54
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/METADATA +84 -82
- kotonebot-0.7.0.dist-info/RECORD +109 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/WHEEL +1 -1
- kotonebot-0.7.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot/client/implements/adb_raw.py +0 -163
- kotonebot-0.5.0.dist-info/RECORD +0 -71
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
# ruff: noqa: E402
|
|
2
|
-
from kotonebot.util import require_windows
|
|
3
|
-
require_windows('"RemoteWindowsImpl" implementation')
|
|
4
|
-
|
|
5
|
-
import io
|
|
6
|
-
import base64
|
|
7
|
-
import logging
|
|
8
|
-
import xmlrpc.client
|
|
9
|
-
import xmlrpc.server
|
|
10
|
-
from typing import Literal, cast, Any, Tuple
|
|
11
|
-
from functools import cached_property
|
|
12
|
-
from threading import Thread
|
|
13
|
-
from dataclasses import dataclass
|
|
14
|
-
|
|
15
|
-
import cv2
|
|
16
|
-
import numpy as np
|
|
17
|
-
from cv2.typing import MatLike
|
|
18
|
-
|
|
19
|
-
from kotonebot import logging
|
|
20
|
-
from ..device import Device, WindowsDevice
|
|
21
|
-
from ..protocol import Touchable, Screenshotable
|
|
22
|
-
from ..registration import ImplConfig
|
|
23
|
-
from .windows import WindowsImpl, WindowsImplConfig
|
|
24
|
-
|
|
25
|
-
logger = logging.getLogger(__name__)
|
|
26
|
-
|
|
27
|
-
# 定义配置模型
|
|
28
|
-
@dataclass
|
|
29
|
-
class RemoteWindowsImplConfig(ImplConfig):
|
|
30
|
-
windows_impl_config: WindowsImplConfig
|
|
31
|
-
host: str = "localhost"
|
|
32
|
-
port: int = 8000
|
|
33
|
-
|
|
34
|
-
def _encode_image(image: MatLike) -> str:
|
|
35
|
-
"""Encode an image as a base64 string."""
|
|
36
|
-
success, buffer = cv2.imencode('.png', image)
|
|
37
|
-
if not success:
|
|
38
|
-
raise RuntimeError("Failed to encode image")
|
|
39
|
-
return base64.b64encode(buffer.tobytes()).decode('ascii')
|
|
40
|
-
|
|
41
|
-
def _decode_image(encoded_image: str) -> MatLike:
|
|
42
|
-
"""Decode a base64 string to an image."""
|
|
43
|
-
buffer = base64.b64decode(encoded_image)
|
|
44
|
-
image = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR)
|
|
45
|
-
if image is None:
|
|
46
|
-
raise RuntimeError("Failed to decode image")
|
|
47
|
-
return image
|
|
48
|
-
|
|
49
|
-
class RemoteWindowsServer:
|
|
50
|
-
"""
|
|
51
|
-
XML-RPC server that exposes a WindowsImpl instance.
|
|
52
|
-
|
|
53
|
-
This class wraps a WindowsImpl instance and exposes its methods via XML-RPC.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def __init__(self, windows_impl_config: WindowsImplConfig, host="localhost", port=8000):
|
|
57
|
-
"""Initialize the server with the given host and port."""
|
|
58
|
-
self.host = host
|
|
59
|
-
self.port = port
|
|
60
|
-
self.server = None
|
|
61
|
-
self.device = WindowsDevice()
|
|
62
|
-
self.impl = WindowsImpl(
|
|
63
|
-
WindowsDevice(),
|
|
64
|
-
ahk_exe_path=windows_impl_config.ahk_exe_path,
|
|
65
|
-
window_title=windows_impl_config.window_title
|
|
66
|
-
)
|
|
67
|
-
self.device._screenshot = self.impl
|
|
68
|
-
self.device._touch = self.impl
|
|
69
|
-
|
|
70
|
-
def start(self):
|
|
71
|
-
"""Start the XML-RPC server."""
|
|
72
|
-
self.server = xmlrpc.server.SimpleXMLRPCServer(
|
|
73
|
-
(self.host, self.port),
|
|
74
|
-
logRequests=True,
|
|
75
|
-
allow_none=True
|
|
76
|
-
)
|
|
77
|
-
self.server.register_instance(self)
|
|
78
|
-
logger.info(f"Starting RemoteWindowsServer on {self.host}:{self.port}")
|
|
79
|
-
self.server.serve_forever()
|
|
80
|
-
|
|
81
|
-
def start_in_thread(self):
|
|
82
|
-
"""Start the XML-RPC server in a separate thread."""
|
|
83
|
-
thread = Thread(target=self.start, daemon=True)
|
|
84
|
-
thread.start()
|
|
85
|
-
return thread
|
|
86
|
-
|
|
87
|
-
# Screenshotable methods
|
|
88
|
-
|
|
89
|
-
def screenshot(self) -> str:
|
|
90
|
-
"""Take a screenshot and return it as a base64-encoded string."""
|
|
91
|
-
try:
|
|
92
|
-
image = self.impl.screenshot()
|
|
93
|
-
return _encode_image(image)
|
|
94
|
-
except Exception as e:
|
|
95
|
-
logger.error(f"Error taking screenshot: {e}")
|
|
96
|
-
raise
|
|
97
|
-
|
|
98
|
-
def get_screen_size(self) -> tuple[int, int]:
|
|
99
|
-
"""Get the screen size."""
|
|
100
|
-
return self.impl.screen_size
|
|
101
|
-
|
|
102
|
-
def detect_orientation(self) -> str | None:
|
|
103
|
-
"""Detect the screen orientation."""
|
|
104
|
-
return self.impl.detect_orientation()
|
|
105
|
-
|
|
106
|
-
# Touchable methods
|
|
107
|
-
|
|
108
|
-
def click(self, x: int, y: int) -> bool:
|
|
109
|
-
"""Click at the given coordinates."""
|
|
110
|
-
try:
|
|
111
|
-
self.impl.click(x, y)
|
|
112
|
-
return True
|
|
113
|
-
except Exception as e:
|
|
114
|
-
logger.error(f"Error clicking at ({x}, {y}): {e}")
|
|
115
|
-
return False
|
|
116
|
-
|
|
117
|
-
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> bool:
|
|
118
|
-
"""Swipe from (x1, y1) to (x2, y2)."""
|
|
119
|
-
try:
|
|
120
|
-
self.impl.swipe(x1, y1, x2, y2, duration)
|
|
121
|
-
return True
|
|
122
|
-
except Exception as e:
|
|
123
|
-
logger.error(f"Error swiping from ({x1}, {y1}) to ({x2}, {y2}): {e}")
|
|
124
|
-
return False
|
|
125
|
-
|
|
126
|
-
# Other methods
|
|
127
|
-
|
|
128
|
-
def get_scale_ratio(self) -> float:
|
|
129
|
-
"""Get the scale ratio."""
|
|
130
|
-
return self.impl.scale_ratio
|
|
131
|
-
|
|
132
|
-
def ping(self) -> bool:
|
|
133
|
-
"""Check if the server is alive."""
|
|
134
|
-
return True
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
class RemoteWindowsImpl(Touchable, Screenshotable):
|
|
138
|
-
"""
|
|
139
|
-
Client implementation that connects to a remote Windows machine via XML-RPC.
|
|
140
|
-
|
|
141
|
-
This class implements the same interfaces as WindowsImpl but forwards all
|
|
142
|
-
method calls to a remote server.
|
|
143
|
-
"""
|
|
144
|
-
|
|
145
|
-
def __init__(self, device: Device, host="localhost", port=8000):
|
|
146
|
-
"""Initialize the client with the given device, host, and port."""
|
|
147
|
-
self.device = device
|
|
148
|
-
self.host = host
|
|
149
|
-
self.port = port
|
|
150
|
-
self.proxy = xmlrpc.client.ServerProxy(
|
|
151
|
-
f"http://{host}:{port}/",
|
|
152
|
-
allow_none=True
|
|
153
|
-
)
|
|
154
|
-
# Test connection
|
|
155
|
-
try:
|
|
156
|
-
if not self.proxy.ping():
|
|
157
|
-
raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}")
|
|
158
|
-
logger.info(f"Connected to RemoteWindowsServer at {host}:{port}")
|
|
159
|
-
except Exception as e:
|
|
160
|
-
raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}: {e}")
|
|
161
|
-
|
|
162
|
-
@cached_property
|
|
163
|
-
def scale_ratio(self) -> float:
|
|
164
|
-
"""Get the scale ratio from the remote server."""
|
|
165
|
-
return cast(float, self.proxy.get_scale_ratio())
|
|
166
|
-
|
|
167
|
-
@property
|
|
168
|
-
def screen_size(self) -> tuple[int, int]:
|
|
169
|
-
"""Get the screen size from the remote server."""
|
|
170
|
-
return cast(Tuple[int, int], self.proxy.get_screen_size())
|
|
171
|
-
|
|
172
|
-
def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
|
|
173
|
-
"""Detect the screen orientation from the remote server."""
|
|
174
|
-
return cast(None | Literal['portrait'] | Literal['landscape'], self.proxy.detect_orientation())
|
|
175
|
-
|
|
176
|
-
def screenshot(self) -> MatLike:
|
|
177
|
-
"""Take a screenshot from the remote server."""
|
|
178
|
-
encoded_image = cast(str, self.proxy.screenshot())
|
|
179
|
-
return _decode_image(encoded_image)
|
|
180
|
-
|
|
181
|
-
def click(self, x: int, y: int) -> None:
|
|
182
|
-
"""Click at the given coordinates on the remote server."""
|
|
183
|
-
if not self.proxy.click(x, y):
|
|
184
|
-
raise RuntimeError(f"Failed to click at ({x}, {y})")
|
|
185
|
-
|
|
186
|
-
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
|
|
187
|
-
"""Swipe from (x1, y1) to (x2, y2) on the remote server."""
|
|
188
|
-
if not self.proxy.swipe(x1, y1, x2, y2, duration):
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
from kotonebot.util import require_windows
|
|
3
|
+
require_windows('"RemoteWindowsImpl" implementation')
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import base64
|
|
7
|
+
import logging
|
|
8
|
+
import xmlrpc.client
|
|
9
|
+
import xmlrpc.server
|
|
10
|
+
from typing import Literal, cast, Any, Tuple
|
|
11
|
+
from functools import cached_property
|
|
12
|
+
from threading import Thread
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
import cv2
|
|
16
|
+
import numpy as np
|
|
17
|
+
from cv2.typing import MatLike
|
|
18
|
+
|
|
19
|
+
from kotonebot import logging
|
|
20
|
+
from ..device import Device, WindowsDevice
|
|
21
|
+
from ..protocol import Touchable, Screenshotable
|
|
22
|
+
from ..registration import ImplConfig
|
|
23
|
+
from .windows import WindowsImpl, WindowsImplConfig
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# 定义配置模型
|
|
28
|
+
@dataclass
|
|
29
|
+
class RemoteWindowsImplConfig(ImplConfig):
|
|
30
|
+
windows_impl_config: WindowsImplConfig
|
|
31
|
+
host: str = "localhost"
|
|
32
|
+
port: int = 8000
|
|
33
|
+
|
|
34
|
+
def _encode_image(image: MatLike) -> str:
|
|
35
|
+
"""Encode an image as a base64 string."""
|
|
36
|
+
success, buffer = cv2.imencode('.png', image)
|
|
37
|
+
if not success:
|
|
38
|
+
raise RuntimeError("Failed to encode image")
|
|
39
|
+
return base64.b64encode(buffer.tobytes()).decode('ascii')
|
|
40
|
+
|
|
41
|
+
def _decode_image(encoded_image: str) -> MatLike:
|
|
42
|
+
"""Decode a base64 string to an image."""
|
|
43
|
+
buffer = base64.b64decode(encoded_image)
|
|
44
|
+
image = cv2.imdecode(np.frombuffer(buffer, np.uint8), cv2.IMREAD_COLOR)
|
|
45
|
+
if image is None:
|
|
46
|
+
raise RuntimeError("Failed to decode image")
|
|
47
|
+
return image
|
|
48
|
+
|
|
49
|
+
class RemoteWindowsServer:
|
|
50
|
+
"""
|
|
51
|
+
XML-RPC server that exposes a WindowsImpl instance.
|
|
52
|
+
|
|
53
|
+
This class wraps a WindowsImpl instance and exposes its methods via XML-RPC.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, windows_impl_config: WindowsImplConfig, host="localhost", port=8000):
|
|
57
|
+
"""Initialize the server with the given host and port."""
|
|
58
|
+
self.host = host
|
|
59
|
+
self.port = port
|
|
60
|
+
self.server = None
|
|
61
|
+
self.device = WindowsDevice()
|
|
62
|
+
self.impl = WindowsImpl(
|
|
63
|
+
WindowsDevice(),
|
|
64
|
+
ahk_exe_path=windows_impl_config.ahk_exe_path,
|
|
65
|
+
window_title=windows_impl_config.window_title
|
|
66
|
+
)
|
|
67
|
+
self.device._screenshot = self.impl
|
|
68
|
+
self.device._touch = self.impl
|
|
69
|
+
|
|
70
|
+
def start(self):
|
|
71
|
+
"""Start the XML-RPC server."""
|
|
72
|
+
self.server = xmlrpc.server.SimpleXMLRPCServer(
|
|
73
|
+
(self.host, self.port),
|
|
74
|
+
logRequests=True,
|
|
75
|
+
allow_none=True
|
|
76
|
+
)
|
|
77
|
+
self.server.register_instance(self)
|
|
78
|
+
logger.info(f"Starting RemoteWindowsServer on {self.host}:{self.port}")
|
|
79
|
+
self.server.serve_forever()
|
|
80
|
+
|
|
81
|
+
def start_in_thread(self):
|
|
82
|
+
"""Start the XML-RPC server in a separate thread."""
|
|
83
|
+
thread = Thread(target=self.start, daemon=True)
|
|
84
|
+
thread.start()
|
|
85
|
+
return thread
|
|
86
|
+
|
|
87
|
+
# Screenshotable methods
|
|
88
|
+
|
|
89
|
+
def screenshot(self) -> str:
|
|
90
|
+
"""Take a screenshot and return it as a base64-encoded string."""
|
|
91
|
+
try:
|
|
92
|
+
image = self.impl.screenshot()
|
|
93
|
+
return _encode_image(image)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Error taking screenshot: {e}")
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
def get_screen_size(self) -> tuple[int, int]:
|
|
99
|
+
"""Get the screen size."""
|
|
100
|
+
return self.impl.screen_size
|
|
101
|
+
|
|
102
|
+
def detect_orientation(self) -> str | None:
|
|
103
|
+
"""Detect the screen orientation."""
|
|
104
|
+
return self.impl.detect_orientation()
|
|
105
|
+
|
|
106
|
+
# Touchable methods
|
|
107
|
+
|
|
108
|
+
def click(self, x: int, y: int) -> bool:
|
|
109
|
+
"""Click at the given coordinates."""
|
|
110
|
+
try:
|
|
111
|
+
self.impl.click(x, y)
|
|
112
|
+
return True
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Error clicking at ({x}, {y}): {e}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> bool:
|
|
118
|
+
"""Swipe from (x1, y1) to (x2, y2)."""
|
|
119
|
+
try:
|
|
120
|
+
self.impl.swipe(x1, y1, x2, y2, duration)
|
|
121
|
+
return True
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(f"Error swiping from ({x1}, {y1}) to ({x2}, {y2}): {e}")
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
# Other methods
|
|
127
|
+
|
|
128
|
+
def get_scale_ratio(self) -> float:
|
|
129
|
+
"""Get the scale ratio."""
|
|
130
|
+
return self.impl.scale_ratio
|
|
131
|
+
|
|
132
|
+
def ping(self) -> bool:
|
|
133
|
+
"""Check if the server is alive."""
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class RemoteWindowsImpl(Touchable, Screenshotable):
|
|
138
|
+
"""
|
|
139
|
+
Client implementation that connects to a remote Windows machine via XML-RPC.
|
|
140
|
+
|
|
141
|
+
This class implements the same interfaces as WindowsImpl but forwards all
|
|
142
|
+
method calls to a remote server.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, device: Device, host="localhost", port=8000):
|
|
146
|
+
"""Initialize the client with the given device, host, and port."""
|
|
147
|
+
self.device = device
|
|
148
|
+
self.host = host
|
|
149
|
+
self.port = port
|
|
150
|
+
self.proxy = xmlrpc.client.ServerProxy(
|
|
151
|
+
f"http://{host}:{port}/",
|
|
152
|
+
allow_none=True
|
|
153
|
+
)
|
|
154
|
+
# Test connection
|
|
155
|
+
try:
|
|
156
|
+
if not self.proxy.ping():
|
|
157
|
+
raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}")
|
|
158
|
+
logger.info(f"Connected to RemoteWindowsServer at {host}:{port}")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise ConnectionError(f"Failed to connect to RemoteWindowsServer at {host}:{port}: {e}")
|
|
161
|
+
|
|
162
|
+
@cached_property
|
|
163
|
+
def scale_ratio(self) -> float:
|
|
164
|
+
"""Get the scale ratio from the remote server."""
|
|
165
|
+
return cast(float, self.proxy.get_scale_ratio())
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def screen_size(self) -> tuple[int, int]:
|
|
169
|
+
"""Get the screen size from the remote server."""
|
|
170
|
+
return cast(Tuple[int, int], self.proxy.get_screen_size())
|
|
171
|
+
|
|
172
|
+
def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
|
|
173
|
+
"""Detect the screen orientation from the remote server."""
|
|
174
|
+
return cast(None | Literal['portrait'] | Literal['landscape'], self.proxy.detect_orientation())
|
|
175
|
+
|
|
176
|
+
def screenshot(self) -> MatLike:
|
|
177
|
+
"""Take a screenshot from the remote server."""
|
|
178
|
+
encoded_image = cast(str, self.proxy.screenshot())
|
|
179
|
+
return _decode_image(encoded_image)
|
|
180
|
+
|
|
181
|
+
def click(self, x: int, y: int) -> None:
|
|
182
|
+
"""Click at the given coordinates on the remote server."""
|
|
183
|
+
if not self.proxy.click(x, y):
|
|
184
|
+
raise RuntimeError(f"Failed to click at ({x}, {y})")
|
|
185
|
+
|
|
186
|
+
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
|
|
187
|
+
"""Swipe from (x1, y1) to (x2, y2) on the remote server."""
|
|
188
|
+
if not self.proxy.swipe(x1, y1, x2, y2, duration):
|
|
189
189
|
raise RuntimeError(f"Failed to swipe from ({x1}, {y1}) to ({x2}, {y2})")
|
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import time
|
|
2
|
-
from typing import Literal
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
try:
|
|
6
|
-
import uiautomator2 as u2
|
|
7
|
-
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
8
|
-
except ImportError as _e:
|
|
9
|
-
from kotonebot.errors import MissingDependencyError
|
|
10
|
-
raise MissingDependencyError(_e, 'android')
|
|
11
|
-
from cv2.typing import MatLike
|
|
12
|
-
|
|
13
|
-
from kotonebot import logging
|
|
14
|
-
from ..device import Device
|
|
15
|
-
from ..protocol import Screenshotable, Commandable, Touchable
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
SCREENSHOT_INTERVAL = 0.2
|
|
20
|
-
|
|
21
|
-
class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
|
|
22
|
-
def __init__(self, adb_connection: AdbUtilsDevice):
|
|
23
|
-
self.u2_client = u2.Device(adb_connection.serial)
|
|
24
|
-
self.__last_screenshot_time = 0
|
|
25
|
-
|
|
26
|
-
def screenshot(self) -> MatLike:
|
|
27
|
-
"""
|
|
28
|
-
截图
|
|
29
|
-
"""
|
|
30
|
-
from kotonebot import sleep
|
|
31
|
-
delta = time.time() - self.__last_screenshot_time
|
|
32
|
-
if delta < SCREENSHOT_INTERVAL:
|
|
33
|
-
time.sleep(SCREENSHOT_INTERVAL - delta)
|
|
34
|
-
start_time = time.time()
|
|
35
|
-
image = self.u2_client.screenshot(format='opencv')
|
|
36
|
-
logger.verbose(f'uiautomator2 screenshot: {time.time() - start_time}s')
|
|
37
|
-
self.__last_screenshot_time = time.time()
|
|
38
|
-
assert isinstance(image, np.ndarray)
|
|
39
|
-
return image
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def screen_size(self) -> tuple[int, int]:
|
|
43
|
-
info = self.u2_client.info
|
|
44
|
-
sizes = info['displayWidth'], info['displayHeight']
|
|
45
|
-
return sizes
|
|
46
|
-
|
|
47
|
-
def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
|
|
48
|
-
"""
|
|
49
|
-
检测设备方向
|
|
50
|
-
"""
|
|
51
|
-
orientation = self.u2_client.info['displayRotation']
|
|
52
|
-
if orientation == 1:
|
|
53
|
-
return 'portrait'
|
|
54
|
-
elif orientation == 0:
|
|
55
|
-
return 'landscape'
|
|
56
|
-
else:
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
def launch_app(self, package_name: str) -> None:
|
|
60
|
-
"""
|
|
61
|
-
启动应用
|
|
62
|
-
"""
|
|
63
|
-
self.u2_client.app_start(package_name)
|
|
64
|
-
|
|
65
|
-
def current_package(self) -> str | None:
|
|
66
|
-
"""
|
|
67
|
-
获取当前应用包名
|
|
68
|
-
"""
|
|
69
|
-
try:
|
|
70
|
-
result = self.u2_client.app_current()
|
|
71
|
-
logger.verbose(f'uiautomator2 current_package: {result}')
|
|
72
|
-
return result['package']
|
|
73
|
-
except:
|
|
74
|
-
return None
|
|
75
|
-
|
|
76
|
-
def click(self, x: int, y: int) -> None:
|
|
77
|
-
"""
|
|
78
|
-
点击屏幕
|
|
79
|
-
"""
|
|
80
|
-
self.u2_client.click(x, y)
|
|
81
|
-
|
|
82
|
-
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float|None = None) -> None:
|
|
83
|
-
"""
|
|
84
|
-
滑动屏幕
|
|
85
|
-
"""
|
|
1
|
+
import time
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
try:
|
|
6
|
+
import uiautomator2 as u2
|
|
7
|
+
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
8
|
+
except ImportError as _e:
|
|
9
|
+
from kotonebot.errors import MissingDependencyError
|
|
10
|
+
raise MissingDependencyError(_e, 'android')
|
|
11
|
+
from cv2.typing import MatLike
|
|
12
|
+
|
|
13
|
+
from kotonebot import logging
|
|
14
|
+
from ..device import Device
|
|
15
|
+
from ..protocol import Screenshotable, Commandable, Touchable
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
SCREENSHOT_INTERVAL = 0.2
|
|
20
|
+
|
|
21
|
+
class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
|
|
22
|
+
def __init__(self, adb_connection: AdbUtilsDevice):
|
|
23
|
+
self.u2_client = u2.Device(adb_connection.serial)
|
|
24
|
+
self.__last_screenshot_time = 0
|
|
25
|
+
|
|
26
|
+
def screenshot(self) -> MatLike:
|
|
27
|
+
"""
|
|
28
|
+
截图
|
|
29
|
+
"""
|
|
30
|
+
from kotonebot import sleep
|
|
31
|
+
delta = time.time() - self.__last_screenshot_time
|
|
32
|
+
if delta < SCREENSHOT_INTERVAL:
|
|
33
|
+
time.sleep(SCREENSHOT_INTERVAL - delta)
|
|
34
|
+
start_time = time.time()
|
|
35
|
+
image = self.u2_client.screenshot(format='opencv')
|
|
36
|
+
logger.verbose(f'uiautomator2 screenshot: {time.time() - start_time}s')
|
|
37
|
+
self.__last_screenshot_time = time.time()
|
|
38
|
+
assert isinstance(image, np.ndarray)
|
|
39
|
+
return image
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def screen_size(self) -> tuple[int, int]:
|
|
43
|
+
info = self.u2_client.info
|
|
44
|
+
sizes = info['displayWidth'], info['displayHeight']
|
|
45
|
+
return sizes
|
|
46
|
+
|
|
47
|
+
def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
|
|
48
|
+
"""
|
|
49
|
+
检测设备方向
|
|
50
|
+
"""
|
|
51
|
+
orientation = self.u2_client.info['displayRotation']
|
|
52
|
+
if orientation == 1:
|
|
53
|
+
return 'portrait'
|
|
54
|
+
elif orientation == 0:
|
|
55
|
+
return 'landscape'
|
|
56
|
+
else:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def launch_app(self, package_name: str) -> None:
|
|
60
|
+
"""
|
|
61
|
+
启动应用
|
|
62
|
+
"""
|
|
63
|
+
self.u2_client.app_start(package_name)
|
|
64
|
+
|
|
65
|
+
def current_package(self) -> str | None:
|
|
66
|
+
"""
|
|
67
|
+
获取当前应用包名
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
result = self.u2_client.app_current()
|
|
71
|
+
logger.verbose(f'uiautomator2 current_package: {result}')
|
|
72
|
+
return result['package']
|
|
73
|
+
except:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def click(self, x: int, y: int) -> None:
|
|
77
|
+
"""
|
|
78
|
+
点击屏幕
|
|
79
|
+
"""
|
|
80
|
+
self.u2_client.click(x, y)
|
|
81
|
+
|
|
82
|
+
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float|None = None) -> None:
|
|
83
|
+
"""
|
|
84
|
+
滑动屏幕
|
|
85
|
+
"""
|
|
86
86
|
self.u2_client.swipe(x1, y1, x2, y2, duration=duration or 0.1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .windows import *
|