| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- """test.py 轨迹 + TouchPipe压感 + 可靠执行"""
- import sys, os, json, time, random, math, threading
- import uiautomator2 as u2
- from uiautomator2.core import AdbHTTPConnection
- import cv2, numpy as np, base64, requests
- from rapidocr_onnxruntime import RapidOCR
- os.environ["PYTHONIOENCODING"] = "utf-8"
- sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- from detect_slider_button import detect_slider_button
- from detect_captcha_edge import detect_captcha_left_edge
- ocr_eng = RapidOCR()
- TOKEN = "1nDVocTE2mJ0yLEYb2sZJ5uUY2VIEoGTkIpW44X7Kgk"
- JFBYM_URL = "http://api.jfbym.com/api/YmServer/customApi"
- IMG_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image", "test2_image")
- OUT = os.path.join(IMG_ROOT, "d")
- ALIGN_DIR = os.path.join(IMG_ROOT, "align")
- SUCCESS_DIR = os.path.join(IMG_ROOT, "success")
- FAILURE_DIR = os.path.join(IMG_ROOT, "failure")
- os.makedirs(OUT, exist_ok=True)
- os.makedirs(ALIGN_DIR, exist_ok=True)
- os.makedirs(SUCCESS_DIR, exist_ok=True)
- os.makedirs(FAILURE_DIR, exist_ok=True)
- OFFSET_COMPENSATE = random.uniform(-7,-5) # MICRO_MODE="fixed" 偏移量(正=右,负=左)
- MICRO_MODE = "fixed" # "fixed"=固定偏移 / "visual"=红绿线对齐 / "none"=不用
- def _clamp(v, lo, hi):
- return max(lo, min(v, hi))
- def _pressure_curve(i, n, phase):
- """压力曲线:开头高→巡航低→末尾高。phase: 'right' 或 'return'。"""
- t = i / max(1, n - 1)
- if t < 0.1: return random.randint(60, 90) # 开头按重
- elif t > 0.90: return random.randint(55, 85) # 末尾对准按重
- else: return random.randint(15, 35) # 巡航轻按
- class TouchPipe:
- def __init__(self, dev):
- self._dev = dev; self._conn = None; self._sock = None
- self._stop = threading.Event(); self._drainer = None
- self._lock = threading.Lock(); self._fallback = False
- def open(self):
- try:
- self._conn = AdbHTTPConnection(self._dev.adb_device, port=9008)
- self._conn.timeout = 15; self._conn.connect()
- self._sock = self._conn.sock; self._sock.settimeout(0.5)
- self._drainer = threading.Thread(target=self._drain, daemon=True)
- self._drainer.start()
- except Exception: self._fallback = True
- return self
- def _drain(self):
- while not self._stop.is_set():
- try:
- if not self._sock.recv(65536): break
- except Exception: continue
- def move(self, x, y, pressure=0):
- if self._fallback:
- try: self._dev.touch.move(int(x), int(y))
- except Exception: pass
- return
- try:
- with self._lock:
- self._sock.sendall(self._req(x, y, pressure))
- except Exception: self._fallback = True; self.move(x, y, pressure)
- def _req(self, x, y, pressure):
- body = json.dumps({"jsonrpc":"2.0","id":1,"method":"injectInputEvent",
- "params":[2,int(x),int(y),int(pressure)]}).encode()
- return (f"POST /jsonrpc/0 HTTP/1.1\r\nHost: localhost\r\n"
- f"User-Agent: u2\r\nAccept-Encoding: \r\n"
- f"Content-Type: application/json\r\nContent-Length: {len(body)}\r\n"
- f"Connection: keep-alive\r\n\r\n").encode() + body
- def close(self):
- self._stop.set()
- if self._drainer: self._drainer.join(timeout=1)
- try:
- if self._sock: self._sock.close()
- except Exception: pass
- def _build_right_track(start_x, start_y, end_x):
- """test.py 原样 — 钟形鼓包 wobble, EMA 0.55"""
- dist = abs(end_x - start_x)
- steps = int(_clamp(dist / random.uniform(6.0, 8.0), 35, 50)) + 20
- end_yd = random.randint(-30, 30)
- wobbles = [(random.uniform(0.15, 0.85), random.uniform(-25, 25))
- for _ in range(random.randint(2, 4))]
- points = [(int(start_x), int(start_y))]
- smooth_y = float(start_y)
- for i in range(1, steps + 1):
- t = i / steps
- x = start_x + dist * (1.0 - (1.0 - t) ** 2.0)
- y = start_y + end_yd * (t ** 0.6)
- if 0.15 < t < 0.9:
- for wp, wv in wobbles:
- dw = abs(t - wp)
- if dw < 0.12: y += wv * (1 - dw / 0.12)
- smooth_y = smooth_y * 0.55 + y * 0.45
- points.append((int(round(x)), int(round(smooth_y))))
- return points
- def _build_human_return_track(start_x, start_y, target_x):
- """test.py 原样 — wobble, EMA 0.55, blend t>0.5 不打勾"""
- distance = start_x - target_x
- if distance <= 0:
- return [(int(target_x), int(start_y))]
- # 点数按折回距离自适应,对标真机但控制上限避免等太久
- if distance > 600:
- steps = random.randint(150, 200) # 长折, Y必往下漂
- end_dy = random.randint(80, 200); cat = '长折'
- elif distance > 450:
- steps = random.randint(120, 170) # 中折
- end_dy = random.randint(-20, 40); cat = '中折'
- else:
- steps = int(_clamp(distance / random.uniform(3.0, 5.0), 35, 55))
- end_dy = random.randint(-20, 40); cat = '短折'
- print(f' 折回: {cat} distance={distance:.0f}px {steps}点 Y漂={end_dy:+d}')
- # 两段: 巡航60%点数走88%距离(大步), 减速40%点数走12%距离(密集蠕动)
- cruise_n = int(steps * 0.60)
- decel_n = steps - cruise_n
- target_y = start_y + end_dy
- r_wobbles = [(random.uniform(0.15, 0.5), random.uniform(-20, 20))
- for _ in range(random.randint(2, 4))]
- track, smooth_y = [], float(start_y)
- cruise_dist = distance * 0.88
- decel_dist = distance * 0.12
- # ── 前90%大步巡航 ──
- for i in range(1, cruise_n + 1):
- t = i / cruise_n
- x = start_x - cruise_dist * t
- y = start_y + (target_y - start_y) * t
- if 0.1 < t < 0.5:
- for wp, wv in r_wobbles:
- dw = abs(t - wp)
- if dw < 0.12: y += wv * (1 - dw / 0.12)
- smooth_y = smooth_y * 0.55 + y * 0.45
- track.append((int(round(x)), int(round(smooth_y))))
- # ── 后10%小步减速对准 ──
- dsx, dsy = track[-1]
- for i in range(1, decel_n + 1):
- tt = i / decel_n
- ease = 1 - (1 - tt) ** 2
- x = dsx - decel_dist * ease
- y = dsy + (target_y - dsy) * tt
- track.append((int(round(x)), int(round(y))))
- if track: track[-1] = (int(target_x), int(target_y))
- return track
- def save(name, img):
- cv2.imwrite(os.path.join(OUT, name), img)
- def save_track_image(pts, filepath):
- if len(pts) < 2: return
- xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
- m = 50
- w = max(xs)-min(xs)+m*2; h = max(ys)-min(ys)+m*2
- w, h = max(w,200), max(h,100)
- c = np.ones((h, w, 3), dtype=np.uint8)*255
- for i in range(1, len(pts)):
- r = i/len(pts); g = 200 if r<0.5 else int(200*(1-r)*2); b = int(200*r*2) if r<0.5 else 200
- cv2.line(c, (pts[i-1][0]-min(xs)+m, pts[i-1][1]-min(ys)+m),
- (pts[i][0]-min(xs)+m, pts[i][1]-min(ys)+m), (0,g,b), 1)
- cv2.circle(c, (pts[0][0]-min(xs)+m, pts[0][1]-min(ys)+m), 4, (0,200,0), -1)
- cv2.circle(c, (pts[-1][0]-min(xs)+m, pts[-1][1]-min(ys)+m), 4, (0,0,200), -1)
- cv2.imwrite(filepath, c)
- def solve_slider(driver, sx=None):
- global d, W, H
- d = driver; W, H = d.window_size(); d.screen_on()
- # 自动判断分辨率: 宽>1000=高分屏(sx=163), 否则=低分屏(sx=87)
- if sx is None:
- sx = 163 if W > 1000 else 87
- print(f" 屏幕: {W}x{H} → sx={sx} ({'高分' if W>1000 else '低分'})")
- screen = d.screenshot(format="opencv")
- slider_y = y_top = slider_bottom = None
- try:
- cv2.imwrite(os.path.join(OUT, "_tmp_slider.png"), screen)
- info = detect_slider_button(os.path.join(OUT, "_tmp_slider.png"))
- if info and len(info) >= 3: sx = info[0] + int(info[2]/2)
- except: pass
- ocr_r = ocr_eng(screen)
- if ocr_r and ocr_r[0]:
- for item in ocr_r[0]:
- t = item[1]; cy = int((item[0][0][1]+item[0][2][1])/2)
- if "请按照说明拖动滑块" in t: slider_y = slider_y or cy; slider_bottom = int(item[0][2][1])
- if "松开" in t: y_top = int(item[0][0][1])
- if slider_y is None: return False
- sy = slider_y
- # 右滑 — TouchPipe + 压感
- d.touch.down(sx, sy); time.sleep(0.08)
- pipe = TouchPipe(d).open()
- t0 = time.perf_counter(); track_pts = []
- right_track = _build_right_track(sx, sy, W-30)
- for i, (x, y) in enumerate(right_track):
- p = _pressure_curve(i, len(right_track), 'right')
- pipe.move(x, y, p)
- track_pts.append({"x": x, "y": y, "pressure": p,
- "rel_ms": (time.perf_counter()-t0)*1000})
- time.sleep(random.uniform(0.002, 0.004))
- # 等设备消化完右滑点再截图 — 每点预留 28ms 处理时间
- settle_ms = max(500, len(right_track) * 18)
- time.sleep(settle_ms / 1000.0)
- # 展开后的幕布截图(后面叠加轨迹用)
- unfolded = d.screenshot(format="opencv")
- # JFBYM
- crop = d.screenshot(format="opencv")
- if y_top and slider_bottom: crop = crop[y_top:slider_bottom, :]
- _, buf = cv2.imencode(".png", crop)
- gap = None
- for a in range(3):
- try:
- r = requests.post(JFBYM_URL, json={"token":TOKEN,"type":"20226","image":base64.b64encode(buf).decode()}, timeout=35).json()
- if r.get("data") and r["data"].get("data"): gap = int(r["data"]["data"])
- elif r.get("data") and isinstance(r["data"],(int,float)): gap = int(r["data"])
- if gap is not None: break
- time.sleep(2)
- except: time.sleep(2)
- if gap is None: pipe.close(); d.touch.up(W-20, sy); return False
- # 折回
- target_x = gap + OFFSET_COMPENSATE
- return_track = _build_human_return_track(W-30, right_track[-1][1], target_x)
- for i, (x, y) in enumerate(return_track):
- p = _pressure_curve(i, len(return_track), 'return')
- pipe.move(x, y, p)
- track_pts.append({"x": x, "y": y, "pressure": p,
- "rel_ms": (time.perf_counter()-t0)*1000})
- # 前60%巡航快发, 后40%减速慢发
- frac = i / max(1, len(return_track)-1)
- if frac > 0.60:
- time.sleep(random.uniform(0.004, 0.008))
- else:
- time.sleep(random.uniform(0.000, 0.002))
- # 等设备消化完折回点
- # settle 按距离缩放: 每px约2~4ms, 上下限保护
- ret_distance = (W-30) - target_x
- settle_ms2 = int(ret_distance * random.uniform(2.0, 4.0))
- settle_ms2 = max(500, min(3500, settle_ms2))
- if ret_distance > 600: scat = '长等'
- elif ret_distance > 450: scat = '中等'
- else: scat = '短等'
- print(f' settle={settle_ms2}ms({settle_ms2/1000:.1f}s)')
- time.sleep(settle_ms2 / 1000.0)
- # 微调: 三种模式
- cur_x, cur_y = return_track[-1]
- aligned_img = None
- if MICRO_MODE == "fixed":
- target_cur_x = int(_clamp(cur_x + OFFSET_COMPENSATE, 10, W-10))
- steps = abs(target_cur_x - cur_x)
- sign = 1 if target_cur_x > cur_x else -1
- for s in range(steps):
- cur_x += sign
- pipe.move(cur_x, cur_y, 60)
- time.sleep(random.uniform(0.005, 0.010))
- print(f" 固定偏移: {OFFSET_COMPENSATE:+.0f}px 分{steps}步 -> x={cur_x}")
- elif MICRO_MODE == "visual":
- fallback_x = cur_x; prev_pl = None
- for attempt in range(3):
- time.sleep(0.3)
- check = d.screenshot(format="opencv")
- cv2.imwrite(os.path.join(ALIGN_DIR, f"align_{attempt}.png"), check)
- try: pl, _ = detect_captcha_left_edge(check)
- except Exception: break
- diff = pl - gap
- marked = check.copy()
- cv2.line(marked, (pl, 0), (pl, marked.shape[0]), (0, 255, 0), 3)
- cv2.line(marked, (gap, 0), (gap, marked.shape[0]), (0, 0, 255), 3)
- cv2.imwrite(os.path.join(ALIGN_DIR, f"align_{attempt}_marked.png"), marked)
- if abs(diff) <= 5: break
- if prev_pl is not None and pl == prev_pl:
- cur_x = int(_clamp(fallback_x,10,W-10)); pipe.move(cur_x,cur_y,60); break
- prev_pl = pl
- cur_x = int(_clamp(cur_x-diff,10,W-10)); pipe.move(cur_x,cur_y,50); time.sleep(0.05)
- # 滑块对准缺口的截图,做轨迹背景
- aligned_img = d.screenshot(format="opencv")
- pipe.close(); d.touch.up(int(cur_x),int(cur_y)); time.sleep(2)
- # 验证
- final = d.screenshot(format="opencv"); check_r = ocr_eng(final)
- passed = True
- if check_r and check_r[0]:
- if any("拖动滑块" in it[1] or "请按住滑块" in it[1] or "安全验证" in it[1] for it in check_r[0]): passed = False
- if not passed:
- time.sleep(2); final = d.screenshot(format="opencv"); check_r = ocr_eng(final)
- passed = True
- if check_r and check_r[0]:
- if any("拖动滑块" in it[1] or "请按住滑块" in it[1] or "安全验证" in it[1] for it in check_r[0]): passed = False
- all_track = right_track + return_track
- rd = SUCCESS_DIR if passed else FAILURE_DIR
- now = time.localtime()
- dev_id = getattr(d, 'serial', getattr(d, '_serial', 'unknown'))
- prefix = f"{dev_id}_{now.tm_year:04d}{now.tm_mon:02d}{now.tm_mday:02d}_{now.tm_hour:02d}{now.tm_min:02d}{now.tm_sec:02d}"
- save_track_image(all_track, os.path.join(rd, f"{prefix}_track.png"))
- # 轨迹叠加在滑块对齐缺口时的截图上
- if aligned_img is not None:
- b_img = aligned_img.copy()
- for i in range(1, len(all_track)):
- cv2.line(b_img, all_track[i-1], all_track[i], (0, 200, 200), 2)
- cv2.circle(b_img, all_track[0], 6, (0, 255, 0), -1)
- cv2.circle(b_img, all_track[-1], 6, (0, 0, 255), -1)
- cv2.imwrite(os.path.join(rd, f"{prefix}_b.png"), b_img)
- tj = {"device": dev_id, "screen": {"w": W, "h": H},
- "time": time.strftime("%Y-%m-%d %H:%M:%S", now),
- "total_points": len(track_pts),
- "duration_ms": track_pts[-1]["rel_ms"] if track_pts else 0,
- "start": {"x": track_pts[0]["x"], "y": track_pts[0]["y"]},
- "end": {"x": track_pts[-1]["x"], "y": track_pts[-1]["y"]},
- "dx": track_pts[-1]["x"]-track_pts[0]["x"],
- "dy": track_pts[-1]["y"]-track_pts[0]["y"],
- "passed": passed, "gap": gap,
- "points": track_pts}
- with open(os.path.join(rd, f"{prefix}_track.json"), 'w', encoding='utf-8') as fp:
- json.dump(tj, fp, ensure_ascii=False)
- return passed
- if __name__ == "__main__":
- DEVICE = "NJZX8DZXT47HQGWO"
- d = u2.connect(DEVICE)
- print(f"设备: {DEVICE}")
- solve_slider(d, sx=None) # None=自动判断分辨率
|