base_control.py 13 KB

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