taotoolkit 1.0.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.
@@ -0,0 +1,1011 @@
1
+ #!/usr/bin/env python
2
+ # -*- encoding: utf-8 -*-
3
+ """设备操作层, adb/设备 设备描述json、打开相机, 拍照, 截屏, 录屏。"""
4
+
5
+ import os
6
+ import re
7
+
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ import json
13
+ import argparse
14
+ from time import sleep
15
+ from colorama import Fore
16
+
17
+ try:
18
+ import uiautomator2 as u2
19
+ except Exception: # pragma: no cover
20
+ u2 = None
21
+
22
+ from func_timeout import FunctionTimedOut, func_timeout
23
+
24
+ from .tao_common import (
25
+ debugCtrl,
26
+ checkMsg,
27
+ eExit,
28
+ exit,
29
+ iExit,
30
+ findFilesBySuffixKeyword,
31
+ inputSerial,
32
+ logd,
33
+ loge,
34
+ logh,
35
+ logi,
36
+ logw,
37
+ oCmd,
38
+ sysVersion,
39
+ pCmd,
40
+ parseArgs,
41
+ parseDeviceInfo,
42
+ rCmd,
43
+ normalize_args,
44
+ register_command,
45
+ )
46
+
47
+ CACHE_FILE = os.path.join(os.path.dirname(__file__), ".dev_cache.json")
48
+ CACHE_TTL = 3600
49
+
50
+
51
+ def _load_cache():
52
+ try:
53
+ if not os.path.exists(CACHE_FILE):
54
+ return None
55
+ with open(CACHE_FILE) as f:
56
+ c = json.load(f)
57
+ if time.time() - c.get("ts", 0) > CACHE_TTL:
58
+ return None
59
+ return c
60
+ except Exception:
61
+ return None
62
+
63
+
64
+ def _save_cache(serial, screen_w, screen_h, android_ver):
65
+ try:
66
+ with open(CACHE_FILE, "w") as f:
67
+ json.dump(
68
+ {
69
+ "ts": time.time(),
70
+ "serial": serial,
71
+ "screen_w": screen_w,
72
+ "screen_h": screen_h,
73
+ "android_ver": android_ver,
74
+ },
75
+ f,
76
+ )
77
+ except Exception:
78
+ pass
79
+
80
+
81
+ class DevOp:
82
+ """android device operate"""
83
+
84
+ def __init__(self, serial="", needRoot=False):
85
+ logd(f"serial = {serial} needroot ={needRoot}")
86
+
87
+ cache = _load_cache()
88
+ out, _ = pCmd("adb devices -l", 1, False, waitDone=True, timeOut=5, logLevel="debug")
89
+
90
+ if cache != None and cache["serial"] in out:
91
+ cache_hit = True
92
+ else:
93
+ cache_hit = False
94
+
95
+ if cache_hit:
96
+ if cache == None:
97
+ eExit("cache file error")
98
+ else:
99
+ logd(f"[cache] 命中缓存,跳过设备扫描: {cache['serial']}")
100
+ self.serial = cache["serial"]
101
+ self.curScreenW = cache["screen_w"]
102
+ self.curScreenH = cache["screen_h"]
103
+ self.androidVersion = cache["android_ver"]
104
+ self.devices = [{"serial": self.serial}]
105
+ else:
106
+ logd("[cache] 未命中,完整初始化")
107
+ self.devices = self.getConnectedDevices()
108
+ if serial == "":
109
+ self.serial = self.getSerial()
110
+ else:
111
+ self.serial = serial
112
+ self.checkSerial(serial)
113
+ self.curScreenW, self.curScreenH = self.getScreenSize()
114
+ self.androidVersion = self.getAndroidVersion()
115
+ _save_cache(self.serial, self.curScreenW, self.curScreenH, self.androidVersion)
116
+
117
+ self.curPath = os.getcwd()
118
+ self.refScreenW = 720
119
+ self.refScreenH = 1600
120
+ self.xRatio = self.curScreenW / self.refScreenW
121
+ self.yRatio = self.curScreenH / self.refScreenH
122
+ self.soc = None
123
+
124
+ if needRoot:
125
+ self.adbRoot()
126
+ info, err = self.adbRemount()
127
+ if "reboot" in err or "reboot" in info:
128
+ self.adbReboot()
129
+ logh("Now reboot your device for settings to take effect")
130
+ exit()
131
+
132
+ def checkSerial(self, serial):
133
+ serialList = []
134
+ if self.devices == None:
135
+ eExit(f"no devices detected")
136
+ else:
137
+ for i in self.devices:
138
+ serialList.append(i["serial"])
139
+ if serial == i["serial"]:
140
+ return True
141
+ eExit(f"[{serial}] no such a device, detected {' , '.join(serialList)}")
142
+
143
+ def getConnectedDevices(self):
144
+ try:
145
+ out, error = pCmd("adb devices -l", 1, True, waitDone=True, timeOut=6, logLevel="debug")
146
+ if "offline" in out or "unauthorized" in out:
147
+ tmp = out.split("\n")[1].split(" ")[0]
148
+ eExit(f"{tmp} devices offline or unauthorized")
149
+ if len(out.split("\n")) == 3:
150
+ eExit("no device detected")
151
+ logd(out)
152
+ lines = out.strip().split("\n")
153
+ devices = []
154
+ for line in lines[1:]:
155
+ device_info = line.split("\t")[0]
156
+ devices.append(parseDeviceInfo(device_info))
157
+ if len(devices) == 0:
158
+ eExit("no devices detect")
159
+ return devices
160
+ except subprocess.CalledProcessError as e:
161
+ loge(str(e))
162
+ return None
163
+
164
+ def getSerial(self):
165
+ if self.devices is None:
166
+ eExit("no devices detected")
167
+ devices = self.devices or []
168
+ serial_index = 0
169
+ if len(devices) == 1:
170
+ serial_index = 0
171
+ elif len(devices) > 1:
172
+ logi(f"more than one devices connect, {len(devices)}")
173
+ for index, value in enumerate(devices):
174
+ logi(f"{index} {value['serial']} {value['model']}")
175
+ serial_index = inputSerial(0, len(devices) - 1)
176
+
177
+ return devices[serial_index]["serial"]
178
+
179
+ def getAndroidVersion(self):
180
+ cmd = f"adb -s {self.serial} shell getprop |grep ro.build.version.release"
181
+ out = rCmd(cmd)[0]
182
+ logd(out)
183
+ if out == "":
184
+ logw("get android version failed, set it to 16 by default")
185
+ return "16"
186
+ return out.split("[")[2].split("]")[0]
187
+
188
+ def getProductBoard(self):
189
+ cmd = f"adb -s {self.serial} shell getprop |grep ro.product.board"
190
+ out = rCmd(cmd)[0]
191
+ return out.split("[")[2].split("]")[0]
192
+
193
+ def getScreenSize(self):
194
+ cmd = f"adb -s {self.serial} shell wm size"
195
+ out, _error = rCmd(cmd, checkErr=True)
196
+ if "Physical size:" in out:
197
+ size_str = out.split(":")[1].strip()
198
+ width, height = map(int, size_str.split("x"))
199
+ return width, height
200
+ logw(f"get {self.serial} lcd size failed, set it to 720x1600 by default")
201
+ return 720, 1600
202
+
203
+ def slectBoard(self):
204
+ boardList = ["dove", "larkl", "lark", "larkpro"]
205
+ while True:
206
+ try:
207
+ user_input = input(f"{Fore.GREEN}请输入SOC型号 {boardList}: {Fore.RESET}")
208
+ if user_input in boardList:
209
+ return user_input
210
+ loge("无效的SOC型号,请重新输入")
211
+ except ValueError:
212
+ loge("输入无效,请输入SOC型号.")
213
+
214
+ def getASRDevice(self):
215
+ if self.devices:
216
+ for device in self.devices:
217
+ prop = ["ro.boot.hardware.soc", "ro.product.model", "ro.soc.model"]
218
+ for i in prop:
219
+ cmd = f"adb -s {device['serial']} shell getprop |grep -iE {i} "
220
+ out = rCmd(cmd)[0]
221
+ logd(out)
222
+ if "asr" in out.lower():
223
+ if "8601" in out.lower():
224
+ self.soc = "dove"
225
+ elif "8602" in out.lower():
226
+ self.soc = "dovelite"
227
+ elif "8661" in out.lower():
228
+ self.soc = "lark"
229
+ elif "8662" in out.lower():
230
+ self.soc = "larkl"
231
+ elif "8861" in out.lower():
232
+ self.soc = "larkpro"
233
+ else:
234
+ self.soc = None
235
+ if self.soc is not None:
236
+ return self.soc
237
+ logw("get SOC from prop failed, manual set it")
238
+ self.soc = self.slectBoard()
239
+ return self.soc
240
+ eExit("No device connected.")
241
+
242
+ def getSoc(self):
243
+ if self.devices:
244
+ prop = ["ro.boot.hardware.soc", "ro.product.model", "ro.soc.model"]
245
+ for i in prop:
246
+ cmd = f"adb -s {self.serial} shell getprop |grep -iE {i} "
247
+ out = rCmd(cmd)[0]
248
+ logd(out)
249
+ if "asr" in out.lower():
250
+ if "8601" in out.lower():
251
+ self.soc = "dove"
252
+ elif "8602" in out.lower():
253
+ self.soc = "dovelite"
254
+ elif "8661" in out.lower():
255
+ self.soc = "lark"
256
+ elif "8662" in out.lower():
257
+ self.soc = "larkl"
258
+ elif "8861" in out.lower():
259
+ self.soc = "larkpro"
260
+ else:
261
+ self.soc = None
262
+ if self.soc is not None:
263
+ return self.soc
264
+ logw("get SOC from prop failed, manual set it")
265
+ self.soc = self.slectBoard()
266
+ return self.soc
267
+ eExit("ASR SOC not found")
268
+
269
+ def checkCameraOpened(self):
270
+ cmd = f"adb -s {self.serial} shell dumpsys window | grep mFocus"
271
+ out, error = rCmd(cmd)
272
+ if "camera" in out.lower():
273
+ return True
274
+ loge("camera not open, start camera ...")
275
+ rCmd("adb shell am start -a android.media.action.STILL_IMAGE_CAMERA", 1)
276
+ sleep(2)
277
+
278
+ def adbRoot(self):
279
+ cmd = f"adb -s {self.serial} root"
280
+ return rCmd(cmd, 0.5, showCmd=True, timeOut=10, showRet=True, logLevel="info")
281
+
282
+ def adbRemount(self):
283
+ cmd = f"adb -s {self.serial} remount"
284
+ info, err = rCmd(cmd, 0.5, timeOut=10)
285
+ self.setLcdTimeout()
286
+ if err != "":
287
+ logi(err.split("\n")[0])
288
+ logi(err.split("\n")[-2])
289
+ elif info != "":
290
+ logi(info.split("\n")[0])
291
+ return info, err
292
+
293
+ def setLcdTimeout(self, time_s=3600):
294
+ cmd = f"adb -s {self.serial} shell settings put system screen_off_timeout {time_s * 1000}"
295
+ _ = rCmd(cmd, 0.5, showCmd=True, timeOut=3, logLevel="debug", showRet=True)
296
+
297
+ def adbReboot(self):
298
+ cmd = f"adb -s {self.serial} reboot"
299
+ return rCmd(cmd, 0.5, timeOut=10)
300
+
301
+ def adbScreenBrightness(self, brightness=255):
302
+ cmd = f'adb -s {self.serial} shell "settings put system screen_brightness {brightness}"'
303
+ oCmd(cmd, showCmd=True, logLevel="info")
304
+
305
+ def adbSocket(self):
306
+ cmd = f"adb -s {self.serial} forward tcp:12218 localabstract:camera_tuning_server"
307
+ rCmd(cmd, 0.5, checkErr=True)
308
+
309
+ def adbTap(self, cordinateABS, slp=1):
310
+ cordinate = [cordinateABS[0] * self.xRatio, cordinateABS[1] * self.yRatio]
311
+ cordinate = list(map(int, cordinate))
312
+ cmd = f"adb -s {self.serial} shell input tap {cordinate[0]} {cordinate[1]}"
313
+ rCmd(cmd, slp, True, logLevel="info")
314
+
315
+ def adbSwipe(self, cordinateABS, slp=2, time_ms=600):
316
+ cordinate = [
317
+ cordinateABS[0] * self.xRatio,
318
+ cordinateABS[1] * self.yRatio,
319
+ cordinateABS[2] * self.xRatio,
320
+ cordinateABS[3] * self.yRatio,
321
+ ]
322
+ cordinate = list(map(int, cordinate))
323
+ cmd = f"adb -s {self.serial} shell input swipe {cordinate[0]} {cordinate[1]} {cordinate[2]} {cordinate[3]} {time_ms}"
324
+ rCmd(cmd, slp, True, logLevel="info")
325
+
326
+ def take_screenshot(self, outPath):
327
+ success = self._try_screencap(args=["-p"], outPath=outPath)
328
+ if success:
329
+ return True
330
+ return self._try_screencap(args=["--preserve-display-colors", "-p"], outPath=outPath)
331
+
332
+ def _try_screencap(self, args, outPath):
333
+ try:
334
+ cmd = ["adb", "-s", self.serial, "exec-out", "screencap"] + args
335
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=15)
336
+ if result.returncode != 0:
337
+ logi(f"failed: {result.stderr.decode()}")
338
+ return False
339
+ data = result.stdout
340
+ if len(data) < 8 or data[:4] != b"\x89PNG":
341
+ logw(f"invalid PNG, size={len(data)}, args={args}")
342
+ return False
343
+ os.makedirs(os.path.dirname(outPath), exist_ok=True)
344
+ with open(outPath, "wb") as f:
345
+ f.write(data)
346
+ logi(f"success: {outPath}, size={len(data)}")
347
+ return True
348
+ except subprocess.TimeoutExpired:
349
+ loge(f"timeout, args={args}")
350
+ return False
351
+ except Exception as e:
352
+ loge(f"exception: {e}")
353
+ return False
354
+
355
+ def adbScreenCap(self, fileName):
356
+ outPath = os.path.join(self.curPath, fileName + ".png")
357
+ self.take_screenshot(outPath)
358
+
359
+ def adbScreenRec(
360
+ self,
361
+ fileName,
362
+ limitTime_s="30",
363
+ bitRate_Mbps="2",
364
+ inputWidth=None,
365
+ inputHeight=None,
366
+ debug=True,
367
+ isThread=False,
368
+ ):
369
+ limitTime_s = int(limitTime_s)
370
+ bitRate_Mbps = int(float(bitRate_Mbps) * 1000000)
371
+ inputWidth = int(inputWidth) if inputWidth is not None else None
372
+ inputHeight = int(inputHeight) if inputHeight is not None else None
373
+ mp4Name = fileName + ".mp4"
374
+ cmd1 = f"adb -s {self.serial} shell screenrecord --time-limit {limitTime_s}"
375
+ cmd2 = f"--bugreport --bit-rate {bitRate_Mbps}" if debug else f"--bit-rate {int(bitRate_Mbps)}"
376
+ if inputWidth is None or inputHeight is None:
377
+ recordWidth, recordHeight = self.curScreenW, self.curScreenH
378
+ elif inputWidth > self.curScreenW or inputHeight > self.curScreenH:
379
+ logw(
380
+ f"inputWidth {inputWidth} or inputHeight {inputHeight} > lcdWidth {self.curScreenW} or lcdHeight {self.curScreenH}, use lcd size"
381
+ )
382
+ recordWidth, recordHeight = self.curScreenW, self.curScreenH
383
+ else:
384
+ recordWidth, recordHeight = inputWidth, inputHeight
385
+
386
+ cmd3 = f"--size {recordWidth}x{recordHeight} /sdcard/{mp4Name}"
387
+ cmd = f"{cmd1} {cmd2} {cmd3}"
388
+ if sysVersion == "Linux":
389
+ pCmd(cmd, showCmd=True, splitCmd=False, shell=True, logLevel="info", waitDone=False)
390
+ else:
391
+ pCmd(cmd, showCmd=True, splitCmd=False, shell=False, logLevel="info", waitDone=False)
392
+ try:
393
+ if isThread:
394
+ logi(
395
+ f"等待{limitTime_s}s 自动结束录制 {mp4Name} {bitRate_Mbps / 1000000}Mbps {recordWidth}x{recordHeight}"
396
+ )
397
+ sleep(float(limitTime_s))
398
+ else:
399
+ logi(
400
+ f"回车或等待{limitTime_s}s 结束录制 {mp4Name} {bitRate_Mbps / 1000000}Mbps {recordWidth}x{recordHeight}"
401
+ )
402
+ char = func_timeout(float(limitTime_s), lambda: input())
403
+ if char or char == "":
404
+ pass
405
+ except FunctionTimedOut:
406
+ pass
407
+ finally:
408
+ stopCmd = f"adb -s {self.serial} shell pkill -2 -f screenrecord"
409
+ pCmd(stopCmd, 0.5, showCmd=True, splitCmd=False, shell=True, logLevel="info")
410
+ outfile = os.path.join(self.curPath, mp4Name)
411
+ cmd = f"adb -s {self.serial} pull sdcard/{mp4Name} {outfile}"
412
+ pCmd(cmd, 1, showCmd=True, splitCmd=False, shell=True, timeOut=10, logLevel="info")
413
+
414
+ def keyHome(self, slp=0.5):
415
+ cmd = f"adb -s {self.serial} shell input keyevent KEYCODE_HOME"
416
+ rCmd(cmd, slp)
417
+
418
+ def keyBack(self, slp=0.5):
419
+ cmd = f"adb -s {self.serial} shell input keyevent BACK"
420
+ rCmd(cmd, slp)
421
+
422
+ def keyMenu(self, slp=0.5):
423
+ cmd = f"adb -s {self.serial} shell input keyevent KEYCODE_MENU"
424
+ rCmd(cmd, slp)
425
+
426
+ def keyPower(self, slp=0.5):
427
+ cmd = f"adb -s {self.serial} shell input keyevent KEYCODE_POWER"
428
+ rCmd(cmd, slp)
429
+
430
+ def keyWakeup(self, slp=0.5):
431
+ cmd = f"adb -s {self.serial} shell input keyevent KEYCODE_WAKEUP"
432
+ rCmd(cmd, slp)
433
+
434
+ def adbCmd(self, cmdStr, withShell=True, slp=0.5):
435
+ if withShell:
436
+ cmd = f"adb -s {self.serial} shell {cmdStr}"
437
+ else:
438
+ cmd = f"adb -s {self.serial} {cmdStr}"
439
+ rCmd(cmd, slp, showCmd=True, logLevel="info")
440
+
441
+ def keyCapture(self, slp=0.5):
442
+ cmd = f"adb -s {self.serial} shell input keyevent 27"
443
+ rCmd(cmd, slp)
444
+
445
+ def pushFile(self, local, target):
446
+ cmd = f"adb -s {self.serial} push {local} {target}"
447
+ out, err = rCmd(cmd, 0.5, showCmd=True, logLevel="info")
448
+ logi(out)
449
+ checkMsg(err, "failed", "device may need remount")
450
+ if "Read-only" in out:
451
+ eExit("device is Read-only, may need remount")
452
+
453
+ def isLcdOn(self):
454
+ cmd = f"adb -s {self.serial} shell dumpsys window policy"
455
+ out, _ = rCmd(cmd, 0.5)
456
+ return not re.search("screenState=SCREEN_STATE_OFF", out)
457
+
458
+ def isScreenLock(self):
459
+ try:
460
+ cmd = f"adb -s {self.serial} shell dumpsys window policy"
461
+ out, _ = rCmd(cmd, 0.5)
462
+ return bool(re.search("showing=true", out))
463
+ except Exception as e:
464
+ loge(f"获取手机lock状态异常{e}")
465
+ return False
466
+
467
+ def lcdOnAndUnlock(self, on=1):
468
+ if on == 0:
469
+ if self.isLcdOn():
470
+ self.keyPower(1)
471
+ if self.isLcdOn():
472
+ self.keyPower(1)
473
+ else:
474
+ if self.isLcdOn():
475
+ if self.isScreenLock():
476
+ self.keyMenu(1)
477
+ if self.isScreenLock():
478
+ self.keyPower(1)
479
+ else:
480
+ self.keyWakeup(1)
481
+ self.keyMenu(1)
482
+ if self.isScreenLock():
483
+ self.keyPower(1)
484
+
485
+ def openCam(self, id=0):
486
+ if id == 0:
487
+ rCmd("adb shell am start -a android.media.action.STILL_IMAGE_CAMERA", 1)
488
+ else:
489
+ rCmd("adb shell am start -a android.media.action.STILL_IMAGE_CAMERA", 2)
490
+ sleep(1)
491
+ self.adbTap((600, 1400))
492
+ sleep(2)
493
+
494
+ def getSuffixList(self, path: str, *suffix: str):
495
+ fileList = []
496
+ cmd = f"adb -s {self.serial} shell ls -t {path}"
497
+ allFileList = rCmd(cmd)[0].split("\n")[0:-1]
498
+ logd(str(allFileList))
499
+ for file in allFileList:
500
+ for i in suffix:
501
+ if file.lower().endswith(i) and "thumb" not in file:
502
+ fileList.append(file)
503
+ return fileList
504
+
505
+ def capJPG(self, name="null", wait=3):
506
+ outPath = self.curPath
507
+ logi(f"capture {os.path.join(outPath, name)}.jpg")
508
+ jpgPath = r"/sdcard/DCIM/Camera/"
509
+ oldFileList = self.getSuffixList(jpgPath, "jpg", "jpeg")
510
+ cmd = f"adb -s {self.serial} shell input keyevent 27"
511
+ rCmd(cmd, 0.5, True, logLevel="info")
512
+ sleep(wait)
513
+
514
+ founded = False
515
+ for i in range(5):
516
+ fileList = self.getSuffixList(jpgPath, "jpg", "jpeg")
517
+ for f in fileList:
518
+ if "~" in f:
519
+ fileList.remove(f)
520
+ jpgNameList = list(set(fileList) - set(oldFileList))
521
+ if jpgNameList != []:
522
+ jpgName = jpgNameList[0]
523
+ founded = True
524
+ if name == "null":
525
+ cmd = f"adb -s {self.serial} pull {jpgPath}{jpgName}"
526
+ else:
527
+ cmd = f"adb -s {self.serial} pull {jpgPath}{jpgName} {name}.jpg"
528
+ rCmd(cmd, 0.5, True, logLevel="info")
529
+ break
530
+ else:
531
+ sleep(1.0)
532
+ if not founded:
533
+ loge(f"capture {os.path.join(outPath, name)}.jpg failed")
534
+ return False
535
+ return True
536
+
537
+ def killCamera(self):
538
+ cmd = f"adb -s {self.serial} shell input keyevent KEYCODE_HOME"
539
+ rCmd(cmd, 0.5)
540
+ rCmd("adb shell pm clear com.asrmicro.camera", 0.5, showCmd=True, logLevel="info")
541
+ keyStr = "camera"
542
+ cmd = f"adb -s {self.serial} shell ps -e |grep {keyStr}"
543
+ out, _err = rCmd(cmd, showCmd=True, logLevel="info")
544
+ for i in out.splitlines():
545
+ PID = i.split()[1]
546
+ if r"android.hardware.camera.provider@2" in i.split()[8]:
547
+ cmd = f"adb -s {self.serial} shell kill -9 {PID}"
548
+ out, err = rCmd(cmd, 0.5, showCmd=True, logLevel="info")
549
+ if "Operation not permitted" in err:
550
+ eExit("Operation not permitted,device need need remount")
551
+ sleep(2)
552
+
553
+ def backUpSo(self, so):
554
+ backUpName = so + ".ori"
555
+ cmd = f"adb -s {self.serial} shell ls vendor/lib64/ | grep {backUpName}"
556
+ out, _ = pCmd(cmd, showCmd=True, showRet=False, logLevel="debug")
557
+ if out == "":
558
+ logi(f"{backUpName} not exist, backup {so}")
559
+ cmd = f"adb -s {self.serial} shell cp vendor/lib64/{so} vendor/lib64/{backUpName}"
560
+ _, _ = pCmd(cmd, showCmd=True, showRet=True, logLevel="debug")
561
+ else:
562
+ logi(f"{backUpName} already exist, skip backup")
563
+
564
+ def pushso(self, so):
565
+ for i in so:
566
+ soWithOutPath = os.path.basename(i)
567
+ self.backUpSo(soWithOutPath)
568
+ cmd = f"adb -s {self.serial} push {i} vendor/lib64/{soWithOutPath}"
569
+ out, err = pCmd(cmd, showCmd=True, showRet=True, logLevel="info")
570
+ if "Read-only" in out:
571
+ eExit("device is Read-only, may need remount")
572
+ self.killCamera()
573
+
574
+ def pushI2cTransfer(self):
575
+ cmd = f"adb -s {self.serial} shell ls /system/bin/ |grep i2c_transfer"
576
+ out = rCmd(cmd)[0]
577
+ if "i2c_transfer" not in out:
578
+ i2cttPath = os.path.join(sys.path[0], "i2c_transfer")
579
+ if not os.path.exists(i2cttPath):
580
+ loge(f"{i2cttPath} not exist!")
581
+ return
582
+ cmd = f"adb -s {self.serial} push {i2cttPath} system/bin"
583
+ out, err = rCmd(cmd, showCmd=True, logLevel="debug")
584
+ checkMsg(err, "failed", "device may need remount")
585
+ cmd = f"adb -s {self.serial} shell chmod a+x /system/bin/i2c_transfer"
586
+ out, err = rCmd(cmd, showCmd=True, logLevel="debug")
587
+
588
+ def i2cTest(self, dev, addr):
589
+ self.pushI2cTransfer()
590
+ cmd = f"adb shell /system/bin/i2c_transfer {dev} w2@{addr} 0x0 0x0 r1"
591
+ info, _ = rCmd(cmd, showCmd=True, logLevel="debug")
592
+ return info != ""
593
+
594
+ def i2cDet(self, devID=None):
595
+ self.pushI2cTransfer()
596
+ if devID is not None:
597
+ for j in range(0x10, 0x77):
598
+ print("\r" + f"detecting dev[{devID}] addr[{hex(j)}]", end=" ")
599
+ if self.i2cTest(devID, hex(j)):
600
+ print(f"| dev-{devID} detected {hex(j)}")
601
+ print("")
602
+ else:
603
+ for i in range(0, 2):
604
+ for j in range(0x10, 0x77):
605
+ print("\r" + f"detecting dev[{i}] addr[{hex(j)}]", end=" ")
606
+ if self.i2cTest(i, hex(j)):
607
+ print(f"| dev-{i} detected {hex(j)}")
608
+ print("")
609
+
610
+ def i2cDump(self, busID, devAddr):
611
+ cmd = f"adb -s {self.serial} shell /system/bin/i2cdump -f -y {busID} {devAddr}"
612
+ rCmd(cmd, showCmd=True, showRet=True, logLevel="info")
613
+
614
+ def i2c(self, devID, devAddr, regAddr, regValue=None, readNum=1):
615
+ self.pushI2cTransfer()
616
+ addr_len = 2 if len(regAddr) > 4 else 1
617
+ preCmdStr = f"adb -s {self.serial} shell /system/bin/i2c_transfer {devID}"
618
+ if regValue == None:
619
+ if readNum == 1:
620
+ if addr_len == 2:
621
+ regAddrH = hex(int(regAddr, 16) >> 8)
622
+ regAddrL = hex(int(regAddr, 16) & 0xFF)
623
+ cmd = f"{preCmdStr} w2@{devAddr} {regAddrH} {regAddrL} r1"
624
+ else:
625
+ cmd = f"{preCmdStr} w1@{devAddr} {regAddr} r1"
626
+ info, err = rCmd(cmd, showCmd=True)
627
+ checkMsg(err, "inaccessible", "device may need remount")
628
+ info = info.strip().split("\n")[0].strip("'")
629
+ logi(f"read bus[{devID}] dev[{devAddr}] {regAddr} = {info}")
630
+ else:
631
+ regAddr = int(regAddr, 16)
632
+ for i in range(readNum):
633
+ if addr_len == 2:
634
+ regAddrH = hex(regAddr >> 8)
635
+ regAddrL = hex(regAddr & 0xFF)
636
+ cmd = f"{preCmdStr} w{addr_len}@{devAddr} {regAddrH} {regAddrL} r1"
637
+ else:
638
+ cmd = f"{preCmdStr} w{addr_len}@{devAddr} {regAddr} r1"
639
+ info, err = rCmd(cmd, showCmd=True)
640
+ checkMsg(err, "inaccessible", "device may need remount")
641
+ info = info.strip().split("\n")[0].strip("'")
642
+ logi(f"read bus[{devID}] dev[{devAddr}] {hex(regAddr)} = {info}")
643
+ regAddr = regAddr + 1
644
+ else:
645
+ cmd = ""
646
+ value_len = 2 if len(regValue) > 4 else 1
647
+ if addr_len == 2:
648
+ regAddrH = hex(int(regAddr, 16) >> 8)
649
+ regAddrL = hex(int(regAddr, 16) & 0xFF)
650
+ if value_len == 1:
651
+ cmd = f"{preCmdStr} w3@{devAddr} {regAddrH} {regAddrL} {regValue}"
652
+ elif value_len == 2:
653
+ regValueH = hex(int(regValue, 16) >> 8)
654
+ regValueL = hex(int(regValue, 16) & 0xFF)
655
+ cmd = f"{preCmdStr} w4@{devAddr} {regAddrH} {regAddrL} {regValueH} {regValueL}"
656
+ else:
657
+ cmd = f"{preCmdStr} w2@{devAddr} {regAddr} {regValue}"
658
+ info, err = rCmd(cmd, showCmd=True)
659
+ checkMsg(err, "inaccessible", "device may need remount")
660
+ info = info.strip().split("\n")[0].strip("'")
661
+ logi(f"write bus[{devID}] dev[{devAddr}] {regAddr} = {regValue}")
662
+
663
+ def i2c_write_addr16_value8(self, devID, devAddr, regAddr, value):
664
+ regValue = str(hex(value))
665
+ regAddrH = hex(int(regAddr, 16) >> 8)
666
+ regAddrL = hex(int(regAddr, 16) & 0xFF)
667
+ preCmdStr = f"adb -s {self.serial} shell /system/bin/i2c_transfer {devID}"
668
+ cmd = f"{preCmdStr} w3@{devAddr} {regAddrH} {regAddrL} {regValue}"
669
+ info, err = rCmd(cmd, showCmd=True)
670
+ checkMsg(err, "inaccessible", "device may need remount")
671
+ info = info.strip().split("\n")[0].strip("'")
672
+ logi(f"write bus[{devID}] dev[{devAddr}] {regAddr} = {regValue}")
673
+
674
+
675
+ def CLI_pushSo(argsList):
676
+ parser = argparse.ArgumentParser(prog="pushso", description="android设备push so文件")
677
+ parser.add_argument("-p", "--path", required=True, nargs="+", help="so文件名或路径,支持多so文件 push")
678
+ parser.add_argument("-s", "--serial", type=str, default="", help="设备SN号(多设备需指定)")
679
+ args = parser.parse_args(argsList)
680
+
681
+ dev = DevOp(args.serial)
682
+ so = []
683
+ if len(args.path) == 1:
684
+ if os.path.isdir(args.path[0]):
685
+ so = findFilesBySuffixKeyword(args.path[0], ".so", "")
686
+ dev.pushso(so)
687
+ else:
688
+ dev.pushso(args.path)
689
+ else:
690
+ dev.pushso(args.path)
691
+
692
+
693
+ def CLI_scrcpy(argsList):
694
+ parser = argparse.ArgumentParser(prog="scrcpy", description="android设备投屏")
695
+ parser.add_argument("-s", "--serial", type=str, default="", help="设备SN号(多设备需指定)")
696
+ parser.add_argument("--top", action="store_true", dest="top", help="投屏置顶")
697
+ parser.set_defaults(top=False)
698
+ parser.add_argument("--lcdoff", action="store_true", dest="lcdoff", help="关闭lcd")
699
+ parser.set_defaults(lcdoff=False)
700
+ args = parser.parse_args(argsList)
701
+
702
+ dev = DevOp(args.serial)
703
+ args_top = "scrcpy --always-on-top" if args.top else "scrcpy"
704
+ lcd_off = "-SKw -s" if args.lcdoff else "-Kw -s"
705
+ cmd = f"{args_top} {lcd_off} {dev.serial} --shortcut-mod=lctrl"
706
+ info, err = pCmd(cmd, showCmd=True, waitDone=False, logLevel="info")
707
+ # print(err)
708
+ checkMsg(err, "Device disconnected", err)
709
+
710
+
711
+ def CLI_power(argsList):
712
+ parser = argparse.ArgumentParser(prog="power", description="android power on/off")
713
+ parser.add_argument("-s", "--serial", type=str, default="", help="设备SN号(多设备需指定)")
714
+ parser.add_argument("--off", action="store_false", dest="power", help="关闭屏幕")
715
+ parser.set_defaults(power=True)
716
+ args = parser.parse_args(argsList)
717
+
718
+ dev = DevOp(args.serial)
719
+ logi("power on" if args.power else "power off")
720
+ dev.lcdOnAndUnlock(args.power)
721
+
722
+
723
+ def CLI_cam(args):
724
+ dev = DevOp()
725
+ if len(args) == 0:
726
+ dev.openCam()
727
+ else:
728
+ # TODO 支持切换前摄
729
+ dev.openCam(int(args[0]))
730
+
731
+
732
+ def CLI_screenCap(argsList):
733
+ parser = argparse.ArgumentParser(prog="screenCap", description="android设备截屏")
734
+ parser.add_argument(
735
+ "-f", "--file", required=False, default="tmp", help="截屏文件名,自动添加后缀.png (默认tmp,可选)"
736
+ )
737
+ parser.add_argument("-s", "--serial", type=str, default="", help="设备SN号(多设备需指定)")
738
+ args = parser.parse_args(argsList)
739
+
740
+ dev = DevOp(args.serial)
741
+ if not dev.isLcdOn():
742
+ eExit("lcd off")
743
+ dev.adbScreenCap(args.file)
744
+
745
+
746
+ def CLI_screenRec(argsList):
747
+ parser = argparse.ArgumentParser(prog="screenRec", description="对android设备录屏")
748
+ parser.add_argument(
749
+ "-f", "--file", required=False, default="tmp", help="录制文件名,自动添加后缀.mp4 (默认tmp,可选)"
750
+ )
751
+ parser.add_argument("-t", "--time", required=False, default="30", help="录制时间,单位秒 (默认30秒,可选)")
752
+ parser.add_argument("-b", "--bitrate", required=False, default="2", help="录制比特率,单位Mbps (默认2,可选)")
753
+ parser.add_argument("-s", "--serial", type=str, default="", help="设备SN号(多设备需指定)")
754
+ parser.add_argument("--width", required=False, default=None, help="录制宽度 (默认lcd size,可选)")
755
+ parser.add_argument("--height", required=False, default=None, help="录制高度 (默认lcd size,可选)")
756
+ parser.add_argument(
757
+ "--no-debug", action="store_false", dest="debug", help="禁用调试模式,开启可在视频上添加时间戳等额外信息"
758
+ )
759
+ parser.set_defaults(debug=True) # 设置默认值为 True
760
+ args = parser.parse_args(argsList)
761
+
762
+ dev = DevOp(args.serial)
763
+ if not dev.isLcdOn():
764
+ eExit("lcd off")
765
+ dev.adbScreenRec(
766
+ fileName=args.file,
767
+ limitTime_s=args.time,
768
+ bitRate_Mbps=args.bitrate,
769
+ inputWidth=args.width if args.width else None,
770
+ inputHeight=args.height if args.height else None,
771
+ debug=args.debug,
772
+ )
773
+
774
+
775
+ def i2cParseArguments(argsList):
776
+ parser = argparse.ArgumentParser(prog="iic", description="对android设备进行iic操作")
777
+
778
+ # 创建子解析器
779
+ subparsers = parser.add_subparsers(dest="operation", required=True, help="操作类型")
780
+
781
+ # 1. rw 操作
782
+ parser_rw = subparsers.add_parser("rw", help="读写操作")
783
+ parser_rw.add_argument("-b", "--busid", required=True, choices=["0", "1", "2"], help="设备ID (必须)")
784
+ parser_rw.add_argument("-d", "--devaddr", required=True, help="设备地址 (必须)")
785
+ parser_rw.add_argument("-a", "--addr", required=True, help="设备寄存器地址 (必须)")
786
+ parser_rw.add_argument("-v", "--value", required=False, default=None, help="设备寄存器值 (读取不用设)")
787
+ parser_rw.add_argument("-r", "--readnum", type=str, default="1", help="读取寄存器数量 (可选)")
788
+
789
+ # 2. det 操作
790
+ parser_det = subparsers.add_parser("det", help="检测操作")
791
+ parser_det.add_argument("-b", "--busid", choices=["0", "1", "2"], help="设备ID (可选)")
792
+
793
+ # 3. dump 操作
794
+ parser_dump = subparsers.add_parser("dump", help="总线检查操作")
795
+ parser_dump.add_argument("-b", "--busid", required=True, choices=["0", "1", "2"], help="设备ID (必须)")
796
+ parser_dump.add_argument("-d", "--devaddr", required=True, help="设备地址 (必须)")
797
+
798
+ args = parser.parse_args(argsList)
799
+ return args
800
+
801
+
802
+ def CLI_i2c(argsList):
803
+ args = i2cParseArguments(argsList)
804
+ dev = DevOp(needRoot=False)
805
+
806
+ if args.operation == "det":
807
+ # det 操作:busid 可选
808
+ if args.busid is not None:
809
+ dev.i2cDet(int(args.busid))
810
+ else:
811
+ dev.i2cDet()
812
+
813
+ elif args.operation == "dump":
814
+ # dump 操作:busid 和 devaddr 必须
815
+ dev.i2cDump(args.busid, args.devaddr)
816
+
817
+ elif args.operation == "rw":
818
+ # rw 操作:
819
+ dev.i2c(args.busid, args.devaddr, args.addr, args.value, int(args.readnum))
820
+
821
+
822
+ def CLI_kcam(args):
823
+ dev = DevOp()
824
+ dev.killCamera()
825
+
826
+
827
+ def CLI_capjpg(argsList):
828
+ parser = argparse.ArgumentParser(prog="capjpg", description="拍摄jpg并导出到PC指定文件名")
829
+ parser.add_argument("-f", "--file", type=str, default="null", help="jpg名称")
830
+ parser.add_argument("-s", "--serial", type=str, default="", help="设备SN号")
831
+ parser.add_argument("-w", "--wait", type=int, default=3, help="拍照之后等待时间,单位秒,deault 3s")
832
+ parser.add_argument("-d", "--devname", type=str, default=None, help="jpg名称后缀标签")
833
+ parser.add_argument("--reset", action="store_true", help="清除缓存,重新扫描设备")
834
+ args = parser.parse_args(argsList)
835
+ if args.reset and os.path.exists(CACHE_FILE):
836
+ os.remove(CACHE_FILE)
837
+ logi("[cache] 缓存已清除")
838
+ dev = DevOp(args.serial)
839
+ dev.checkCameraOpened()
840
+ name = args.file + args.devname if args.devname else args.file
841
+ dev.capJPG(name, wait=args.wait)
842
+
843
+
844
+ def CLI_capjpgThread(argsList):
845
+ parser = argparse.ArgumentParser(prog="capjpg2", description="同时拍摄两台机器jpg并导出到PC指定文件名")
846
+ parser.add_argument("-f", "--file", type=str, default="null", help="jpg名称")
847
+ parser.add_argument("-s1", "--serial1", type=str, required=True, help="设备1 SN号")
848
+ parser.add_argument("-s2", "--serial2", type=str, required=True, help="设备2 SN号")
849
+ parser.add_argument("-w", "--wait", type=int, default=3, help="拍照之后等待时间,单位秒")
850
+ parser.add_argument("-d1", "--devname1", type=str, required=True, help="设备1 jpg名称后缀标签")
851
+ parser.add_argument("-d2", "--devname2", type=str, required=True, help="设备2 jpg名称后缀标签")
852
+ args = parser.parse_args(argsList)
853
+ d1 = f"-f {args.file} -s {args.serial1} -w {args.wait} -d {args.devname1}".split()
854
+ d2 = f"-f {args.file} -s {args.serial2} -w {args.wait} -d {args.devname2}".split()
855
+ thread1 = threading.Thread(target=CLI_capjpg, args=(d1,))
856
+ thread2 = threading.Thread(target=CLI_capjpg, args=(d2,))
857
+ thread1.start()
858
+ thread2.start()
859
+ thread1.join()
860
+ thread2.join()
861
+
862
+
863
+ def CLI_wirelessHelp(args=None):
864
+ print(" linux: 10.1.164.212")
865
+ print(" windows: 10.1.176.39")
866
+ print(" 手机开启热点,PC连接热点")
867
+ print(' adb shell ifconfig |grep "inet addr" #查看ip')
868
+ print(" adb shell ip route #查看ip")
869
+ print(" adb tcpip 5 ")
870
+ print(" adb connect 192.168.191.2:5 ")
871
+ print(" adb disconnect 192.168.191.2:5 #断开连接")
872
+ print(" adb usb #切换回usb")
873
+
874
+
875
+ def CLI_rootRemount(argsList):
876
+ parser = argparse.ArgumentParser(prog="rootRemount", description="android设备root,remount")
877
+ parser.add_argument("-s", "--serial", type=str, default="", help="设备SN号(多设备需指定)")
878
+ args = parser.parse_args(argsList)
879
+
880
+ dev = DevOp(args.serial, needRoot=True)
881
+
882
+
883
+ def _dsrPrepare(dev_1: DevOp, dev_2: DevOp):
884
+ dev_1.adbTap((500, 1500), 0)
885
+ dev_1.adbTap((500, 1500), 0)
886
+ dev_1.adbTap((580, 128), 0)
887
+ dev_1.adbTap((580, 128), 0)
888
+ dev_2.adbTap((580, 128), 0)
889
+ dev_2.adbTap((580, 128), 0)
890
+ sleep(1)
891
+ dev_1.adbTap((580, 128), 0)
892
+ dev_2.adbTap((580, 128), 0)
893
+
894
+
895
+ def CLI_threadScreenRec(argsList):
896
+ parser = argparse.ArgumentParser(prog="screenRec", description="对android设备录屏")
897
+ parser.add_argument(
898
+ "-f", "--file", required=False, default="tmp", help="录制文件名,自动添加后缀.mp4 (默认tmp,可选)"
899
+ )
900
+ parser.add_argument("-t", "--time", required=False, default="30", help="录制时间,单位秒 (默认30秒,可选)")
901
+ parser.add_argument("-b", "--bitrate", required=False, default="2", help="录制比特率,单位Mbps (默认2,可选)")
902
+ parser.add_argument("--width", required=False, default=None, help="录制宽度 (默认lcd size,可选)")
903
+ parser.add_argument("--height", required=False, default=None, help="录制高度 (默认lcd size,可选)")
904
+ parser.add_argument(
905
+ "--no-debug", action="store_false", dest="debug", help="禁用调试模式,开启可在视频上添加时间戳等额外信息"
906
+ )
907
+ parser.set_defaults(debug=True) # 设置默认值为 True
908
+
909
+ parser.add_argument("--serial1", required=True, help="设备1 SN号")
910
+ parser.add_argument("--serial2", required=True, help="设备2 SN号")
911
+ args = parser.parse_args(argsList)
912
+ s1 = args.serial1
913
+ s2 = args.serial2
914
+ dev_1 = DevOp(s1)
915
+ dev_2 = DevOp(s2)
916
+ _dsrPrepare(dev_1, dev_2)
917
+
918
+ thread1 = threading.Thread(
919
+ target=dev_1.adbScreenRec,
920
+ kwargs={
921
+ "fileName": f"{args.file}_{s1}",
922
+ "limitTime_s": args.time,
923
+ "bitRate_Mbps": args.bitrate,
924
+ "inputWidth": args.width,
925
+ "inputHeight": args.height,
926
+ "debug": args.debug,
927
+ "isThread": True,
928
+ },
929
+ )
930
+
931
+ thread2 = threading.Thread(
932
+ target=dev_2.adbScreenRec,
933
+ kwargs={
934
+ "fileName": f"{args.file}_{s2}",
935
+ "limitTime_s": args.time,
936
+ "bitRate_Mbps": args.bitrate,
937
+ "inputWidth": args.width,
938
+ "inputHeight": args.height,
939
+ "debug": args.debug,
940
+ "isThread": True,
941
+ },
942
+ )
943
+
944
+ thread1.start()
945
+ thread2.start()
946
+ thread1.join()
947
+ thread2.join()
948
+
949
+
950
+ def CLI_clenDevCache(argsList):
951
+ if os.path.exists(CACHE_FILE):
952
+ os.remove(CACHE_FILE)
953
+ logi("[cache] 缓存已清除")
954
+
955
+
956
+ _COMMAND_REGISTRY = {}
957
+
958
+
959
+ def printHelp():
960
+ print("[device]")
961
+ print("pushso -h :push so to vendor/lib64")
962
+ print("scr -h :android 投屏")
963
+ print("po -h :android power {--off}")
964
+ print("cam :open camera")
965
+ print("sc -h :android 截屏- {name}:without suffix")
966
+ print("sr -h :android 录屏- {name}:without suffix")
967
+ print("dsr -h :android 双设备同时录屏")
968
+ print("iic {rw/det/dump} -h :android i2c 操作")
969
+ print("kcam :kill android cameraserver and clean camera app")
970
+ print("capjpg -h :capture jpg")
971
+ print("capjpg2 -h :capture jpg in a threaded way")
972
+ print("wi :show wireless help")
973
+ print("rt -h :root and remount")
974
+ print("cl :clear device cache")
975
+
976
+
977
+ CLI_pushSo = register_command("pushso", registry=_COMMAND_REGISTRY)(CLI_pushSo)
978
+ CLI_scrcpy = register_command("scr", registry=_COMMAND_REGISTRY)(CLI_scrcpy)
979
+ CLI_power = register_command("po", registry=_COMMAND_REGISTRY)(CLI_power)
980
+ CLI_cam = register_command("cam", registry=_COMMAND_REGISTRY)(CLI_cam)
981
+ CLI_screenCap = register_command("sc", registry=_COMMAND_REGISTRY)(CLI_screenCap)
982
+ CLI_screenRec = register_command("sr", registry=_COMMAND_REGISTRY)(CLI_screenRec)
983
+ CLI_threadScreenRec = register_command("dsr", registry=_COMMAND_REGISTRY)(CLI_threadScreenRec)
984
+ CLI_i2c = register_command("iic", registry=_COMMAND_REGISTRY)(CLI_i2c)
985
+ CLI_kcam = register_command("kcam", registry=_COMMAND_REGISTRY)(CLI_kcam)
986
+ CLI_capjpg = register_command("capjpg", registry=_COMMAND_REGISTRY)(CLI_capjpg)
987
+ CLI_capjpgThread = register_command("capjpg2", registry=_COMMAND_REGISTRY)(CLI_capjpgThread)
988
+ CLI_wirelessHelp = register_command("wi", registry=_COMMAND_REGISTRY)(CLI_wirelessHelp)
989
+ CLI_rootRemount = register_command("rt", registry=_COMMAND_REGISTRY)(CLI_rootRemount)
990
+ CLI_clenDevCache = register_command("cl", registry=_COMMAND_REGISTRY)(CLI_clenDevCache)
991
+
992
+
993
+ def deviceMain(args):
994
+ debugCtrl(1) if "debug" in args else debugCtrl(0)
995
+ args.remove("debug") if "debug" in args else args
996
+ args = normalize_args(args)
997
+ if len(args) < 1 or args[0] in {"help", "-h", "--help"}:
998
+ printHelp()
999
+ return
1000
+ cmd = args[0]
1001
+ handler = _COMMAND_REGISTRY.get(cmd)
1002
+ if handler is None:
1003
+ loge(f"unsupport command {cmd}")
1004
+ return
1005
+ handler(args[1:])
1006
+
1007
+
1008
+ if __name__ == "__main__":
1009
+ debugCtrl(1) if "debug" in sys.argv else debugCtrl(0)
1010
+ sys.argv.remove("debug") if "debug" in sys.argv else sys.argv
1011
+ deviceMain(sys.argv)