ok-script 0.0.7__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.
- autohelper/AutoHelper.py +116 -0
- autohelper/__init__.py +0 -0
- autohelper/capture/BaseCaptureMethod.py +30 -0
- autohelper/capture/HwndWindow.py +138 -0
- autohelper/capture/__init__.py +0 -0
- autohelper/capture/adb/ADBCaptureMethod.py +29 -0
- autohelper/capture/adb/DeviceManager.py +209 -0
- autohelper/capture/adb/__init__.py +0 -0
- autohelper/capture/adb/bin/__init__.py +0 -0
- autohelper/capture/adb/targets.py +61 -0
- autohelper/capture/adb/vbox.py +140 -0
- autohelper/capture/windows/WindowsGraphicsCaptureMethod.py +228 -0
- autohelper/capture/windows/__init__.py +0 -0
- autohelper/capture/windows/d3d11.py +74 -0
- autohelper/capture/windows/utils.py +149 -0
- autohelper/capture/windows/window.py +36 -0
- autohelper/color/Color.py +41 -0
- autohelper/color/__init__.py +0 -0
- autohelper/config/Config.py +79 -0
- autohelper/config/InfoDict.py +16 -0
- autohelper/config/__init__.py +0 -0
- autohelper/feature/Box.py +199 -0
- autohelper/feature/Feature.py +20 -0
- autohelper/feature/FeatureSet.py +207 -0
- autohelper/feature/FindFeature.py +52 -0
- autohelper/feature/__init__.py +0 -0
- autohelper/gui/App.py +120 -0
- autohelper/gui/Communicate.py +22 -0
- autohelper/gui/MainWindow.py +69 -0
- autohelper/gui/TabTitles.py +56 -0
- autohelper/gui/__init__.py +5 -0
- autohelper/gui/debug/AspectRatioWidget.py +31 -0
- autohelper/gui/debug/DebugTab.py +37 -0
- autohelper/gui/debug/FrameWidget.py +125 -0
- autohelper/gui/debug/InfoWidget.py +40 -0
- autohelper/gui/debug/LoggerWidget.py +74 -0
- autohelper/gui/debug/__init__.py +0 -0
- autohelper/gui/i18n/__init__.py +0 -0
- autohelper/gui/i18n/path.py +3 -0
- autohelper/gui/icon/__init__.py +0 -0
- autohelper/gui/icon/icon.py +6 -0
- autohelper/gui/loading/LoadingWindow.py +142 -0
- autohelper/gui/loading/__init__.py +0 -0
- autohelper/gui/overlay/OverlayWindow.py +34 -0
- autohelper/gui/overlay/__init__.py +0 -0
- autohelper/gui/resources.py +485 -0
- autohelper/gui/tasks/ConfigItemFactory.py +14 -0
- autohelper/gui/tasks/StartButton.py +47 -0
- autohelper/gui/tasks/TaskOpButton.py +33 -0
- autohelper/gui/tasks/TaskTab.py +121 -0
- autohelper/gui/tasks/TooltipTableWidget.py +39 -0
- autohelper/gui/tasks/__init__.py +0 -0
- autohelper/gui/util/Alert.py +20 -0
- autohelper/gui/util/InitWorker.py +17 -0
- autohelper/gui/util/__init__.py +0 -0
- autohelper/gui/widget/ListTableWidgetItem.py +27 -0
- autohelper/gui/widget/NumericTableWidgetItem.py +33 -0
- autohelper/gui/widget/RoundCornerContainer.py +59 -0
- autohelper/gui/widget/TabWidget.py +30 -0
- autohelper/gui/widget/UpdateConfigWidgetItem.py +16 -0
- autohelper/gui/widget/YesNonWidgetItem.py +23 -0
- autohelper/gui/widget/__init__.py +0 -0
- autohelper/interaction/ADBInteraction.py +33 -0
- autohelper/interaction/BaseInteraction.py +36 -0
- autohelper/interaction/Win32Interaction.py +56 -0
- autohelper/interaction/__init__.py +0 -0
- autohelper/logging/Logger.py +103 -0
- autohelper/logging/__init__.py +0 -0
- autohelper/ocr/OCR.py +50 -0
- autohelper/ocr/__init__.py +0 -0
- autohelper/predict/__init__.py +0 -0
- autohelper/predict/predict.py +45 -0
- autohelper/rotypes/Windows/Foundation/Collections.py +60 -0
- autohelper/rotypes/Windows/Foundation/__init__.py +153 -0
- autohelper/rotypes/Windows/Globalization/__init__.py +25 -0
- autohelper/rotypes/Windows/Graphics/Capture/__init__.py +110 -0
- autohelper/rotypes/Windows/Graphics/DirectX/Direct3D11/__init__.py +41 -0
- autohelper/rotypes/Windows/Graphics/DirectX/__init__.py +167 -0
- autohelper/rotypes/Windows/Graphics/Imaging/__init__.py +34 -0
- autohelper/rotypes/Windows/Graphics/__init__.py +5 -0
- autohelper/rotypes/Windows/Media/Ocr/__init__.py +76 -0
- autohelper/rotypes/Windows/Media/__init__.py +0 -0
- autohelper/rotypes/Windows/Security/Cryptography/__init__.py +18 -0
- autohelper/rotypes/Windows/Security/__init__.py +0 -0
- autohelper/rotypes/Windows/Storage/Streams/__init__.py +27 -0
- autohelper/rotypes/Windows/Storage/__init__.py +0 -0
- autohelper/rotypes/Windows/__init__.py +0 -0
- autohelper/rotypes/__init__.py +4 -0
- autohelper/rotypes/delegate.py +105 -0
- autohelper/rotypes/export.py +21 -0
- autohelper/rotypes/idldsl.py +272 -0
- autohelper/rotypes/inspectable.py +79 -0
- autohelper/rotypes/roapi.py +30 -0
- autohelper/rotypes/types.py +77 -0
- autohelper/rotypes/winstring.py +44 -0
- autohelper/save/BlackBarProcessor.py +25 -0
- autohelper/save/PostProcessor.py +6 -0
- autohelper/save/SaveByInterval.py +30 -0
- autohelper/save/SaveByKeyPress.py +27 -0
- autohelper/save/SaveMethodBase.py +46 -0
- autohelper/save/__init__.py +0 -0
- autohelper/scene/FeatureScene.py +14 -0
- autohelper/scene/Scene.py +21 -0
- autohelper/scene/__init__.py +0 -0
- autohelper/stats/StreamStats.py +34 -0
- autohelper/stats/__init__.py +0 -0
- autohelper/task/BaseTask.py +109 -0
- autohelper/task/ExecutorOperation.py +119 -0
- autohelper/task/FindFeatureTask.py +6 -0
- autohelper/task/TaskExecutor.py +249 -0
- autohelper/task/__init__.py +0 -0
- autohelper/util/__init__.py +0 -0
- autohelper/util/json.py +27 -0
- autohelper/util/list.py +2 -0
- autohelper/util/path.py +37 -0
- autohelper/util/thread.py +12 -0
- autohelper/util/win32_process.py +86 -0
- autohelper/util/yaml.py +28 -0
- ok_script-0.0.7.dist-info/METADATA +64 -0
- ok_script-0.0.7.dist-info/RECORD +122 -0
- ok_script-0.0.7.dist-info/WHEEL +5 -0
- ok_script-0.0.7.dist-info/top_level.txt +1 -0
autohelper/AutoHelper.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
from PySide6.QtWidgets import QApplication
|
|
7
|
+
|
|
8
|
+
import autohelper
|
|
9
|
+
from autohelper.feature.FeatureSet import FeatureSet
|
|
10
|
+
from autohelper.gui.App import App
|
|
11
|
+
from autohelper.gui.Communicate import communicate
|
|
12
|
+
from autohelper.gui.overlay.OverlayWindow import OverlayWindow
|
|
13
|
+
from autohelper.gui.util.InitWorker import InitWorker
|
|
14
|
+
from autohelper.logging.Logger import get_logger, config_logger
|
|
15
|
+
from autohelper.task.TaskExecutor import TaskExecutor
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AutoHelper:
|
|
21
|
+
executor: TaskExecutor
|
|
22
|
+
adb = None
|
|
23
|
+
adb_device = None
|
|
24
|
+
feature_set = None
|
|
25
|
+
hwnd = None
|
|
26
|
+
device_manager = None
|
|
27
|
+
ocr = None
|
|
28
|
+
overlay_window = None
|
|
29
|
+
app = None
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: Dict[str, Any]):
|
|
32
|
+
print(f"AutoHelper init, config: {config}")
|
|
33
|
+
self.debug = config.get("debug", False)
|
|
34
|
+
self.exit_event = threading.Event()
|
|
35
|
+
self.config = config
|
|
36
|
+
self.init_device_manager()
|
|
37
|
+
if config.get("use_gui"):
|
|
38
|
+
self.app = App(self.config.get('gui_icon'), self.debug, self.config.get('gui_title'),
|
|
39
|
+
self.config['tasks'], self.exit_event)
|
|
40
|
+
autohelper.gui.app = self.app
|
|
41
|
+
self.app.show_loading()
|
|
42
|
+
self.worker = InitWorker(self.do_init)
|
|
43
|
+
self.worker.start()
|
|
44
|
+
self.app.exec()
|
|
45
|
+
else:
|
|
46
|
+
self.device_manager.set_preferred_device()
|
|
47
|
+
self.device_manager.start()
|
|
48
|
+
self.do_init()
|
|
49
|
+
self.task_executor.start()
|
|
50
|
+
if config.get("debug"):
|
|
51
|
+
self.app = QApplication(sys.argv)
|
|
52
|
+
self.overlay_window = OverlayWindow(autohelper.gui.device_manager.hwnd)
|
|
53
|
+
self.app.exec()
|
|
54
|
+
else:
|
|
55
|
+
try:
|
|
56
|
+
# Starting the task in a separate thread (optional)
|
|
57
|
+
# This allows the main thread to remain responsive to keyboard interrupts
|
|
58
|
+
task_thread = threading.Thread(target=self.wait_task())
|
|
59
|
+
task_thread.start()
|
|
60
|
+
|
|
61
|
+
# Wait for the task thread to end (which it won't, in this case, without an interrupt)
|
|
62
|
+
task_thread.join()
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
self.exit_event.set()
|
|
65
|
+
logger.info("Keyboard interrupt received, exiting script.")
|
|
66
|
+
finally:
|
|
67
|
+
# Clean-up code goes here (if any)
|
|
68
|
+
# This block ensures that the script terminates gracefully,
|
|
69
|
+
# releasing resources or performing necessary clean-up operations.
|
|
70
|
+
logger.info("Script has terminated.")
|
|
71
|
+
|
|
72
|
+
def init_message(self, message: str, done=False):
|
|
73
|
+
communicate.init.emit(done, message)
|
|
74
|
+
if self.exit_event.is_set():
|
|
75
|
+
self.worker.quit()
|
|
76
|
+
|
|
77
|
+
def do_init(self):
|
|
78
|
+
logger.info(f"initializing {self.__class__.__name__}, config: {self.config}")
|
|
79
|
+
if self.config.get('ocr'):
|
|
80
|
+
self.init_message("RapidOCR init Start")
|
|
81
|
+
from rapidocr_onnxruntime import RapidOCR
|
|
82
|
+
self.ocr = RapidOCR()
|
|
83
|
+
self.init_message("RapidOCR init Complete")
|
|
84
|
+
|
|
85
|
+
config_logger(self.config)
|
|
86
|
+
|
|
87
|
+
if self.config.get('coco_feature_folder') is not None:
|
|
88
|
+
self.init_message("FeatureSet init Start")
|
|
89
|
+
coco_feature_folder = self.config.get('coco_feature_folder')
|
|
90
|
+
self.feature_set = FeatureSet(coco_feature_folder,
|
|
91
|
+
default_horizontal_variance=self.config.get('default_horizontal_variance', 0),
|
|
92
|
+
default_vertical_variance=self.config.get('default_vertical_variance', 0),
|
|
93
|
+
default_threshold=self.config.get('default_threshold', 0))
|
|
94
|
+
self.init_message("FeatureSet init Complete")
|
|
95
|
+
|
|
96
|
+
self.init_message("TaskExecutor init Start")
|
|
97
|
+
self.task_executor = TaskExecutor(self.device_manager, exit_event=self.exit_event,
|
|
98
|
+
tasks=self.config['tasks'], scenes=self.config['scenes'],
|
|
99
|
+
feature_set=self.feature_set,
|
|
100
|
+
ocr=self.ocr, config_folder=self.config.get("config_folder") or "config")
|
|
101
|
+
self.init_message("TaskExecutor init Done")
|
|
102
|
+
|
|
103
|
+
if self.app:
|
|
104
|
+
autohelper.gui.executor = self.task_executor
|
|
105
|
+
|
|
106
|
+
def wait_task(self):
|
|
107
|
+
while not self.exit_event.is_set():
|
|
108
|
+
time.sleep(1)
|
|
109
|
+
|
|
110
|
+
def init_device_manager(self):
|
|
111
|
+
if self.device_manager is None:
|
|
112
|
+
from autohelper.capture.adb.DeviceManager import DeviceManager
|
|
113
|
+
self.device_manager = DeviceManager(self.config.get("config_folder") or "config",
|
|
114
|
+
self.config.get('capture_window_title'), self.config.get("debug"),
|
|
115
|
+
self.exit_event)
|
|
116
|
+
autohelper.gui.device_manager = self.device_manager
|
autohelper/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseCaptureMethod:
|
|
5
|
+
name = "None"
|
|
6
|
+
description = ""
|
|
7
|
+
last_captured_frame: np.ndarray
|
|
8
|
+
top_cut = 0
|
|
9
|
+
bottom_cut = 0
|
|
10
|
+
left_cut = 0
|
|
11
|
+
right_cut = 0
|
|
12
|
+
width = 0
|
|
13
|
+
height = 0
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
# Some capture methods don't need an initialization process
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def close(self):
|
|
20
|
+
# Some capture methods don't need an initialization process
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def get_frame(self) -> np.ndarray | None:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def draw_rectangle(self):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def clickable(self):
|
|
30
|
+
pass
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# original https://github.com/dantmnf & https://github.com/hakaboom/winAuto
|
|
2
|
+
import re
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from typing_extensions import override
|
|
6
|
+
from win32 import win32gui
|
|
7
|
+
|
|
8
|
+
from autohelper.capture.windows.window import is_foreground_window, get_window_bounds
|
|
9
|
+
from autohelper.gui.Communicate import communicate
|
|
10
|
+
from autohelper.logging.Logger import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HwndWindow:
|
|
16
|
+
visible = True
|
|
17
|
+
x = 0
|
|
18
|
+
y = 0
|
|
19
|
+
width = 0
|
|
20
|
+
height = 0
|
|
21
|
+
title_height = 0
|
|
22
|
+
border = 0
|
|
23
|
+
scaling = 1
|
|
24
|
+
top_cut = 0
|
|
25
|
+
right_cut = 0
|
|
26
|
+
bottom_cut = 0
|
|
27
|
+
left_cut = 0
|
|
28
|
+
window_change_listeners = []
|
|
29
|
+
frame_aspect_ratio = 0
|
|
30
|
+
hwnd = None
|
|
31
|
+
frame_width = 0
|
|
32
|
+
frame_height = 0
|
|
33
|
+
exists = False
|
|
34
|
+
|
|
35
|
+
def __init__(self, title="", exit_event=threading.Event(), frame_width=0, frame_height=0):
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.app_exit_event = exit_event
|
|
38
|
+
self.stop_event = threading.Event()
|
|
39
|
+
self.update_title_re(title)
|
|
40
|
+
self.visible = False
|
|
41
|
+
self.update_frame_size(frame_width, frame_height)
|
|
42
|
+
self.do_update_window_size()
|
|
43
|
+
self.thread = threading.Thread(target=self.update_window_size)
|
|
44
|
+
self.thread.start()
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def close(self):
|
|
48
|
+
self.app_exit_event.set()
|
|
49
|
+
|
|
50
|
+
def stop(self):
|
|
51
|
+
self.stop_event.set()
|
|
52
|
+
|
|
53
|
+
def update_title_re(self, title):
|
|
54
|
+
self.title = re.compile(title)
|
|
55
|
+
|
|
56
|
+
def update_frame_size(self, width, height):
|
|
57
|
+
if width != self.frame_width or height != self.frame_height:
|
|
58
|
+
self.frame_width = width
|
|
59
|
+
self.frame_height = height
|
|
60
|
+
if width > 0 and height > 0:
|
|
61
|
+
self.frame_aspect_ratio = width / height
|
|
62
|
+
logger.debug(f"HwndWindow: frame ratio:{self.frame_aspect_ratio} width: {width}, height: {height}")
|
|
63
|
+
|
|
64
|
+
def update_window_size(self):
|
|
65
|
+
while not self.app_exit_event.is_set() and not self.stop_event.is_set():
|
|
66
|
+
self.do_update_window_size()
|
|
67
|
+
self.app_exit_event.wait(0.1)
|
|
68
|
+
|
|
69
|
+
def get_abs_cords(self, x, y):
|
|
70
|
+
return int(self.x * self.scaling + (self.border * self.scaling + x)), int(
|
|
71
|
+
self.y * self.scaling + (y + self.title_height * self.scaling))
|
|
72
|
+
|
|
73
|
+
def get_top_left_frame_offset(self):
|
|
74
|
+
return int(self.border * self.scaling), int(
|
|
75
|
+
self.title_height * self.scaling)
|
|
76
|
+
|
|
77
|
+
def do_update_window_size(self):
|
|
78
|
+
visible, x, y, border, title_height, width, height, scaling = self.visible, self.x, self.y, self.border, self.title_height, self.width, self.height, self.scaling
|
|
79
|
+
if self.hwnd is None:
|
|
80
|
+
self.hwnd = find_hwnds_by_title(self.title)
|
|
81
|
+
if self.hwnd is not None:
|
|
82
|
+
self.exists = win32gui.IsWindow(self.hwnd)
|
|
83
|
+
if self.exists:
|
|
84
|
+
visible = is_foreground_window(self.hwnd)
|
|
85
|
+
x, y, border, title_height, width, height, scaling = get_window_bounds(
|
|
86
|
+
self.hwnd)
|
|
87
|
+
if self.frame_aspect_ratio != 0:
|
|
88
|
+
window_ratio = width / height
|
|
89
|
+
if window_ratio < self.frame_aspect_ratio:
|
|
90
|
+
cropped_window_height = int(width / self.frame_aspect_ratio)
|
|
91
|
+
title_height += height - cropped_window_height
|
|
92
|
+
height = cropped_window_height
|
|
93
|
+
height = height
|
|
94
|
+
width = width
|
|
95
|
+
title_height = title_height
|
|
96
|
+
else:
|
|
97
|
+
self.hwnd = None
|
|
98
|
+
changed = False
|
|
99
|
+
if visible != self.visible or self.scaling != scaling:
|
|
100
|
+
self.visible = visible
|
|
101
|
+
self.scaling = scaling
|
|
102
|
+
changed = True
|
|
103
|
+
if (
|
|
104
|
+
x != self.x or y != self.y or border != self.border or title_height != self.title_height or width != self.width or height != self.height or scaling != self.scaling) and (
|
|
105
|
+
(x >= 0 and y >= 0) or self.visible):
|
|
106
|
+
self.x, self.y, self.border, self.title_height, self.width, self.height = x, y, border, title_height, width, height
|
|
107
|
+
changed = True
|
|
108
|
+
if changed:
|
|
109
|
+
logger.debug(
|
|
110
|
+
f"do_update_window_size changed: {self.visible} {self.x} {self.y} {self.border} {self.width} {self.height} {self.scaling}")
|
|
111
|
+
communicate.window.emit(self.visible, self.x, self.y, self.border, self.title_height, self.width,
|
|
112
|
+
self.height, self.scaling)
|
|
113
|
+
|
|
114
|
+
def frame_ratio(self, size):
|
|
115
|
+
if self.frame_width > 0 and self.width > 0:
|
|
116
|
+
return int(size / self.frame_width * self.width)
|
|
117
|
+
else:
|
|
118
|
+
return size
|
|
119
|
+
|
|
120
|
+
def title_text(self):
|
|
121
|
+
if self.hwnd:
|
|
122
|
+
return win32gui.GetWindowText(self.hwnd)
|
|
123
|
+
return ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def find_hwnds_by_title(title):
|
|
127
|
+
hwnds = []
|
|
128
|
+
|
|
129
|
+
def enum_windows_proc(hwnd, lParam):
|
|
130
|
+
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
|
|
131
|
+
if re.search(title, win32gui.GetWindowText(hwnd)):
|
|
132
|
+
hwnds.append(hwnd)
|
|
133
|
+
|
|
134
|
+
win32gui.EnumWindows(enum_windows_proc, None)
|
|
135
|
+
if len(hwnds) > 0:
|
|
136
|
+
if len(hwnds) > 1:
|
|
137
|
+
logger.warning(f"Found multiple hwnds {len(hwnds)}")
|
|
138
|
+
return hwnds[0]
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# original https://github.com/Toufool/AutoSplit/blob/master/src/capture_method/WindowsGraphicsCaptureMethod.py
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
5
|
+
from autohelper.capture.BaseCaptureMethod import BaseCaptureMethod
|
|
6
|
+
from autohelper.logging.Logger import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ADBCaptureMethod(BaseCaptureMethod):
|
|
12
|
+
name = "ADB command line Capture"
|
|
13
|
+
description = "use the adb screencap command, slow but works when in background/minimized, takes 300ms per frame"
|
|
14
|
+
|
|
15
|
+
def __init__(self, device_manager):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.device_manager = device_manager
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def width(self):
|
|
21
|
+
return self.device_manager.width
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def height(self):
|
|
25
|
+
return self.device_manager.height
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
def get_frame(self) -> np.ndarray | None:
|
|
29
|
+
return self.device_manager.screencap()
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
import adbutils
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from autohelper.capture.HwndWindow import HwndWindow
|
|
8
|
+
from autohelper.capture.adb.ADBCaptureMethod import ADBCaptureMethod
|
|
9
|
+
from autohelper.capture.adb.vbox import installed_emulator
|
|
10
|
+
from autohelper.capture.windows.WindowsGraphicsCaptureMethod import WindowsGraphicsCaptureMethod
|
|
11
|
+
from autohelper.config.Config import Config
|
|
12
|
+
from autohelper.gui.Communicate import communicate
|
|
13
|
+
from autohelper.interaction.ADBInteraction import ADBBaseInteraction
|
|
14
|
+
from autohelper.interaction.Win32Interaction import Win32Interaction
|
|
15
|
+
from autohelper.logging.Logger import get_logger
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DeviceManager:
|
|
21
|
+
|
|
22
|
+
def __init__(self, config_folder, hwnd_title=None, debug=False, exit_event=None):
|
|
23
|
+
self._device = None
|
|
24
|
+
self.adb = None
|
|
25
|
+
self.hwnd_title = hwnd_title
|
|
26
|
+
self.debug = debug
|
|
27
|
+
self.interaction = None
|
|
28
|
+
self.exit_event = exit_event
|
|
29
|
+
if hwnd_title is not None:
|
|
30
|
+
self.hwnd = HwndWindow(hwnd_title, exit_event)
|
|
31
|
+
self.config = Config({"devices": {}}, config_folder, "DeviceManager")
|
|
32
|
+
self._size = (0, 0)
|
|
33
|
+
self.thread = None
|
|
34
|
+
self.capture_method = None
|
|
35
|
+
self.refresh()
|
|
36
|
+
|
|
37
|
+
def refresh(self):
|
|
38
|
+
if self.thread is None or not self.thread.is_alive():
|
|
39
|
+
self.thread = threading.Thread(target=self.do_refresh, name="refresh adb")
|
|
40
|
+
self.thread.start()
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def device_dict(self):
|
|
44
|
+
return self.config.get("devices")
|
|
45
|
+
|
|
46
|
+
def connect_all(self):
|
|
47
|
+
for device in self.device_dict.values():
|
|
48
|
+
self.adb.connect(device['address'])
|
|
49
|
+
logger.debug(f'connect_all {self.adb.device_list()}')
|
|
50
|
+
|
|
51
|
+
def get_devices(self):
|
|
52
|
+
return self.device_dict.values()
|
|
53
|
+
|
|
54
|
+
def do_refresh(self):
|
|
55
|
+
if self.adb is None:
|
|
56
|
+
self.adb = adbutils.AdbClient(host="127.0.0.1")
|
|
57
|
+
logger.debug(f'connect adb')
|
|
58
|
+
self.connect_all()
|
|
59
|
+
installed_emulators = installed_emulator()
|
|
60
|
+
for device in self.adb.list():
|
|
61
|
+
logger.debug(f'adb.list() {device}')
|
|
62
|
+
|
|
63
|
+
for emulator in installed_emulators:
|
|
64
|
+
logger.debug(f"installed_emulator: {emulator}")
|
|
65
|
+
self.adb.connect(emulator.adb_address)
|
|
66
|
+
|
|
67
|
+
device_list = self.adb.device_list()
|
|
68
|
+
|
|
69
|
+
device_list = sorted(device_list, key=lambda x: x.serial)
|
|
70
|
+
|
|
71
|
+
for device in self.device_dict.values():
|
|
72
|
+
device['connected'] = False
|
|
73
|
+
|
|
74
|
+
for device in device_list:
|
|
75
|
+
imei = device.shell("settings get secure android_id") or device.shell(
|
|
76
|
+
"service call iphonesubinfo 4") or device.prop.model
|
|
77
|
+
frame = do_screencap(device)
|
|
78
|
+
width, height = 0, 0
|
|
79
|
+
if frame is not None:
|
|
80
|
+
height, width, _ = frame.shape
|
|
81
|
+
adb_device = {"address": device.serial, "imei": imei, "method": "adb",
|
|
82
|
+
"model": device.prop.model, "nick": device.prop.model, "connected": True, "preferred": False,
|
|
83
|
+
"resolution": f"{width}x{height}"}
|
|
84
|
+
found = False
|
|
85
|
+
for emulator in installed_emulators:
|
|
86
|
+
if emulator.adb_address == adb_device['address']:
|
|
87
|
+
adb_device['nick'] = emulator.description
|
|
88
|
+
found = True
|
|
89
|
+
if self.device_dict.get(imei):
|
|
90
|
+
if found or not self.device_dict[imei]['connected']:
|
|
91
|
+
self.device_dict[imei] = adb_device
|
|
92
|
+
else:
|
|
93
|
+
self.device_dict[imei] = adb_device
|
|
94
|
+
if self.hwnd_title:
|
|
95
|
+
nick = self.hwnd_title
|
|
96
|
+
width = 0
|
|
97
|
+
height = 0
|
|
98
|
+
if self.hwnd:
|
|
99
|
+
nick = self.hwnd.title_text()
|
|
100
|
+
width = self.hwnd.width
|
|
101
|
+
height = self.hwnd.height
|
|
102
|
+
self.device_dict[self.hwnd_title] = {"address": "", "imei": self.hwnd_title, "method": "windows",
|
|
103
|
+
"model": "", "nick": nick or self.hwnd_title,
|
|
104
|
+
"connected": self.hwnd is not None and self.hwnd.exists,
|
|
105
|
+
"preferred": False,
|
|
106
|
+
"resolution": f"{width}x{height}"}
|
|
107
|
+
communicate.adb_devices.emit()
|
|
108
|
+
self.config.save_file()
|
|
109
|
+
logger.debug(f'refresh {self.device_dict}')
|
|
110
|
+
|
|
111
|
+
def set_preferred_device(self, imei=None):
|
|
112
|
+
logger.debug(f"set_preferred_device {imei}")
|
|
113
|
+
preferred = None
|
|
114
|
+
if imei is None:
|
|
115
|
+
for device in self.device_dict.values():
|
|
116
|
+
if device.get('preferred'):
|
|
117
|
+
preferred = device
|
|
118
|
+
imei = preferred['imei']
|
|
119
|
+
else:
|
|
120
|
+
preferred = self.device_dict.get(imei)
|
|
121
|
+
for key in self.device_dict.keys():
|
|
122
|
+
if imei == key:
|
|
123
|
+
self.device_dict[imei]["preferred"] = False
|
|
124
|
+
if preferred is None and len(self.device_dict) > 0:
|
|
125
|
+
preferred = next(iter(self.device_dict.values()))
|
|
126
|
+
if preferred is not None:
|
|
127
|
+
preferred['preferred'] = True
|
|
128
|
+
if preferred['method'] == 'windows':
|
|
129
|
+
if not isinstance(self.capture_method, WindowsGraphicsCaptureMethod):
|
|
130
|
+
if self.capture_method is not None:
|
|
131
|
+
self.capture_method.close()
|
|
132
|
+
self.capture_method = WindowsGraphicsCaptureMethod(self.hwnd)
|
|
133
|
+
self.interaction = Win32Interaction(self.capture_method)
|
|
134
|
+
self.capture_method.hwnd_window = self.hwnd
|
|
135
|
+
else:
|
|
136
|
+
for adb_device in self.adb.device_list():
|
|
137
|
+
if adb_device.serial == preferred.get('address'):
|
|
138
|
+
self._device = adb_device
|
|
139
|
+
if not isinstance(self.capture_method, ADBCaptureMethod):
|
|
140
|
+
if self.capture_method is not None:
|
|
141
|
+
self.capture_method.close()
|
|
142
|
+
self.capture_method = ADBCaptureMethod(self)
|
|
143
|
+
self.interaction = ADBBaseInteraction(self, self.capture_method)
|
|
144
|
+
self.config.save_file()
|
|
145
|
+
logger.debug(f'preferred device: {preferred}')
|
|
146
|
+
|
|
147
|
+
def start(self):
|
|
148
|
+
if isinstance(self.capture_method, ADBCaptureMethod):
|
|
149
|
+
if self.debug:
|
|
150
|
+
if self.hwnd is not None:
|
|
151
|
+
self.hwnd.update_title_re("autohelper_debug")
|
|
152
|
+
self.hwnd.update_frame_size(self.capture_method.width, self.capture_method.height)
|
|
153
|
+
else:
|
|
154
|
+
self.hwnd = HwndWindow("autohelper_debug", self.exit_event, self.capture_method.width,
|
|
155
|
+
self.capture_method.height)
|
|
156
|
+
else:
|
|
157
|
+
self.hwnd.stop()
|
|
158
|
+
self.hwnd = None
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def device(self):
|
|
162
|
+
if self._device is None:
|
|
163
|
+
self.set_preferred_device()
|
|
164
|
+
return self._device
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def width(self):
|
|
168
|
+
if self._size[0] == 0:
|
|
169
|
+
self.screencap()
|
|
170
|
+
return self._size[0]
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def height(self):
|
|
174
|
+
if self._size[1] == 0:
|
|
175
|
+
self.screencap()
|
|
176
|
+
return self._size[1]
|
|
177
|
+
|
|
178
|
+
def update_device_list(self):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
def shell(self, *args, **kwargs):
|
|
182
|
+
device = self.device
|
|
183
|
+
if device is not None:
|
|
184
|
+
try:
|
|
185
|
+
return device.shell(*args, **kwargs)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.error(f"shell_wrapper error occurred: {e}")
|
|
188
|
+
|
|
189
|
+
def screencap(self):
|
|
190
|
+
if self.exit_event.is_set():
|
|
191
|
+
return None
|
|
192
|
+
self._size = (0, 0)
|
|
193
|
+
frame = do_screencap(self.device)
|
|
194
|
+
if frame is not None:
|
|
195
|
+
height, width, _ = frame.shape
|
|
196
|
+
self._size = (width, height)
|
|
197
|
+
return frame
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def do_screencap(device) -> np.ndarray | None:
|
|
201
|
+
if device is not None:
|
|
202
|
+
png_bytes = device.shell("screencap -p", encoding=None)
|
|
203
|
+
if png_bytes is not None and len(png_bytes) > 0:
|
|
204
|
+
image_data = np.frombuffer(png_bytes, dtype=np.uint8)
|
|
205
|
+
image = cv2.imdecode(image_data, cv2.IMREAD_COLOR)
|
|
206
|
+
if image is not None:
|
|
207
|
+
return image
|
|
208
|
+
else:
|
|
209
|
+
logger.error(f"Screencap image decode error, probably disconnected")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ADBControllerTarget:
|
|
9
|
+
adb_serial: Optional[str]
|
|
10
|
+
description: str
|
|
11
|
+
adb_address: str
|
|
12
|
+
dedup_priority: int
|
|
13
|
+
auto_connect_priority: int
|
|
14
|
+
display_id: Optional[int] = None
|
|
15
|
+
override_identifier: Optional[str] = None
|
|
16
|
+
preload_device_info: Optional[dict] = None
|
|
17
|
+
|
|
18
|
+
def describe(self):
|
|
19
|
+
"""returns identifier, description"""
|
|
20
|
+
if self.override_identifier is not None:
|
|
21
|
+
identifier = self.override_identifier
|
|
22
|
+
description = f'{self.description}, {self.adb_serial or self.adb_address}'
|
|
23
|
+
else:
|
|
24
|
+
identifier = self.adb_serial or self.adb_address
|
|
25
|
+
description = self.description
|
|
26
|
+
if self.display_id:
|
|
27
|
+
identifier += f':display={self.display_id}'
|
|
28
|
+
return identifier, description
|
|
29
|
+
|
|
30
|
+
def __str__(self):
|
|
31
|
+
identifier, description = self.describe()
|
|
32
|
+
return f'{identifier} ({description})'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def dedup_targets(targets: list[ADBControllerTarget]) -> list[ADBControllerTarget]:
|
|
36
|
+
"""
|
|
37
|
+
dedup targets by adb_address
|
|
38
|
+
"""
|
|
39
|
+
groups = {}
|
|
40
|
+
deduped = []
|
|
41
|
+
for x in targets:
|
|
42
|
+
key = (x.adb_address, x.display_id)
|
|
43
|
+
groups.setdefault(key, []).append(x)
|
|
44
|
+
for group in groups.values():
|
|
45
|
+
group_targets = list(group)
|
|
46
|
+
group_targets.sort(key=lambda x: x.dedup_priority)
|
|
47
|
+
merged_target = group_targets[0]
|
|
48
|
+
for target in group_targets[1:]:
|
|
49
|
+
if target.adb_serial is not None:
|
|
50
|
+
merged_target.adb_serial = target.adb_serial
|
|
51
|
+
merged_target.description = target.description
|
|
52
|
+
merged_target.auto_connect_priority = max(merged_target.auto_connect_priority, target.auto_connect_priority)
|
|
53
|
+
merged_target.override_identifier = target.override_identifier
|
|
54
|
+
if target.preload_device_info:
|
|
55
|
+
if merged_target.preload_device_info is None:
|
|
56
|
+
merged_target.preload_device_info = {}
|
|
57
|
+
merged_target.preload_device_info.update(target.preload_device_info)
|
|
58
|
+
merged_target.dedup_priority = target.dedup_priority
|
|
59
|
+
merged_target.adb_server = target.adb_server
|
|
60
|
+
deduped.append(merged_target)
|
|
61
|
+
return deduped
|