devhelmkit 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.
Files changed (58) hide show
  1. devhelmkit/__init__.py +44 -0
  2. devhelmkit/android/__init__.py +7 -0
  3. devhelmkit/android/driver.py +18 -0
  4. devhelmkit/assets/so/arm64-v8a/agent_v1.so +0 -0
  5. devhelmkit/assets/so/arm64-v8a/agent_v2.so +0 -0
  6. devhelmkit/assets/so/x86_64/agent.so +0 -0
  7. devhelmkit/core/__init__.py +3 -0
  8. devhelmkit/core/base_component.py +214 -0
  9. devhelmkit/core/base_driver.py +414 -0
  10. devhelmkit/core/base_window.py +25 -0
  11. devhelmkit/core/selector_spec.py +102 -0
  12. devhelmkit/entry.py +90 -0
  13. devhelmkit/exceptions.py +56 -0
  14. devhelmkit/harmony/__init__.py +3 -0
  15. devhelmkit/harmony/agent/__init__.py +7 -0
  16. devhelmkit/harmony/agent/so_manager.py +300 -0
  17. devhelmkit/harmony/config.py +124 -0
  18. devhelmkit/harmony/device/__init__.py +6 -0
  19. devhelmkit/harmony/device/hdc.py +430 -0
  20. devhelmkit/harmony/driver.py +1416 -0
  21. devhelmkit/harmony/finder/__init__.py +12 -0
  22. devhelmkit/harmony/finder/component_finder.py +336 -0
  23. devhelmkit/harmony/finder/popup_handler.py +81 -0
  24. devhelmkit/harmony/finder/selector_adapter.py +101 -0
  25. devhelmkit/harmony/finder/xpath_query.py +110 -0
  26. devhelmkit/harmony/rpc/__init__.py +12 -0
  27. devhelmkit/harmony/rpc/client.py +126 -0
  28. devhelmkit/harmony/rpc/proxy_v2.py +106 -0
  29. devhelmkit/harmony/rpc/remote_object.py +48 -0
  30. devhelmkit/harmony/uiobject.py +246 -0
  31. devhelmkit/harmony/uiwindow.py +43 -0
  32. devhelmkit/harmony/webview/__init__.py +17 -0
  33. devhelmkit/harmony/webview/chromedriver_manager.py +251 -0
  34. devhelmkit/harmony/webview/devtools_finder.py +131 -0
  35. devhelmkit/harmony/webview/webview_driver.py +326 -0
  36. devhelmkit/model/__init__.py +3 -0
  37. devhelmkit/model/action.py +147 -0
  38. devhelmkit/model/app_state.py +50 -0
  39. devhelmkit/model/constants.py +22 -0
  40. devhelmkit/model/display.py +32 -0
  41. devhelmkit/model/format_string.py +24 -0
  42. devhelmkit/model/input.py +50 -0
  43. devhelmkit/model/json_base.py +42 -0
  44. devhelmkit/model/keys.py +375 -0
  45. devhelmkit/model/match_pattern.py +15 -0
  46. devhelmkit/model/page.py +13 -0
  47. devhelmkit/model/params.py +42 -0
  48. devhelmkit/model/rect.py +58 -0
  49. devhelmkit/model/runnable.py +18 -0
  50. devhelmkit/utils/__init__.py +3 -0
  51. devhelmkit/utils/logger.py +72 -0
  52. devhelmkit/utils/retry.py +46 -0
  53. devhelmkit/utils/timeout.py +64 -0
  54. devhelmkit-0.1.0.dist-info/METADATA +411 -0
  55. devhelmkit-0.1.0.dist-info/RECORD +58 -0
  56. devhelmkit-0.1.0.dist-info/WHEEL +5 -0
  57. devhelmkit-0.1.0.dist-info/licenses/LICENSE +201 -0
  58. devhelmkit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,414 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """BaseDriver:跨平台设备驱动契约。
4
+
5
+ 命名原则:U2 风格 snake_case 为基准,语义化方法为补充。
6
+ 抽象层只定义跨平台通用接口,平台特有能力通过子类扩展提供。
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from PIL.Image import Image
15
+ from devhelmkit.core.base_component import BaseComponent
16
+ from devhelmkit.core.base_window import BaseWindow
17
+
18
+
19
+ class BaseDriver(ABC):
20
+ """跨平台设备驱动契约。"""
21
+
22
+ # ============================================================
23
+ # 生命周期
24
+ # ============================================================
25
+
26
+ @abstractmethod
27
+ def close(self) -> None:
28
+ """关闭驱动,释放连接资源。"""
29
+
30
+ def __enter__(self) -> 'BaseDriver':
31
+ return self
32
+
33
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
34
+ self.close()
35
+ return False
36
+
37
+ @property
38
+ @abstractmethod
39
+ def device_sn(self) -> str:
40
+ """设备序列号。"""
41
+
42
+ @property
43
+ @abstractmethod
44
+ def platform(self) -> str:
45
+ """平台标识:'harmony' / 'android'。"""
46
+
47
+ # ============================================================
48
+ # 设备信息
49
+ # ============================================================
50
+
51
+ @property
52
+ @abstractmethod
53
+ def info(self) -> Dict[str, Any]:
54
+ """设备基本信息(品牌、型号、系统版本等)。"""
55
+
56
+ @abstractmethod
57
+ def get_display_size(self) -> Tuple[int, int]:
58
+ """获取屏幕分辨率 (width, height)。"""
59
+
60
+ @abstractmethod
61
+ def get_display_rotation(self) -> int:
62
+ """获取屏幕方向。"""
63
+
64
+ @abstractmethod
65
+ def set_display_rotation(self, rotation: int) -> None:
66
+ """设置屏幕方向。"""
67
+
68
+ @abstractmethod
69
+ def set_display_rotation_enabled(self, enabled: bool) -> None:
70
+ """自动旋转开关。"""
71
+
72
+ @abstractmethod
73
+ def get_device_type(self) -> str:
74
+ """设备类型(phone / tablet / 2in1 / wearable / ...)。"""
75
+
76
+ @abstractmethod
77
+ def get_os_type(self) -> str:
78
+ """操作系统类型标识。"""
79
+
80
+ # ============================================================
81
+ # 屏幕操作
82
+ # ============================================================
83
+
84
+ @abstractmethod
85
+ def screen_on(self) -> None:
86
+ """亮屏(唤醒屏幕)。"""
87
+
88
+ @abstractmethod
89
+ def screen_off(self) -> None:
90
+ """熄屏。"""
91
+
92
+ @abstractmethod
93
+ def is_screen_on(self) -> bool:
94
+ """屏幕是否点亮。"""
95
+
96
+ @abstractmethod
97
+ def is_screen_locked(self) -> bool:
98
+ """屏幕是否锁屏。"""
99
+
100
+ @abstractmethod
101
+ def unlock(self) -> None:
102
+ """解锁设备(亮屏 + 上滑/回车解除锁屏)。"""
103
+
104
+ @abstractmethod
105
+ def set_sleep_time(self, seconds: float) -> None:
106
+ """设置熄屏时间(秒)。"""
107
+
108
+ @abstractmethod
109
+ def restore_sleep_time(self) -> None:
110
+ """恢复默认熄屏时间。"""
111
+
112
+ # ============================================================
113
+ # 选择器入口(U2 风格)
114
+ # ============================================================
115
+
116
+ @abstractmethod
117
+ def __call__(self, **kwargs) -> 'BaseComponent':
118
+ """U2 风格选择器:d(text=, resourceId=, className=),返回控件对象。
119
+
120
+ 平台实现返回具体子类(协变),如 HarmonyDriver 返回 UiObject。
121
+ """
122
+
123
+ @abstractmethod
124
+ def xpath(self, xpath: str) -> 'BaseComponent':
125
+ """xpath 选择器,返回控件对象。"""
126
+
127
+ # ============================================================
128
+ # 应用管理
129
+ # ============================================================
130
+
131
+ @abstractmethod
132
+ def app_start(self, package: str, activity: Optional[str] = None,
133
+ params: str = "", wait_time: float = 1) -> None:
134
+ """启动应用。"""
135
+
136
+ @abstractmethod
137
+ def app_stop(self, package: str, wait_time: float = 0.5) -> None:
138
+ """停止应用。"""
139
+
140
+ @abstractmethod
141
+ def app_current(self) -> Tuple[str, str]:
142
+ """获取当前前台应用 (package, activity)。"""
143
+
144
+ @abstractmethod
145
+ def app_list(self) -> List[str]:
146
+ """已安装应用列表。"""
147
+
148
+ @abstractmethod
149
+ def has_app(self, package: str) -> bool:
150
+ """查询是否已安装应用。"""
151
+
152
+ @abstractmethod
153
+ def clear_app_data(self, package: str) -> None:
154
+ """清除应用数据。"""
155
+
156
+ # ============================================================
157
+ # 坐标操作
158
+ # ============================================================
159
+
160
+ @abstractmethod
161
+ def click(self, x: int, y: int) -> None:
162
+ """坐标点击。"""
163
+
164
+ @abstractmethod
165
+ def long_click(self, x: int, y: int, duration: float = 0.5) -> None:
166
+ """坐标长按。"""
167
+
168
+ @abstractmethod
169
+ def double_click(self, x: int, y: int) -> None:
170
+ """坐标双击。"""
171
+
172
+ @abstractmethod
173
+ def swipe(self, x1: int, y1: int, x2: int, y2: int,
174
+ duration: float = 0.5) -> None:
175
+ """精确滑动(起止坐标)。"""
176
+
177
+ @abstractmethod
178
+ def swipe_dir(self, direction: str, distance: int = 60,
179
+ area=None, speed=None) -> None:
180
+ """方向滑动:'UP' / 'DOWN' / 'LEFT' / 'RIGHT'。"""
181
+
182
+ @abstractmethod
183
+ def drag(self, x1: int, y1: int, x2: int, y2: int,
184
+ duration: float = 0.5) -> None:
185
+ """拖拽。"""
186
+
187
+ @abstractmethod
188
+ def fling(self, direction: str, distance: int = 50,
189
+ area=None, speed: str = "fast") -> None:
190
+ """抛滑(快速惯性滑动)。"""
191
+
192
+ # ============================================================
193
+ # 实时触控(支持 down/move/up 序列,用于实时拖拽等场景)
194
+ # ============================================================
195
+
196
+ @abstractmethod
197
+ def touch_down(self, x: int, y: int) -> None:
198
+ """按下触控点。"""
199
+
200
+ @abstractmethod
201
+ def touch_move(self, x: int, y: int) -> None:
202
+ """移动触控点(需先 touch_down)。"""
203
+
204
+ @abstractmethod
205
+ def touch_up(self, x: int, y: int) -> None:
206
+ """抬起触控点(结束触控序列)。"""
207
+
208
+ # ============================================================
209
+ # 按键
210
+ # ============================================================
211
+
212
+ @abstractmethod
213
+ def press(self, key: str) -> None:
214
+ """语义按键:'back' / 'home' / 'power' / 'volume_up' / ..."""
215
+
216
+ @abstractmethod
217
+ def press_keycode(self, keycode: int) -> None:
218
+ """按键码(平台原始按键码,由平台实现自行解释)。"""
219
+
220
+ @abstractmethod
221
+ def go_home(self) -> None:
222
+ """返回桌面(自动选最稳定路径)。"""
223
+
224
+ @abstractmethod
225
+ def go_back(self) -> None:
226
+ """返回上一级(自动选最稳定路径)。"""
227
+
228
+ @abstractmethod
229
+ def press_power(self) -> None:
230
+ """按下电源键。"""
231
+
232
+ @abstractmethod
233
+ def press_combination_key(self, key1: int, key2: int,
234
+ key3: Optional[int] = None) -> None:
235
+ """按下组合键(支持 2 键或 3 键)。"""
236
+
237
+ # ============================================================
238
+ # Shell
239
+ # ============================================================
240
+
241
+ @abstractmethod
242
+ def shell(self, cmd: str, timeout: float = 60) -> str:
243
+ """执行 shell 命令,返回回显内容。"""
244
+
245
+ # ============================================================
246
+ # 截图
247
+ # ============================================================
248
+
249
+ @abstractmethod
250
+ def screenshot(self, filename: Optional[str] = None,
251
+ area=None) -> Union['Image', str, None]:
252
+ """截图。
253
+
254
+ - filename 为 None:返回 PIL.Image.Image
255
+ - filename 指定:保存到文件,返回文件路径 str
256
+ - area 指定区域截图(Rect / SelectorSpec / None)
257
+ - 失败:返回 None
258
+ """
259
+
260
+ @abstractmethod
261
+ def dump_hierarchy(self, source: str = "rpc",
262
+ filename: Optional[str] = None) -> Union[dict, str, None]:
263
+ """导出控件树。
264
+
265
+ - source: 获取方式
266
+ - "rpc": 走 uitest RPC(Captures.captureLayout),直接返回控件树 JSON,默认
267
+ - "hdc": 走 hdc shell(uitest dumpLayout -p),独立于 RPC 守护进程状态
268
+ - filename 为 None:返回解析后的 dict
269
+ - filename 指定:保存 JSON 到文件,返回文件路径 str
270
+ - 失败:返回 None
271
+ """
272
+
273
+ # ============================================================
274
+ # 等待
275
+ # ============================================================
276
+
277
+ @abstractmethod
278
+ def wait(self, seconds: float) -> None:
279
+ """强制等待指定秒数。"""
280
+
281
+ @abstractmethod
282
+ def wait_for_idle(self, idle_time: float = 0.7,
283
+ timeout: float = 10) -> None:
284
+ """等待 UI 进入空闲状态。"""
285
+
286
+ @abstractmethod
287
+ def implicitly_wait(self, seconds: float) -> None:
288
+ """设置隐式等待。"""
289
+
290
+ # ============================================================
291
+ # 窗口
292
+ # ============================================================
293
+
294
+ @property
295
+ @abstractmethod
296
+ def window(self) -> 'BaseWindow':
297
+ """当前窗口对象。"""
298
+
299
+ @abstractmethod
300
+ def get_windows(self) -> List['BaseWindow']:
301
+ """获取所有窗口。"""
302
+
303
+ # ============================================================
304
+ # webview 自动化(可选实现,不支持时抛 NotImplementedError)
305
+ # ============================================================
306
+
307
+ def webview(self, bundle_name: str, **kwargs):
308
+ """连接应用 webview,返回 selenium webdriver 封装。
309
+
310
+ 平台不支持 webview 测试时抛 NotImplementedError。
311
+ """
312
+ raise NotImplementedError("当前平台不支持 webview 自动化")
313
+
314
+ # ============================================================
315
+ # 文件操作
316
+ # ============================================================
317
+
318
+ @abstractmethod
319
+ def push_file(self, local_path: str, remote_path: str,
320
+ timeout: int = 60) -> None:
321
+ """推送文件到设备。"""
322
+
323
+ @abstractmethod
324
+ def pull_file(self, remote_path: str,
325
+ local_path: Optional[str] = None,
326
+ timeout: int = 60) -> None:
327
+ """从设备拉取文件。"""
328
+
329
+ @abstractmethod
330
+ def has_file(self, path: str) -> bool:
331
+ """查询设备端文件是否存在。"""
332
+
333
+ # ============================================================
334
+ # 控件查找
335
+ # ============================================================
336
+
337
+ @abstractmethod
338
+ def find_component(self, target, scroll_target=None) -> Optional['BaseComponent']:
339
+ """查找控件返回对象。"""
340
+
341
+ @abstractmethod
342
+ def find_all_components(self, target) -> List['BaseComponent']:
343
+ """查找所有匹配控件。"""
344
+
345
+ @abstractmethod
346
+ def get_component_bound(self, target) -> Optional[Any]:
347
+ """获取控件边界 Rect,未找到返回 None。"""
348
+
349
+ @abstractmethod
350
+ def get_component_property(self, target, name: str) -> Any:
351
+ """获取控件指定属性(id/text/key/type/enabled/focused/clickable/scrollable/checked/checkable)。"""
352
+
353
+ # ============================================================
354
+ # 图片识别
355
+ # ============================================================
356
+
357
+ @abstractmethod
358
+ def find_image(self, image_path: str,
359
+ mode: str = "sift") -> Optional[Any]:
360
+ """在屏幕上查找图片位置,返回 Rect 或 None。"""
361
+
362
+ @abstractmethod
363
+ def touch_image(self, image_path: str, mode: str = "normal",
364
+ similarity: float = 0.95) -> None:
365
+ """点击图片位置。"""
366
+
367
+ # ============================================================
368
+ # 文本输入辅助
369
+ # ============================================================
370
+
371
+ @abstractmethod
372
+ def hide_keyboard(self) -> None:
373
+ """隐藏软键盘。"""
374
+
375
+ @abstractmethod
376
+ def input_text_on_cursor(self, text: str) -> None:
377
+ """在当前光标处输入文本(不依赖控件定位)。"""
378
+
379
+ @abstractmethod
380
+ def move_cursor(self, direction: str, times: int = 1) -> None:
381
+ """移动输入框光标:'LEFT' / 'RIGHT' / 'UP' / 'DOWN' / 'BEGIN' / 'END'。"""
382
+
383
+ # ============================================================
384
+ # 手势扩展
385
+ # ============================================================
386
+
387
+ @abstractmethod
388
+ def inject_gesture(self, gesture, speed: int = 2000) -> None:
389
+ """自定义手势。"""
390
+
391
+ # ============================================================
392
+ # 手势导航
393
+ # ============================================================
394
+
395
+ @abstractmethod
396
+ def swipe_to_home(self, times: int = 1) -> None:
397
+ """屏幕底端上滑回到桌面(需开启手势导航)。"""
398
+
399
+ @abstractmethod
400
+ def swipe_to_back(self, side: str = "LEFT", times: int = 1,
401
+ height: float = 0.5) -> None:
402
+ """侧滑返回:side='LEFT'/'RIGHT',height 为屏幕高度比例。"""
403
+
404
+ @abstractmethod
405
+ def swipe_to_recent_task(self) -> None:
406
+ """底端上滑停顿进入多任务界面。"""
407
+
408
+ # ============================================================
409
+ # 坐标转换
410
+ # ============================================================
411
+
412
+ @abstractmethod
413
+ def to_abs_pos(self, x: float, y: float) -> Tuple[int, int]:
414
+ """比例坐标转绝对坐标。"""
@@ -0,0 +1,25 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """BaseWindow:跨平台窗口对象契约。"""
4
+ from __future__ import annotations
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, List, Tuple
8
+
9
+
10
+ class BaseWindow(ABC):
11
+ """窗口契约,平台实现需继承并实现全部抽象方法。"""
12
+
13
+ @property
14
+ @abstractmethod
15
+ def size(self) -> Tuple[int, int]:
16
+ """窗口大小 (width, height)。"""
17
+
18
+ @property
19
+ @abstractmethod
20
+ def info(self) -> Dict[str, Any]:
21
+ """窗口信息。"""
22
+
23
+ @abstractmethod
24
+ def get_windows(self) -> List['BaseWindow']:
25
+ """所有窗口对象列表。"""
@@ -0,0 +1,102 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """SelectorSpec:纯数据选择器规格。
4
+
5
+ 对齐 U2 单对象模型:d(**kwargs) 直接返回 UiObject,SelectorSpec 仅作为
6
+ 内部纯数据类封装控件定位条件,不向用户暴露任何操作接口。所有操作与
7
+ 关系方法统一在 UiObject / BaseComponent 上。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from typing import Any, Dict, Optional
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class SelectorSpec:
17
+ """控件定位条件的纯数据封装(内部使用,不直接暴露给用户)。
18
+
19
+ 用户通过 d(text="x", resourceId="y") 构造,由 UiObject 持有。
20
+ frozen=True 保证不可变,关系选择器衍生新实例时安全。
21
+ """
22
+
23
+ # 文本匹配
24
+ text: Optional[str] = None
25
+ text_contains: Optional[str] = None
26
+ text_starts_with: Optional[str] = None
27
+ text_ends_with: Optional[str] = None
28
+ text_matches: Optional[str] = None
29
+ text_matches_flags: Optional[int] = None
30
+
31
+ # 描述匹配
32
+ desc: Optional[str] = None
33
+ desc_contains: Optional[str] = None
34
+ desc_starts_with: Optional[str] = None
35
+ desc_ends_with: Optional[str] = None
36
+ desc_matches: Optional[str] = None
37
+
38
+ # 通用定位
39
+ resource_id: Optional[str] = None # 对应 U2 resourceId / id
40
+ class_name: Optional[str] = None # 对应 U2 className / class_
41
+ key: Optional[str] = None # 鸿蒙 key 选择器
42
+ type: Optional[str] = None # 鸿蒙 type 选择器
43
+ index: Optional[int] = None
44
+ instance: Optional[int] = None
45
+
46
+ # 关系选择器
47
+ parent: Optional['SelectorSpec'] = None
48
+ relation: Optional[str] = None # 'child' / 'sibling' / 'after' / 'before'
49
+
50
+ # xpath
51
+ xpath: Optional[str] = None
52
+
53
+ def merge(self, **kwargs) -> 'SelectorSpec':
54
+ """合并新的选择条件,返回新实例(不可变)。"""
55
+ normalized = _normalize_aliases(kwargs)
56
+ merged = {**self.__dict__, **normalized}
57
+ return SelectorSpec(**merged)
58
+
59
+ @classmethod
60
+ def with_relation(cls, parent: 'SelectorSpec', relation: str,
61
+ child: 'SelectorSpec') -> 'SelectorSpec':
62
+ """构造关系选择器,避免 parent/relation 重复传参。"""
63
+ child_data = {
64
+ key: value
65
+ for key, value in child.__dict__.items()
66
+ if key not in ('parent', 'relation')
67
+ }
68
+ return cls(parent=parent, relation=relation, **child_data)
69
+
70
+
71
+ # U2 / 鸿蒙选择器别名 → 内部字段名
72
+ _ALIAS_MAPPING = {
73
+ 'className': 'class_name',
74
+ 'class_': 'class_name',
75
+ 'resourceId': 'resource_id',
76
+ 'id': 'resource_id',
77
+ 'description': 'desc',
78
+ 'textContains': 'text_contains',
79
+ 'textStartswith': 'text_starts_with',
80
+ 'textStartsWith': 'text_starts_with',
81
+ 'textEndswith': 'text_ends_with',
82
+ 'textEndsWith': 'text_ends_with',
83
+ 'textMatches': 'text_matches',
84
+ 'flags': 'text_matches_flags',
85
+ 'descContains': 'desc_contains',
86
+ 'descStartswith': 'desc_starts_with',
87
+ 'descStartsWith': 'desc_starts_with',
88
+ 'descEndswith': 'desc_ends_with',
89
+ 'descEndsWith': 'desc_ends_with',
90
+ 'descMatches': 'desc_matches',
91
+ }
92
+
93
+
94
+ def _normalize_aliases(kwargs: Dict[str, Any]) -> Dict[str, Any]:
95
+ """U2 / 鸿蒙选择器别名归一化为内部字段。"""
96
+ return {_ALIAS_MAPPING.get(key, key): value for key, value in kwargs.items()}
97
+
98
+
99
+ def build_selector(**kwargs) -> SelectorSpec:
100
+ """从用户 kwargs 构建 SelectorSpec(含别名归一化)。"""
101
+ normalized = _normalize_aliases(kwargs)
102
+ return SelectorSpec(**normalized)
devhelmkit/entry.py ADDED
@@ -0,0 +1,90 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """设备连接入口:平台识别、设备发现与平台分发。
4
+
5
+ 平台实现延迟导入,避免导入包时触发设备环境探测。
6
+ """
7
+ from typing import Optional
8
+
9
+ from devhelmkit.core.base_driver import BaseDriver
10
+ from devhelmkit.exceptions import (
11
+ DeviceConnectError,
12
+ DeviceNotFoundError,
13
+ PlatformNotSupportedError,
14
+ )
15
+ from devhelmkit.utils.logger import setup_logger
16
+
17
+
18
+ def connect(
19
+ serial: Optional[str] = None,
20
+ platform: str = "auto",
21
+ config=None,
22
+ log_level: int = None,
23
+ **kwargs
24
+ ) -> BaseDriver:
25
+ """连接设备。
26
+
27
+ Args:
28
+ serial: 设备序列号,None 时自动发现。
29
+ platform: 平台,"auto" | "harmony" | "android"。
30
+ config: 平台专用配置对象(如 HarmonyDriverConfig)。
31
+ log_level: 日志级别,None 默认 INFO。传入 logging.DEBUG 可开启调试日志。
32
+ **kwargs: 平台特定参数,覆盖 config 同名字段。
33
+
34
+ Returns:
35
+ BaseDriver: 设备驱动实例(具体平台子类)。
36
+
37
+ Raises:
38
+ DeviceNotFoundError: 未检测到可连接设备。
39
+ DeviceConnectError: 设备连接失败。
40
+ PlatformNotSupportedError: 平台不支持。
41
+ """
42
+ import logging
43
+ setup_logger(log_level if log_level is not None else logging.INFO)
44
+
45
+ if serial is None:
46
+ serial = _discover_serial()
47
+
48
+ selected_platform = _detect_platform(serial) if platform == "auto" else platform
49
+
50
+ if selected_platform == "harmony":
51
+ from devhelmkit.harmony.driver import HarmonyDriver
52
+ return HarmonyDriver(serial, config=config, **kwargs)
53
+
54
+ if selected_platform == "android":
55
+ raise PlatformNotSupportedError("Android 平台暂未接入,阶段四实现")
56
+
57
+ raise PlatformNotSupportedError("不支持的平台: %s" % selected_platform)
58
+
59
+
60
+ def _discover_serial() -> str:
61
+ """自动发现首个可用设备序列号。"""
62
+ try:
63
+ from devhelmkit.harmony.device.hdc import HdcDevice
64
+ targets = HdcDevice.list_targets()
65
+ except Exception as exc:
66
+ raise DeviceConnectError("鸿蒙设备发现失败: %s" % exc) from exc
67
+
68
+ if not targets:
69
+ raise DeviceNotFoundError(
70
+ "未检测到可连接设备,请手动指定 serial 或 platform"
71
+ )
72
+ return targets[0]
73
+
74
+
75
+ def _detect_platform(serial: Optional[str]) -> str:
76
+ """自动识别平台:当前仅识别鸿蒙设备。"""
77
+ try:
78
+ from devhelmkit.harmony.device.hdc import HdcDevice
79
+ targets = HdcDevice.list_targets()
80
+ except Exception as exc:
81
+ raise DeviceConnectError("鸿蒙设备发现失败: %s" % exc) from exc
82
+
83
+ if serial is None and targets:
84
+ return "harmony"
85
+ if serial is not None and serial in targets:
86
+ return "harmony"
87
+
88
+ raise DeviceNotFoundError(
89
+ "未检测到可连接设备,请手动指定 serial 或 platform"
90
+ )
@@ -0,0 +1,56 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """统一异常体系。
4
+
5
+ 异常类型单文件定义,避免各平台重复定义。命名约束:
6
+ - DevhelmTimeoutError 不与 Python 内建 TimeoutError 同名冲突;
7
+ - DeviceConnectError 作为设备连接失败的唯一命名;
8
+ - ComponentNotFoundError 作为控件未找到的唯一命名。
9
+ """
10
+ from typing import Optional
11
+
12
+
13
+ class DevhelmError(Exception):
14
+ """框架基础异常,所有 devhelmkit 异常的根。"""
15
+
16
+
17
+ class DeviceNotFoundError(DevhelmError):
18
+ """未检测到可连接设备。"""
19
+
20
+
21
+ class DeviceConnectError(DevhelmError):
22
+ """设备连接失败(hdc 连接、RPC socket 建立失败等)。"""
23
+
24
+
25
+ class PlatformNotSupportedError(DevhelmError):
26
+ """平台不支持(如阶段一调用 connect(platform="android"))。"""
27
+
28
+
29
+ class DevhelmTimeoutError(DevhelmError):
30
+ """框架操作超时,刻意不与 Python 内建 TimeoutError 同名。"""
31
+
32
+
33
+ class RpcError(DevhelmError):
34
+ """RPC 通信异常,携带 api 与原始 reply 上下文便于诊断。"""
35
+
36
+ def __init__(self, message: str, api: Optional[str] = None,
37
+ reply: Optional[str] = None):
38
+ super().__init__(message)
39
+ self.api = api
40
+ self.reply = reply
41
+
42
+
43
+ class BackendObjectDroppedError(RpcError):
44
+ """设备端远程对象引用已失效,需恢复后重试。"""
45
+
46
+
47
+ class ComponentNotFoundError(DevhelmError):
48
+ """控件未找到。"""
49
+
50
+
51
+ class ComponentDisappearedError(ComponentNotFoundError):
52
+ """控件查找到后又消失。"""
53
+
54
+
55
+ class AgentError(DevhelmError):
56
+ """设备端 uitest / Agent 异常(进程崩溃、版本不支持等)。"""
@@ -0,0 +1,3 @@
1
+ # !/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """HarmonyOS 平台实现包。"""