human_touch.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # -*- coding: utf-8 -*-
  2. """
  3. 拟人触摸滑动 — 人体关节模型(手腕枢轴)弧线轨迹 + motionevent 连续手势注入
  4. 供 main1(列表滑动/说明书上滑) 与 snapshot(快照滚动) 共用。
  5. 轨迹模型: 一笔画 = 手腕绕枢轴转动的单向弧(二次贝塞尔),
  6. 起终点横向错开(漂移量与笔画长度相关), 速度 smoothstep 缓起-快中-缓收, 带 ±2px 指尖微抖。
  7. 注入方式: 单个 adb shell 会话内连发 input motionevent 触点(手指全程不抬),
  8. ~45ms/点是未root user机的物理上限; /dev/input(sendevent) 被SELinux封死不可用。
  9. """
  10. import json
  11. import random
  12. import socket
  13. import subprocess
  14. import sys
  15. import threading
  16. import time
  17. # Windows 默认定时器精度 ~15.6ms, time.sleep(0.004) 实际睡 ~15.6ms,
  18. # 导致滑动触点节奏全部变慢(设计4ms实跑15ms)。进程内调到 1ms 精度,一次性全局生效。
  19. if sys.platform == "win32":
  20. try:
  21. import ctypes
  22. ctypes.windll.winmm.timeBeginPeriod(1)
  23. except Exception:
  24. pass
  25. from uiautomator2.core import AdbHTTPConnection
  26. class TouchPipe:
  27. """uiautomator2 9008端口的keep-alive触点长连接(来自 yzm/tmp_captcha_test6.py, 验证码同款)。
  28. 每个触点 = 一条极小的 injectInputEvent 请求(~1~3ms), 比 input 命令(45ms/点)快20倍。"""
  29. def __init__(self, dev):
  30. self._dev = dev
  31. self._conn = None
  32. self._sock = None
  33. self._stop = threading.Event()
  34. self._drainer = None
  35. self._lock = threading.Lock()
  36. self._fallback = False
  37. self._open_ok = False
  38. def open(self):
  39. try:
  40. self._conn = AdbHTTPConnection(self._dev.adb_device, port=9008)
  41. self._conn.timeout = 15
  42. self._conn.connect()
  43. self._sock = self._conn.sock
  44. self._sock.settimeout(0.5)
  45. self._drainer = threading.Thread(target=self._drain, daemon=True)
  46. self._drainer.start()
  47. self._open_ok = True
  48. except Exception:
  49. self._fallback = True
  50. self._open_ok = False
  51. return self
  52. def _drain(self):
  53. while not self._stop.is_set():
  54. try:
  55. if not self._sock.recv(65536):
  56. break
  57. except Exception:
  58. continue
  59. def move(self, x, y, pressure=0):
  60. if self._fallback:
  61. self._dev.touch.move(int(x), int(y))
  62. return
  63. with self._lock:
  64. self._sock.sendall(self._req(x, y, pressure))
  65. def _req(self, x, y, pressure):
  66. body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "injectInputEvent",
  67. "params": [2, int(x), int(y), 0]}).encode()
  68. return (f"POST /jsonrpc/0 HTTP/1.1\r\nHost: localhost\r\n"
  69. f"User-Agent: u2\r\nAccept-Encoding: \r\n"
  70. f"Content-Type: application/json\r\nContent-Length: {len(body)}\r\n"
  71. f"Connection: keep-alive\r\n\r\n").encode() + body
  72. def close(self):
  73. self._stop.set()
  74. try:
  75. if self._sock:
  76. self._sock.shutdown(socket.SHUT_RDWR) # 立即打断drainer的recv,不等0.5s超时
  77. except Exception:
  78. pass
  79. try:
  80. if self._sock:
  81. self._sock.close()
  82. except Exception:
  83. pass
  84. if self._drainer:
  85. self._drainer.join(timeout=0.2)
  86. def touchpipe_drag(d, pts: list, step: float = None, hold: float = None,
  87. tail: float = None) -> bool:
  88. """TouchPipe 长连发送一笔弧线(60点/100Hz级, 手指全程不抬, 丝滑+弧线)。
  89. d = uiautomator2 设备对象。返回 True=成功, False=通道不可用(调用方降级)。
  90. hold: 按下后停顿(默认0.08~0.14s拟人); tail: 抬起前停顿(默认0.03~0.06s);
  91. 轻扫场景(如图片左滑)传更小的 step/hold/tail。
  92. 注: 按住 ~500ms 不动 App 会判长按(列表页误触"店铺问题"反馈弹窗),
  93. 故 down→首move 间隔超 0.35s 即中止; 异常时 finally 必补 UP, 防手指钉在屏幕上。"""
  94. if step is None:
  95. step = random.uniform(0.008, 0.014)
  96. if hold is None:
  97. hold = random.uniform(0.08, 0.14)
  98. if tail is None:
  99. tail = random.uniform(0.03, 0.06)
  100. pipe = None
  101. up_sent = False
  102. try:
  103. pipe = TouchPipe(d).open()
  104. if not pipe._open_ok:
  105. return False
  106. t0 = time.time()
  107. d.touch.down(int(pts[0][0]), int(pts[0][1]))
  108. time.sleep(hold) # 按住停顿(验证码同款手感)
  109. if time.time() - t0 > 0.35: # down延迟过久→一动就成长按
  110. return False
  111. for (x, y) in pts[1:-1]:
  112. time.sleep(step)
  113. pipe.move(int(x), int(y), 0)
  114. time.sleep(tail)
  115. d.touch.up(int(pts[-1][0]), int(pts[-1][1]))
  116. up_sent = True
  117. return True
  118. except Exception:
  119. return False
  120. finally:
  121. if not up_sent:
  122. try: # 补发UP,防指针残留触发长按
  123. d.touch.up(int(pts[-1][0]), int(pts[-1][1]))
  124. except Exception:
  125. pass
  126. if pipe is not None:
  127. pipe.close()
  128. def wrist_arc_pts(w: int, h: int, distance: int, n: int = 10,
  129. drift_range=(80, 140)) -> list:
  130. """生成一笔弧线的触点序列 [(x,y), ...]
  131. drift_range: 起终点横向漂移范围(短笔画给小值, 如(40,90))"""
  132. drift = random.choice([-1, 1]) * random.randint(*drift_range)
  133. x0 = int(w * random.uniform(0.40, 0.60))
  134. y0 = int(h * random.uniform(0.70, 0.80))
  135. x2 = x0 + drift
  136. y2 = max(60, y0 - distance)
  137. cx = (x0 + x2) / 2 - drift // 2 # 控制点反向偏 → 单向弧
  138. cy = (y0 + y2) / 2
  139. pts = []
  140. prev_y = None
  141. for i in range(n):
  142. t = i / (n - 1)
  143. te = t * t * (3 - 2 * t) # smoothstep 缓起-快中-缓收
  144. x = (1 - te) ** 2 * x0 + 2 * (1 - te) * te * cx + te ** 2 * x2
  145. y = (1 - te) ** 2 * y0 + 2 * (1 - te) * te * cy + te ** 2 * y2
  146. # 指尖微抖只作用横向; y保证严格上行(密集触点时防1~2px反向)
  147. x += random.randint(-2, 2)
  148. y = int(y)
  149. if prev_y is not None and y >= prev_y:
  150. y = prev_y - 1
  151. pts.append((int(x), y))
  152. prev_y = y
  153. return pts
  154. # ── 单手拇指侧滑(列表两段式用) ──
  155. # 手性区: 起指在拇指自然落点(屏高74%~86%), 弧线中段扫向对侧, 终点在本侧偏上
  156. RIGHT_SIDE_ZONE = {"x": (0.42, 0.65), "y": (0.74, 0.86), "drift": (40, 140)}
  157. LEFT_SIDE_ZONE = {"x": (0.35, 0.58), "y": (0.74, 0.86), "drift": (-140, -40)}
  158. def wrist_arc_pts_side(w: int, h: int, distance: int, zone: dict,
  159. anchor=None, bow_ratio: float = 0.2, n: int = 60) -> list:
  160. """单手拇指侧滑轨迹(两段式每笔用)。
  161. anchor: 拇指自然落点(同一滑动的两笔共用,起指位置靠近); None=在zone内随机
  162. bow_ratio: 弓形强度×距离, 符号=凸向(正=凸右,负=凸左); 右手传负值 → "("形
  163. 起伏基准: 弦偏差≈bow_ratio×distance/2, 生产用0.15~0.25 → 40~60px"""
  164. drift = random.randint(*zone["drift"]) # 终点横移(方向=手性)
  165. if anchor is None:
  166. x0 = int(w * random.uniform(*zone["x"]))
  167. y0 = int(h * random.uniform(*zone["y"]))
  168. else:
  169. x0 = int(anchor[0] + random.randint(-30, 30))
  170. y0 = int(anchor[1] + random.randint(-30, 30))
  171. x2 = max(20, min(w - 20, x0 + drift)) # 终点边界保护,防甩出屏幕
  172. y2 = max(60, y0 - distance)
  173. bow = bow_ratio * distance # 符号=凸向
  174. cx = (x0 + x2) / 2 + bow
  175. cy = (y0 + y2) / 2
  176. pts, prev_y = [], None
  177. for i in range(n):
  178. t = i / (n - 1)
  179. te = t * t * (3 - 2 * t) # smoothstep 缓起-快中-缓收
  180. x = (1 - te) ** 2 * x0 + 2 * (1 - te) * te * cx + te ** 2 * x2
  181. y = (1 - te) ** 2 * y0 + 2 * (1 - te) * te * cy + te ** 2 * y2
  182. # 指尖微抖只作用横向; y保证严格上行(密集触点时防1~2px反向)
  183. x += random.randint(-2, 2)
  184. y = int(y)
  185. if prev_y is not None and y >= prev_y:
  186. y = prev_y - 1
  187. pts.append((int(x), y))
  188. prev_y = y
  189. return pts
  190. def wrist_arc_pts_h(w: int, h: int, distance: int, y_ratio: float = 0.3,
  191. n: int = 10, drift_range=(30, 70)) -> list:
  192. """横向弧线: 从右往左滑(商品图轮播)。纵向漂移 = 手腕横向发力时的上下弓弧"""
  193. dy = random.choice([-1, 1]) * random.randint(*drift_range)
  194. x0 = int(w * random.uniform(0.72, 0.85))
  195. y0 = int(h * y_ratio) + random.randint(-30, 30)
  196. x2 = max(40, x0 - distance)
  197. y2 = y0 + dy
  198. cx = (x0 + x2) / 2
  199. cy = (y0 + y2) / 2 + random.choice([-1, 1]) * random.randint(20, 50) # 垂直于运动方向的弓
  200. pts = []
  201. prev_x = None
  202. for i in range(n):
  203. t = i / (n - 1)
  204. te = t * t * (3 - 2 * t)
  205. x = (1 - te) ** 2 * x0 + 2 * (1 - te) * te * cx + te ** 2 * x2
  206. y = (1 - te) ** 2 * y0 + 2 * (1 - te) * te * cy + te ** 2 * y2
  207. # 纵向微抖; x保证严格递减(横向轮播滑动不回退)
  208. y += random.randint(-2, 2)
  209. x = int(x)
  210. if prev_x is not None and x >= prev_x:
  211. x = prev_x - 1
  212. pts.append((x, int(y)))
  213. prev_x = x
  214. return pts
  215. def motionevent_drag(device_id: str, pts: list, step: float = None):
  216. """把触点序列作为一笔连续手势注入(单shell会话连发)"""
  217. if step is None:
  218. step = random.uniform(0.030, 0.045)
  219. lines = [f"input motionevent DOWN {pts[0][0]} {pts[0][1]}"]
  220. for (x, y) in pts[1:-1]:
  221. lines.append(f"sleep {step:.3f}")
  222. lines.append(f"input motionevent MOVE {x} {y}")
  223. lines.append(f"input motionevent UP {pts[-1][0]} {pts[-1][1]}")
  224. subprocess.run(["adb", "-s", device_id, "shell", "sh"],
  225. input=("\n".join(lines) + "\n").encode("ascii"),
  226. capture_output=True, timeout=60)