kotonebot 0.3.1__py3-none-any.whl → 0.5.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.
Files changed (66) hide show
  1. kotonebot/__init__.py +39 -39
  2. kotonebot/backend/bot.py +312 -302
  3. kotonebot/backend/color.py +525 -525
  4. kotonebot/backend/context/__init__.py +3 -3
  5. kotonebot/backend/context/context.py +49 -56
  6. kotonebot/backend/context/task_action.py +183 -175
  7. kotonebot/backend/core.py +129 -126
  8. kotonebot/backend/debug/entry.py +89 -89
  9. kotonebot/backend/debug/mock.py +78 -78
  10. kotonebot/backend/debug/server.py +222 -222
  11. kotonebot/backend/debug/vars.py +351 -351
  12. kotonebot/backend/dispatch.py +227 -227
  13. kotonebot/backend/flow_controller.py +196 -196
  14. kotonebot/backend/loop.py +12 -88
  15. kotonebot/backend/ocr.py +535 -529
  16. kotonebot/backend/preprocessor.py +103 -103
  17. kotonebot/client/__init__.py +9 -9
  18. kotonebot/client/device.py +528 -502
  19. kotonebot/client/fast_screenshot.py +377 -377
  20. kotonebot/client/host/__init__.py +43 -12
  21. kotonebot/client/host/adb_common.py +107 -94
  22. kotonebot/client/host/custom.py +118 -114
  23. kotonebot/client/host/leidian_host.py +196 -201
  24. kotonebot/client/host/mumu12_host.py +353 -358
  25. kotonebot/client/host/protocol.py +214 -213
  26. kotonebot/client/host/windows_common.py +58 -55
  27. kotonebot/client/implements/__init__.py +71 -7
  28. kotonebot/client/implements/adb.py +89 -85
  29. kotonebot/client/implements/adb_raw.py +162 -158
  30. kotonebot/client/implements/nemu_ipc/__init__.py +11 -7
  31. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
  32. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
  33. kotonebot/client/implements/remote_windows.py +188 -192
  34. kotonebot/client/implements/uiautomator2.py +85 -81
  35. kotonebot/client/implements/windows.py +176 -168
  36. kotonebot/client/protocol.py +69 -69
  37. kotonebot/client/registration.py +24 -24
  38. kotonebot/config/base_config.py +96 -96
  39. kotonebot/config/manager.py +36 -36
  40. kotonebot/errors.py +76 -71
  41. kotonebot/interop/win/__init__.py +10 -0
  42. kotonebot/interop/win/_mouse.py +311 -0
  43. kotonebot/interop/win/message_box.py +313 -313
  44. kotonebot/interop/win/reg.py +37 -37
  45. kotonebot/interop/win/shortcut.py +43 -43
  46. kotonebot/interop/win/task_dialog.py +513 -469
  47. kotonebot/logging/__init__.py +2 -2
  48. kotonebot/logging/log.py +17 -17
  49. kotonebot/primitives/__init__.py +17 -17
  50. kotonebot/primitives/geometry.py +862 -290
  51. kotonebot/primitives/visual.py +63 -63
  52. kotonebot/tools/mirror.py +354 -354
  53. kotonebot/ui/file_host/sensio.py +36 -36
  54. kotonebot/ui/file_host/tmp_send.py +54 -54
  55. kotonebot/ui/pushkit/__init__.py +3 -3
  56. kotonebot/ui/pushkit/image_host.py +88 -87
  57. kotonebot/ui/pushkit/protocol.py +13 -13
  58. kotonebot/ui/pushkit/wxpusher.py +54 -53
  59. kotonebot/ui/user.py +148 -143
  60. kotonebot/util.py +436 -409
  61. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/METADATA +82 -76
  62. kotonebot-0.5.0.dist-info/RECORD +71 -0
  63. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/licenses/LICENSE +673 -673
  64. kotonebot-0.3.1.dist-info/RECORD +0 -70
  65. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/WHEEL +0 -0
  66. {kotonebot-0.3.1.dist-info → kotonebot-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,85 +1,89 @@
1
- import logging
2
- from typing import cast
3
- from typing_extensions import override
4
-
5
- import cv2
6
- import numpy as np
7
- from cv2.typing import MatLike
8
- from adbutils._device import AdbDevice as AdbUtilsDevice
9
-
10
- from ..device import AndroidDevice
11
- from ..protocol import AndroidCommandable, Touchable, Screenshotable
12
- from ..registration import ImplConfig
13
- from dataclasses import dataclass
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- # 定义配置模型
18
- @dataclass
19
- class AdbImplConfig(ImplConfig):
20
- addr: str
21
- connect: bool = True
22
- disconnect: bool = True
23
- device_serial: str | None = None
24
- timeout: float = 180
25
-
26
- class AdbImpl(AndroidCommandable, Touchable, Screenshotable):
27
- def __init__(self, adb_connection: AdbUtilsDevice):
28
- self.adb = adb_connection
29
-
30
- @override
31
- def launch_app(self, package_name: str) -> None:
32
- self.adb.shell(f"monkey -p {package_name} 1")
33
-
34
- @override
35
- def current_package(self) -> str | None:
36
- # https://blog.csdn.net/guangdeshishe/article/details/117154406
37
- result_text = self.adb.shell('dumpsys activity top | grep ACTIVITY | tail -n 1')
38
- logger.debug(f"adb returned: {result_text}")
39
- if not isinstance(result_text, str):
40
- logger.error(f"Invalid result_text: {result_text}")
41
- return None
42
- result_text = result_text.strip()
43
- if result_text == '':
44
- logger.error("No current package found")
45
- return None
46
- _, activity, *_ = result_text.split(' ')
47
- package = activity.split('/')[0]
48
- return package
49
-
50
- def adb_shell(self, cmd: str) -> str:
51
- """执行 ADB shell 命令"""
52
- return cast(str, self.adb.shell(cmd))
53
-
54
- @override
55
- def detect_orientation(self):
56
- # 判断方向:https://stackoverflow.com/questions/10040624/check-if-device-is-landscape-via-adb
57
- # 但是上面这种方法不准确
58
- # 因此这里直接通过截图判断方向
59
- img = self.screenshot()
60
- if img.shape[0] > img.shape[1]:
61
- return 'portrait'
62
- return 'landscape'
63
-
64
- @property
65
- def screen_size(self) -> tuple[int, int]:
66
- ret = cast(str, self.adb.shell("wm size")).strip('Physical size: ')
67
- spiltted = tuple(map(int, ret.split("x")))
68
- # 检测当前方向
69
- orientation = self.detect_orientation()
70
- landscape = orientation == 'landscape'
71
- spiltted = tuple(sorted(spiltted, reverse=landscape))
72
- if len(spiltted) != 2:
73
- raise ValueError(f"Invalid screen size: {ret}")
74
- return spiltted
75
-
76
- def screenshot(self) -> MatLike:
77
- return cv2.cvtColor(np.array(self.adb.screenshot()), cv2.COLOR_RGB2BGR)
78
-
79
- def click(self, x: int, y: int) -> None:
80
- self.adb.shell(f"input tap {x} {y}")
81
-
82
- def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
83
- if duration is not None:
84
- logger.warning("Swipe duration is not supported with AdbDevice. Ignoring duration.")
85
- self.adb.shell(f"input touchscreen swipe {x1} {y1} {x2} {y2}")
1
+ import logging
2
+ from typing import cast
3
+ from typing_extensions import override
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from cv2.typing import MatLike
8
+ try:
9
+ from adbutils._device import AdbDevice as AdbUtilsDevice
10
+ except ImportError as _e:
11
+ from kotonebot.errors import MissingDependencyError
12
+ raise MissingDependencyError(_e, 'android')
13
+
14
+ from ..device import AndroidDevice
15
+ from ..protocol import AndroidCommandable, Touchable, Screenshotable
16
+ from ..registration import ImplConfig
17
+ from dataclasses import dataclass
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # 定义配置模型
22
+ @dataclass
23
+ class AdbImplConfig(ImplConfig):
24
+ addr: str
25
+ connect: bool = True
26
+ disconnect: bool = True
27
+ device_serial: str | None = None
28
+ timeout: float = 180
29
+
30
+ class AdbImpl(AndroidCommandable, Touchable, Screenshotable):
31
+ def __init__(self, adb_connection: AdbUtilsDevice):
32
+ self.adb = adb_connection
33
+
34
+ @override
35
+ def launch_app(self, package_name: str) -> None:
36
+ self.adb.shell(f"monkey -p {package_name} 1")
37
+
38
+ @override
39
+ def current_package(self) -> str | None:
40
+ # https://blog.csdn.net/guangdeshishe/article/details/117154406
41
+ result_text = self.adb.shell('dumpsys activity top | grep ACTIVITY | tail -n 1')
42
+ logger.debug(f"adb returned: {result_text}")
43
+ if not isinstance(result_text, str):
44
+ logger.error(f"Invalid result_text: {result_text}")
45
+ return None
46
+ result_text = result_text.strip()
47
+ if result_text == '':
48
+ logger.error("No current package found")
49
+ return None
50
+ _, activity, *_ = result_text.split(' ')
51
+ package = activity.split('/')[0]
52
+ return package
53
+
54
+ def adb_shell(self, cmd: str) -> str:
55
+ """执行 ADB shell 命令"""
56
+ return cast(str, self.adb.shell(cmd))
57
+
58
+ @override
59
+ def detect_orientation(self):
60
+ # 判断方向:https://stackoverflow.com/questions/10040624/check-if-device-is-landscape-via-adb
61
+ # 但是上面这种方法不准确
62
+ # 因此这里直接通过截图判断方向
63
+ img = self.screenshot()
64
+ if img.shape[0] > img.shape[1]:
65
+ return 'portrait'
66
+ return 'landscape'
67
+
68
+ @property
69
+ def screen_size(self) -> tuple[int, int]:
70
+ ret = cast(str, self.adb.shell("wm size")).strip('Physical size: ')
71
+ spiltted = tuple(map(int, ret.split("x")))
72
+ # 检测当前方向
73
+ orientation = self.detect_orientation()
74
+ landscape = orientation == 'landscape'
75
+ spiltted = tuple(sorted(spiltted, reverse=landscape))
76
+ if len(spiltted) != 2:
77
+ raise ValueError(f"Invalid screen size: {ret}")
78
+ return spiltted
79
+
80
+ def screenshot(self) -> MatLike:
81
+ return cv2.cvtColor(np.array(self.adb.screenshot()), cv2.COLOR_RGB2BGR)
82
+
83
+ def click(self, x: int, y: int) -> None:
84
+ self.adb.shell(f"input tap {x} {y}")
85
+
86
+ def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
87
+ if duration is not None:
88
+ logger.warning("Swipe duration is not supported with AdbDevice. Ignoring duration.")
89
+ self.adb.shell(f"input touchscreen swipe {x1} {y1} {x2} {y2}")
@@ -1,159 +1,163 @@
1
- import os
2
- import time
3
- import subprocess
4
- import struct
5
- from threading import Thread, Lock
6
- from functools import cached_property
7
- from typing_extensions import override
8
-
9
- import cv2
10
- import numpy as np
11
- from cv2.typing import MatLike
12
- from adbutils._utils import adb_path
13
-
14
- from .adb import AdbImpl
15
- from adbutils._device import AdbDevice as AdbUtilsDevice
16
- from kotonebot import logging
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
- WAIT_TIMEOUT = 10
21
- MAX_RETRY_COUNT = 5
22
- SCRIPT: str = """#!/bin/sh
23
- while true; do
24
- screencap
25
- sleep 0.3
26
- done
27
- """
28
-
29
- class AdbRawImpl(AdbImpl):
30
- def __init__(self, adb_connection: AdbUtilsDevice):
31
- super().__init__(adb_connection)
32
- self.__worker: Thread | None = None
33
- self.__process: subprocess.Popen | None = None
34
- self.__data: MatLike | None = None
35
- self.__retry_count = 0
36
- self.__lock = Lock()
37
- self.__stopping = False
38
-
39
- def __cleanup_worker(self) -> None:
40
- if self.__process:
41
- try:
42
- self.__process.kill()
43
- except:
44
- pass
45
- self.__process = None
46
- if self.__worker:
47
- try:
48
- self.__worker.join()
49
- except:
50
- pass
51
- self.__worker = None
52
- self.__data = None
53
-
54
- def __start_worker(self) -> None:
55
- self.__stopping = True
56
- self.__cleanup_worker()
57
- self.__stopping = False
58
- self.__worker = Thread(target=self.__worker_thread_with_retry, daemon=True)
59
- self.__worker.start()
60
-
61
- def __worker_thread_with_retry(self) -> None:
62
- try:
63
- self.__worker_thread()
64
- except Exception as e:
65
- logger.error(f"Worker thread failed: {e}")
66
- with self.__lock:
67
- self.__retry_count += 1
68
- raise
69
-
70
- def __worker_thread(self) -> None:
71
- with open('screenshot.sh', 'w', encoding='utf-8', newline='\n') as f:
72
- f.write(SCRIPT)
73
- self.adb.push('screenshot.sh', '/data/local/tmp/screenshot.sh')
74
- self.adb.shell(f'chmod 755 /data/local/tmp/screenshot.sh')
75
- os.remove('screenshot.sh')
76
-
77
- cmd = fr'{adb_path()} -s {self.adb.serial} exec-out "sh /data/local/tmp/screenshot.sh"'
78
- self.__process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
79
-
80
- while not self.__stopping and self.__process.poll() is None:
81
- if self.__process.stdout is None:
82
- logger.error("Failed to get stdout from process")
83
- continue
84
-
85
- # 解析 header
86
- # https://stackoverflow.com/questions/22034959/what-format-does-adb-screencap-sdcard-screenshot-raw-produce-without-p-f
87
- if self.__api_level >= 26:
88
- metadata = self.__process.stdout.read(16)
89
- w, h, p, c = struct.unpack('<IIII', metadata)
90
- # w=width, h=height, p=pixel_format, c=color_space
91
- # 详见:https://android.googlesource.com/platform/frameworks/base/+/26a2b97dbe48ee45e9ae70110714048f2f360f97/cmds/screencap/screencap.cpp#209
92
- else:
93
- metadata = self.__process.stdout.read(12)
94
- w, h, p = struct.unpack('<III', metadata)
95
- if p == 1: # PixelFormat.RGBA_8888
96
- channel = 4
97
- else:
98
- raise ValueError(f"Unsupported pixel format: {p}")
99
- data_size = w * h * channel
100
-
101
- if (data_size < 100 * 100 * 4) or (data_size > 3000 * 3000 * 4):
102
- raise ValueError(f"Invaild data_size: {w}x{h}.")
103
-
104
- # 读取图像数据
105
- # logger.verbose(f"receiving image data: {w}x{h} {data_size} bytes")
106
- image_data = self.__process.stdout.read(data_size)
107
- if not isinstance(image_data, bytes) or len(image_data) != data_size:
108
- logger.error(f"Failed to read image data, expected {data_size} bytes but got {len(image_data) if isinstance(image_data, bytes) else 'non-bytes'}")
109
- raise RuntimeError("Failed to read image data")
110
-
111
- np_data = np.frombuffer(image_data, np.uint8)
112
- np_data = np_data.reshape(h, w, channel)
113
- self.__data = cv2.cvtColor(np_data, cv2.COLOR_RGBA2BGR)
114
-
115
- @cached_property
116
- def __api_level(self) -> int:
117
- try:
118
- output = self.adb.shell("getprop ro.build.version.sdk")
119
- assert isinstance(output, str)
120
- return int(output.strip())
121
- except Exception as e:
122
- logger.error(f"Failed to get API level: {e}")
123
- return 0
124
-
125
- @override
126
- def screenshot(self) -> MatLike:
127
- with self.__lock:
128
- if self.__retry_count >= MAX_RETRY_COUNT:
129
- raise RuntimeError(f"Maximum retry count ({MAX_RETRY_COUNT}) exceeded")
130
-
131
- if not self.__worker or (self.__worker and not self.__worker.is_alive()):
132
- self.__start_worker()
133
-
134
- start_time = time.time()
135
- while self.__data is None:
136
- time.sleep(0.01)
137
- if time.time() - start_time > WAIT_TIMEOUT:
138
- logger.warning("Screenshot timeout, cleaning up and restarting worker...")
139
- with self.__lock:
140
- if self.__retry_count < MAX_RETRY_COUNT:
141
- self.__start_worker()
142
- start_time = time.time() # 重置超时计时器
143
- continue
144
- else:
145
- raise RuntimeError(f"Maximum retry count ({MAX_RETRY_COUNT}) exceeded")
146
-
147
- # 检查 worker 是否还活着
148
- if self.__worker and not self.__worker.is_alive():
149
- with self.__lock:
150
- if self.__retry_count < MAX_RETRY_COUNT:
151
- logger.warning("Worker thread died, restarting...")
152
- self.__start_worker()
153
- else:
154
- raise RuntimeError(f"Maximum retry count ({MAX_RETRY_COUNT}) exceeded")
155
-
156
- logger.verbose(f"adb raw screenshot wait time: {time.time() - start_time:.4f}s")
157
- data = self.__data
158
- self.__data = None
1
+ import os
2
+ import time
3
+ import subprocess
4
+ import struct
5
+ from threading import Thread, Lock
6
+ from functools import cached_property
7
+ from typing_extensions import override
8
+
9
+ import cv2
10
+ import numpy as np
11
+ from cv2.typing import MatLike
12
+ try:
13
+ from adbutils._utils import adb_path
14
+ from adbutils._device import AdbDevice as AdbUtilsDevice
15
+ except ImportError as _e:
16
+ from kotonebot.errors import MissingDependencyError
17
+ raise MissingDependencyError(_e, 'android')
18
+
19
+ from .adb import AdbImpl
20
+ from kotonebot import logging
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ WAIT_TIMEOUT = 10
25
+ MAX_RETRY_COUNT = 5
26
+ SCRIPT: str = """#!/bin/sh
27
+ while true; do
28
+ screencap
29
+ sleep 0.3
30
+ done
31
+ """
32
+
33
+ class AdbRawImpl(AdbImpl):
34
+ def __init__(self, adb_connection: AdbUtilsDevice):
35
+ super().__init__(adb_connection)
36
+ self.__worker: Thread | None = None
37
+ self.__process: subprocess.Popen | None = None
38
+ self.__data: MatLike | None = None
39
+ self.__retry_count = 0
40
+ self.__lock = Lock()
41
+ self.__stopping = False
42
+
43
+ def __cleanup_worker(self) -> None:
44
+ if self.__process:
45
+ try:
46
+ self.__process.kill()
47
+ except:
48
+ pass
49
+ self.__process = None
50
+ if self.__worker:
51
+ try:
52
+ self.__worker.join()
53
+ except:
54
+ pass
55
+ self.__worker = None
56
+ self.__data = None
57
+
58
+ def __start_worker(self) -> None:
59
+ self.__stopping = True
60
+ self.__cleanup_worker()
61
+ self.__stopping = False
62
+ self.__worker = Thread(target=self.__worker_thread_with_retry, daemon=True)
63
+ self.__worker.start()
64
+
65
+ def __worker_thread_with_retry(self) -> None:
66
+ try:
67
+ self.__worker_thread()
68
+ except Exception as e:
69
+ logger.error(f"Worker thread failed: {e}")
70
+ with self.__lock:
71
+ self.__retry_count += 1
72
+ raise
73
+
74
+ def __worker_thread(self) -> None:
75
+ with open('screenshot.sh', 'w', encoding='utf-8', newline='\n') as f:
76
+ f.write(SCRIPT)
77
+ self.adb.push('screenshot.sh', '/data/local/tmp/screenshot.sh')
78
+ self.adb.shell(f'chmod 755 /data/local/tmp/screenshot.sh')
79
+ os.remove('screenshot.sh')
80
+
81
+ cmd = fr'{adb_path()} -s {self.adb.serial} exec-out "sh /data/local/tmp/screenshot.sh"'
82
+ self.__process = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
83
+
84
+ while not self.__stopping and self.__process.poll() is None:
85
+ if self.__process.stdout is None:
86
+ logger.error("Failed to get stdout from process")
87
+ continue
88
+
89
+ # 解析 header
90
+ # https://stackoverflow.com/questions/22034959/what-format-does-adb-screencap-sdcard-screenshot-raw-produce-without-p-f
91
+ if self.__api_level >= 26:
92
+ metadata = self.__process.stdout.read(16)
93
+ w, h, p, c = struct.unpack('<IIII', metadata)
94
+ # w=width, h=height, p=pixel_format, c=color_space
95
+ # 详见:https://android.googlesource.com/platform/frameworks/base/+/26a2b97dbe48ee45e9ae70110714048f2f360f97/cmds/screencap/screencap.cpp#209
96
+ else:
97
+ metadata = self.__process.stdout.read(12)
98
+ w, h, p = struct.unpack('<III', metadata)
99
+ if p == 1: # PixelFormat.RGBA_8888
100
+ channel = 4
101
+ else:
102
+ raise ValueError(f"Unsupported pixel format: {p}")
103
+ data_size = w * h * channel
104
+
105
+ if (data_size < 100 * 100 * 4) or (data_size > 3000 * 3000 * 4):
106
+ raise ValueError(f"Invaild data_size: {w}x{h}.")
107
+
108
+ # 读取图像数据
109
+ # logger.verbose(f"receiving image data: {w}x{h} {data_size} bytes")
110
+ image_data = self.__process.stdout.read(data_size)
111
+ if not isinstance(image_data, bytes) or len(image_data) != data_size:
112
+ logger.error(f"Failed to read image data, expected {data_size} bytes but got {len(image_data) if isinstance(image_data, bytes) else 'non-bytes'}")
113
+ raise RuntimeError("Failed to read image data")
114
+
115
+ np_data = np.frombuffer(image_data, np.uint8)
116
+ np_data = np_data.reshape(h, w, channel)
117
+ self.__data = cv2.cvtColor(np_data, cv2.COLOR_RGBA2BGR)
118
+
119
+ @cached_property
120
+ def __api_level(self) -> int:
121
+ try:
122
+ output = self.adb.shell("getprop ro.build.version.sdk")
123
+ assert isinstance(output, str)
124
+ return int(output.strip())
125
+ except Exception as e:
126
+ logger.error(f"Failed to get API level: {e}")
127
+ return 0
128
+
129
+ @override
130
+ def screenshot(self) -> MatLike:
131
+ with self.__lock:
132
+ if self.__retry_count >= MAX_RETRY_COUNT:
133
+ raise RuntimeError(f"Maximum retry count ({MAX_RETRY_COUNT}) exceeded")
134
+
135
+ if not self.__worker or (self.__worker and not self.__worker.is_alive()):
136
+ self.__start_worker()
137
+
138
+ start_time = time.time()
139
+ while self.__data is None:
140
+ time.sleep(0.01)
141
+ if time.time() - start_time > WAIT_TIMEOUT:
142
+ logger.warning("Screenshot timeout, cleaning up and restarting worker...")
143
+ with self.__lock:
144
+ if self.__retry_count < MAX_RETRY_COUNT:
145
+ self.__start_worker()
146
+ start_time = time.time() # 重置超时计时器
147
+ continue
148
+ else:
149
+ raise RuntimeError(f"Maximum retry count ({MAX_RETRY_COUNT}) exceeded")
150
+
151
+ # 检查 worker 是否还活着
152
+ if self.__worker and not self.__worker.is_alive():
153
+ with self.__lock:
154
+ if self.__retry_count < MAX_RETRY_COUNT:
155
+ logger.warning("Worker thread died, restarting...")
156
+ self.__start_worker()
157
+ else:
158
+ raise RuntimeError(f"Maximum retry count ({MAX_RETRY_COUNT}) exceeded")
159
+
160
+ logger.verbose(f"adb raw screenshot wait time: {time.time() - start_time:.4f}s")
161
+ data = self.__data
162
+ self.__data = None
159
163
  return data
@@ -1,8 +1,12 @@
1
- from .external_renderer_ipc import ExternalRendererIpc
2
- from .nemu_ipc import NemuIpcImpl, NemuIpcImplConfig
3
-
4
- __all__ = [
5
- "ExternalRendererIpc",
6
- "NemuIpcImpl",
7
- "NemuIpcImplConfig",
1
+ # ruff: noqa: E402
2
+ from kotonebot.util import require_windows
3
+ require_windows('"RemoteWindowsImpl" implementation')
4
+
5
+ from .external_renderer_ipc import ExternalRendererIpc
6
+ from .nemu_ipc import NemuIpcImpl, NemuIpcImplConfig
7
+
8
+ __all__ = [
9
+ "ExternalRendererIpc",
10
+ "NemuIpcImpl",
11
+ "NemuIpcImplConfig",
8
12
  ]