base_control.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. # 定义抽象类
  2. import json
  3. import logging
  4. import tempfile
  5. from abc import ABC, abstractmethod
  6. from typing import List
  7. import uiautomator2 as u2
  8. from uiautomator2.exceptions import LaunchUiAutomationError
  9. class BaseControl(ABC):
  10. # ctx_dict = {
  11. # 'connect': 1,
  12. # 'platform': 2,
  13. # }
  14. # 连接信息
  15. connect_dict = {
  16. }
  17. app_pkg = {
  18. 'com.niocpeed.dna': 'DeepCoin',
  19. }
  20. def __init__(self, name: str, ctx: int = 2):
  21. """
  22. 软件UI交互的控制器
  23. :param name:
  24. :param ctx:
  25. """
  26. # 名称
  27. self.name = name
  28. self.ctx = ctx
  29. pass
  30. def init_adb(self, commands: List[str], *args, **kwargs):
  31. """
  32. 连接设备
  33. 使用 python shell执行命令
  34. """
  35. pass
  36. def devices_list(self, *args, **kwargs):
  37. """
  38. 获取设备列表 adb devices
  39. :return:
  40. """
  41. pass
  42. def print_log(self, msg):
  43. logging.info(f'打印信息 {msg}')
  44. def connect_adb(self, serial: str):
  45. """
  46. 连接设备
  47. """
  48. logging.info(f'连接设备[{self.name}]:{serial}', )
  49. try:
  50. d = u2.connect(serial)
  51. # self.enable_click_monitor()
  52. logging.info(f"设置成功:{d}")
  53. d.shell('settings put system pointer_location 1')
  54. d.shell('settings put system show_touches 1')
  55. # d.debug = True
  56. return d
  57. except LaunchUiAutomationError as e:
  58. logging.error(f"uiautomator2 连接失败: {e}")
  59. def re_connect(self):
  60. """
  61. 重新建立连接
  62. :return:
  63. """
  64. pass
  65. # def enable_click_monitor(self):
  66. # """
  67. # 启用点击监控,在屏幕上显示点击位置
  68. # """
  69. # # self.d.settings['operation_delay'] = (0.5, 0.5) # 增加操作延迟以便观察
  70. # # self.d.debug = True
  71. # self.d.toast.show('点击监控已启用') # 显示提示
  72. #
  73. # # 确保有悬浮窗权限
  74. # # self.d.set_fastinput_ime(True) # 启用ime
  75. # # self.d.show_float_window(True) # 显示悬浮窗
  76. #
  77. # # 可选:打开开发者选项中的"指针位置"
  78. # self.d.shell('settings put system pointer_location 1')
  79. class UIControl(ABC):
  80. @abstractmethod
  81. def event_1(self, **kwargs):
  82. """
  83. F1 开仓界面,仓位滑竿百分比(30-60)
  84. """
  85. pass
  86. @abstractmethod
  87. def event_2(self, **kwargs):
  88. """
  89. F2 确认开仓,开多 限价职中间数值
  90. 检查撤销限价委托单
  91. """
  92. pass
  93. @abstractmethod
  94. def event_3(self, **kwargs):
  95. """
  96. F3 平仓界面 平空,仓位滑竿百分比(90-100)确认开仓,限价职中间数值
  97. """
  98. pass
  99. @abstractmethod
  100. def event_4(self, **kwargs):
  101. """
  102. F4 确认平仓 平多 限价取中间数值 检查撤销限价委托单
  103. """
  104. pass
  105. @abstractmethod
  106. def event_5(self, **kwargs):
  107. """
  108. F5 开仓界面 仓位滑竿百分比(30-60)
  109. """
  110. pass
  111. @abstractmethod
  112. def event_6(self, **kwargs):
  113. """
  114. F6 确认开仓, 开空 限价值中间数值
  115. 检查撤销限价委托单
  116. """
  117. pass
  118. @abstractmethod
  119. def event_7(self, **kwargs):
  120. """
  121. F7 平仓界面二仓位滑竿百分比(90-100)
  122. """
  123. pass
  124. @abstractmethod
  125. def event_8(self, **kwargs):
  126. """
  127. 确认平仓, 平空 限价职中间数值
  128. 检查撤销限价委托单
  129. """
  130. pass
  131. @abstractmethod
  132. def event_9(self, **kwargs):
  133. """
  134. 一键撤销全部委托订单
  135. """
  136. pass
  137. @abstractmethod
  138. def event_10(self, **kwargs):
  139. """
  140. 获取 订单状态.
  141. :param kwargs:
  142. :return:
  143. """
  144. @abstractmethod
  145. def event_11(self, **kwargs):
  146. """
  147. 一键全平
  148. :param kwargs:
  149. :return:
  150. """
  151. class AbsControl(UIControl):
  152. def __init__(self, serial: str, ctx: dict, *args, **kwargs):
  153. """
  154. 用于平台交互的控制器
  155. :param name: 当前名称
  156. :param ctx: 控制器上下文
  157. :param args:
  158. :param kwargs:
  159. """
  160. # u2.logger.setLevel(logging.DEBUG)
  161. self.serial = serial
  162. self.ctx = ctx
  163. self.info = ctx['info']
  164. #
  165. self.d = ctx['d']
  166. # 屏幕高度
  167. self.height = 0
  168. # 屏幕宽度
  169. self.width = 0
  170. self._points = {}
  171. self._func = []
  172. self._log_func = None
  173. def prevent_sleep(self):
  174. """
  175. 防止设备休眠
  176. """
  177. self.d.wake()
  178. self.d.screen_on()
  179. self.d.unlock()
  180. def screenshot(self):
  181. """
  182. 截图
  183. """
  184. # 获取系统/tmp路径
  185. tmp = tempfile.gettempdir()
  186. return self.d.screenshot(f"{tmp}/{self.serial.replace(':', '_')}.png")
  187. def to_top_swipe(self, sleep=0.1, times=3):
  188. """
  189. 滑动到屏幕最顶部,通过多次滑动确保到达顶部
  190. :param sleep: 滑动后等待时间
  191. :param times: 滑动次数
  192. :return:
  193. """
  194. width, height = self.get_screen_size()
  195. # 循环滑动直到无法继续滑动
  196. for _ in range(times):
  197. self.d.swipe(width // 2, height * 0.8, width // 2, height) # 向上滑动
  198. # 短暂等待确保滑动完成
  199. self.d.sleep(sleep)
  200. def to_next_swipe(self, sleep=0.1, y=0.2):
  201. """
  202. 向上滑动一屏
  203. """
  204. width, height = self.get_screen_size()
  205. # 从屏幕下方向上滑动到顶部
  206. self.d.swipe(width // 2, height * 0.8, width // 2, height * y) # 向上滑动
  207. # 短暂等待确保滑动完成
  208. self.d.sleep(sleep)
  209. def click_point(self, x: int, y: int):
  210. """
  211. 点击指定坐标
  212. :param x: x坐标
  213. :param y: y坐标
  214. """
  215. self.d.click(x, y)
  216. def click_xpath(self, xpath: str):
  217. """
  218. 点击指定xpath
  219. """
  220. logging.debug(f'xpath: {xpath}')
  221. el = self.d.xpath(xpath).get()
  222. logging.debug(f'找到元素{xpath}:{el}')
  223. if el:
  224. el.click()
  225. else:
  226. logging.warning(f"未找到元素: {xpath}")
  227. def get_content_desc(self, xpath):
  228. """
  229. 获取xpath节点的完整内容
  230. :param xpath:
  231. :return:
  232. """
  233. logging.info(f"xpath: {xpath}")
  234. el = self.d.xpath(xpath)
  235. # 获取第一个匹配的元素 (如果存在)
  236. element = el.get()
  237. if element:
  238. # 获取元素的完整文本,通常 content-desc 就是元素的文本描述
  239. info = element.info
  240. logging.debug(f"元素的信息: {info}")
  241. logging.debug(f"元素的完整文本: {info['contentDescription']}")
  242. return info['contentDescription']
  243. else:
  244. print("没有找到匹配的元素")
  245. return None
  246. def get_screen_size(self):
  247. """
  248. 获取屏幕尺寸,宽度和高度
  249. """
  250. self.width, self.height = self.d.window_size()
  251. logging.info(f"屏幕尺寸: {self.width}x{self.height}")
  252. return self.width, self.height
  253. def drag_slider_ext(self, start_x: int, start_y: int, end_x: int, end_y: int, steps: int = 50):
  254. """
  255. 精确拖动滑块
  256. :param start_x: 起始x坐标
  257. :param start_y: 起始y坐标
  258. :param end_x: 结束x坐标
  259. :param end_y: 结束y坐标
  260. :param steps: 步数,值越大滑动越平滑
  261. """
  262. self.d.swipe_ext(start_x, start_y, end_x, end_y, steps)
  263. def add_point(self, point=None):
  264. """
  265. 添加坐标点
  266. point = {
  267. "name": "",
  268. "x": 0,
  269. "y": 0,
  270. "desc": "",
  271. "xpath": ""
  272. }
  273. """
  274. logging.info("保存坐标点: %s", point)
  275. self._points[point['name']] = point
  276. def print_log(self, msg):
  277. """
  278. 打印日志
  279. """
  280. logging.info(f'>>111>{msg}')
  281. print(f'>222>>{msg},{self._log_func}')
  282. if self._log_func:
  283. self._log_func(msg)
  284. def add_func(self, func):
  285. """
  286. 添加坐标点采集函数
  287. """
  288. self._func.append(func)
  289. def set_log_func(self, func):
  290. """
  291. 添加日志函数
  292. """
  293. self._log_func = func
  294. def save_point(self):
  295. """
  296. 保存坐标
  297. """
  298. self.to_top_swipe()
  299. self.d.sleep(2)
  300. for func in self._func:
  301. func()
  302. __tmp__ = {}
  303. for k, v in self._points.items():
  304. __tmp__[k] = v
  305. with open(self.point_path, "w", encoding="utf-8") as f:
  306. json.dump(__tmp__, f, ensure_ascii=False, indent=4)
  307. def get_point(self, info: dict):
  308. """
  309. 获取坐标点
  310. :param info: 数据结构
  311. {
  312. "name": "btn_mairu_kaiduo",
  313. "desc": "买入/开多",
  314. "xpath": '//*[@content-desc="买入/开多"]'
  315. }
  316. :return: x, y, el
  317. """
  318. x, y, el = self.get_point_by_xpath(info['xpath'])
  319. # self.add_point({
  320. # "name": info['name'],
  321. # "x": x,
  322. # "y": y,
  323. # "desc": info['desc'],
  324. # "xpath": info['xpath'],
  325. # })
  326. return x, y, el
  327. def get_point_by_xpath(self, xpath: str):
  328. """
  329. :param xpath: xpath
  330. 获取坐标第一个匹配的点
  331. info 数据结构
  332. :return: x, y, el
  333. """
  334. el = self.d.xpath(xpath).get()
  335. x, y = el.center()
  336. return x, y, el
  337. def get_points_by_xpath(self, xpath: str):
  338. """
  339. 获取所有坐标点
  340. info 数据结构
  341. """
  342. els = self.d.xpath(xpath).all()
  343. items = []
  344. for el in els:
  345. x, y = el.center()
  346. items.append((x, y, el))
  347. return items
  348. def check_element_exists(self, xpath: str, timeout: float = 0.05) -> bool:
  349. """
  350. 判断指定XPath的元素是否存在
  351. :param xpath: 元素的XPath
  352. :param timeout: 超时时间(秒)
  353. :return: 是否存在
  354. """
  355. self.d.sleep(timeout)
  356. return self.d.xpath(xpath).exists
  357. def input_xpath(self, xpath: str, text: str, clear: bool = True):
  358. """
  359. 给指定xpath的输入框输入文本
  360. :param xpath: 输入框的xpath
  361. :param text: 要输入的文本
  362. :param clear: 输入前是否清空原有内容
  363. """
  364. element = self.d.xpath(xpath).get()
  365. if element:
  366. if clear:
  367. element.text.set_text("") # 清空原有内容
  368. element.set_text(text) # 输入新内容
  369. else:
  370. logging.warning(f"未找到输入框: {xpath}")
  371. def input_by_position(self, x: int, y: int, text: str, clear: bool = True):
  372. """
  373. 通过坐标点击并在输入框中输入文本
  374. :param x: 输入框x坐标
  375. :param y: 输入框y坐标
  376. :param text: 要输入的文本
  377. :param clear: 是否清空原有内容
  378. """
  379. self.click_point(x, y) # 先点击获取焦点
  380. if clear:
  381. self.d.clear_text() # 清空原有内容
  382. self.d.send_keys(text) # 输入新文本
  383. # self.d.hide_keyboard()
  384. # def i
  385. @abstractmethod
  386. def event_1(self, **kwargs):
  387. """
  388. F1 开仓界面,仓位滑竿百分比(30-60)
  389. """
  390. pass
  391. @abstractmethod
  392. def event_2(self, **kwargs):
  393. """
  394. F2 确认开仓,开多 限价职中间数值
  395. 检查撤销限价委托单
  396. """
  397. pass
  398. @abstractmethod
  399. def event_3(self, **kwargs):
  400. """
  401. F3 平仓界面 平空,仓位滑竿百分比(90-100)确认开仓,限价职中间数值
  402. """
  403. pass
  404. @abstractmethod
  405. def event_4(self, **kwargs):
  406. """
  407. F4 确认平仓 平多 限价取中间数值 检查撤销限价委托单
  408. """
  409. pass
  410. @abstractmethod
  411. def event_5(self, **kwargs):
  412. """
  413. F5 开仓界面 仓位滑竿百分比(30-60)
  414. """
  415. pass
  416. @abstractmethod
  417. def event_6(self, **kwargs):
  418. """
  419. F6 确认开仓, 开空 限价值中间数值
  420. 检查撤销限价委托单
  421. """
  422. pass
  423. @abstractmethod
  424. def event_7(self, **kwargs):
  425. """
  426. F7 平仓界面二仓位滑竿百分比(90-100)
  427. """
  428. pass
  429. @abstractmethod
  430. def event_8(self, **kwargs):
  431. """
  432. F8 确认平仓, 平空 限价职中间数值
  433. 检查撤销限价委托单
  434. """
  435. pass
  436. @abstractmethod
  437. def event_9(self, **kwargs):
  438. """
  439. 一键撤销全部委托订单
  440. :param kwargs:
  441. :return:
  442. """
  443. pass
  444. def event_10(self, **kwargs):
  445. """
  446. 获取订单状态
  447. :param kwargs:
  448. :return:
  449. """
  450. pass
  451. @abstractmethod
  452. def event_11(self, **kwargs):
  453. """
  454. 一键全平
  455. :param kwargs:
  456. :return:
  457. """