def turn_off_tv(self): # Load the public and private keys with open(self.adb_key_filepath) as f: priv = f.read() with open(self.adb_key_filepath + '.pub') as f: pub = f.read() signer = PythonRSASigner(pub, priv) # Connect android_tv = AdbDeviceTcp(self.tv_ip_address, self.adb_port, default_transport_timeout_s=9.) android_tv.connect(rsa_keys=[signer], auth_timeout_s=0.1) # Send a shell command logger.info('Shutting off TV via shell command') tv_off_command_key = '26' android_tv.shell('input keyevent %s' % tv_off_command_key) android_tv.close()
class AndroidTV(ImageSourceBase): """ Works via ADB requires USB debugging enabled and require adbkey file for authentication (generate new and copy files to device or use old ones usually found at [~/.android/adbkey, %HOMEDRIVE%%HOMEPATH%\.android\adbkey] Works with all android based TVs """ def __init__(self, ip, adbkeypath='adbkey', port=5555): self.signer = self._get_signer(adbkeypath) self.ip = ip self.port = port self.conn = AdbDeviceTcp(ip, port, default_timeout_s=9.) ## context manager def __enter__(self): self.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def connect(self): self.conn.connect(rsa_keys=[self.signer], auth_timeout_s=0.1) def close(self): self.conn.close() def get_ss(self): """ Take screenshot and create image (PIL) object""" raw_bytes = self.conn.shell('screencap -p', decode=False).replace( b'\x0D\x0A', b'\x0A') return Image.open(BytesIO(raw_bytes)) def get_theme_color(self): return self.get_dom_color_from_image(self.get_ss()) #return self.get_avg_color_from_image(self.get_ss()) def _get_signer(self, adbkeypath): with open(adbkeypath) as f: return PythonRSASigner('', f.read())
class ADBPython(object): """A manager for ADB connections that uses a Python implementation of the ADB protocol. Parameters ---------- host : str The address of the device; may be an IP address or a host name port : int The device port to which we are connecting (default is 5555) adbkey : str The path to the ``adbkey`` file for ADB authentication """ def __init__(self, host, port, adbkey=''): self.host = host self.port = int(port) self.adbkey = adbkey self._adb = AdbDeviceTcp(host=self.host, port=self.port, default_timeout_s=9.) # keep track of whether the ADB connection is intact self._available = False # use a lock to make sure that ADB commands don't overlap self._adb_lock = threading.Lock() @property def available(self): """Check whether the ADB connection is intact. Returns ------- bool Whether or not the ADB connection is intact """ return self._adb.available def close(self): """Close the ADB socket connection. """ self._adb.close() def connect(self, always_log_errors=True, auth_timeout_s=DEFAULT_AUTH_TIMEOUT_S): """Connect to an Android TV / Fire TV device. Parameters ---------- always_log_errors : bool If True, errors will always be logged; otherwise, errors will only be logged on the first failed reconnect attempt auth_timeout_s : float Authentication timeout (in seconds) Returns ------- bool Whether or not the connection was successfully established and the device is available """ try: with _acquire(self._adb_lock): # Catch exceptions try: # Connect with authentication if self.adbkey: # private key with open(self.adbkey) as f: priv = f.read() # public key try: with open(self.adbkey + '.pub') as f: pub = f.read() except FileNotFoundError: pub = '' signer = PythonRSASigner(pub, priv) self._adb.connect(rsa_keys=[signer], auth_timeout_s=auth_timeout_s) # Connect without authentication else: self._adb.connect(auth_timeout_s=auth_timeout_s) # ADB connection successfully established _LOGGER.debug( "ADB connection to %s:%d successfully established", self.host, self.port) self._available = True return True except OSError as exc: if self._available or always_log_errors: if exc.strerror is None: exc.strerror = "Timed out trying to connect to ADB device." _LOGGER.warning("Couldn't connect to %s:%d. %s: %s", self.host, self.port, exc.__class__.__name__, exc.strerror) # ADB connection attempt failed self.close() self._available = False return False except Exception as exc: # pylint: disable=broad-except if self._available or always_log_errors: _LOGGER.warning("Couldn't connect to %s:%d. %s: %s", self.host, self.port, exc.__class__.__name__, exc) # ADB connection attempt failed self.close() self._available = False return False except LockNotAcquiredException: _LOGGER.warning( "Couldn't connect to %s:%d because adb-shell lock not acquired.", self.host, self.port) self.close() self._available = False return False def pull(self, local_path, device_path): """Pull a file from the device using the Python ADB implementation. Parameters ---------- local_path : str The path where the file will be saved device_path : str The file on the device that will be pulled """ if not self.available: _LOGGER.debug( "ADB command not sent to %s:%d because adb-shell connection is not established: pull(%s, %s)", self.host, self.port, local_path, device_path) return with _acquire(self._adb_lock): _LOGGER.debug( "Sending command to %s:%d via adb-shell: pull(%s, %s)", self.host, self.port, local_path, device_path) self._adb.pull(device_path, local_path) return def push(self, local_path, device_path): """Push a file to the device using the Python ADB implementation. Parameters ---------- local_path : str The file that will be pushed to the device device_path : str The path where the file will be saved on the device """ if not self.available: _LOGGER.debug( "ADB command not sent to %s:%d because adb-shell connection is not established: push(%s, %s)", self.host, self.port, local_path, device_path) return with _acquire(self._adb_lock): _LOGGER.debug( "Sending command to %s:%d via adb-shell: push(%s, %s)", self.host, self.port, local_path, device_path) self._adb.push(local_path, device_path) return def shell(self, cmd): """Send an ADB command using the Python ADB implementation. Parameters ---------- cmd : str The ADB command to be sent Returns ------- str, None The response from the device, if there is a response """ if not self.available: _LOGGER.debug( "ADB command not sent to %s:%d because adb-shell connection is not established: %s", self.host, self.port, cmd) return None with _acquire(self._adb_lock): _LOGGER.debug("Sending command to %s:%d via adb-shell: %s", self.host, self.port, cmd) return self._adb.shell(cmd)
class GridInputter: # fractions of the screen that the grid takes up START_X = 0.0 START_Y = 0.22 END_X = 1.0 END_Y = 0.78 GEV_MULTIPLIER = 17.4 def __init__(self, grid_size, host='localhost', port=5555): self.dev = AdbDeviceTcp(host, port, default_transport_timeout_s=9.) self.dev.connect() self.swidth, self.sheight = self.screen_dimensions() self.grid_size = grid_size def __del__(self): self.dev.close() def screen_dimensions(self): line = self.dev.shell('wm size') result = re.match(r"Physical size: (?P<height>\d+)x(?P<width>\d+)\n", line) return int(result.group('width')), int(result.group('height')) def input_swipe(self, p1, p2, time=100): start_pix = self.get_pixels(p1) end_pix = self.get_pixels(p2) self.dev.shell( f'input swipe {start_pix[0]} {start_pix[1]} {end_pix[0]} {end_pix[1]} {time}' ) def simplify_path(self, path): path = path[:] new_path = [] i = 0 while i + 2 < len(path): if path[i + 0][0] == path[i + 1][0] == path[i + 2][0]: # we can remove the middle one path.remove(path[i + 1]) elif path[i + 0][1] == path[i + 1][1] == path[i + 2][1]: path.remove(path[i + 1]) else: new_path.append(path[i + 0]) i += 1 new_path += path[i:] # lastly, the game wants to help us complete paths so if the last two are neighbors we remove the last one if len(new_path) >= 3: if abs(new_path[-1][0] - new_path[-2][0]) + abs(new_path[-1][1] - new_path[-2][1]) == 1: new_path = new_path[:-1] return new_path def get_pixels(self, point): # translate grid point to pixel offset x, y = point dx = self.swidth / self.grid_size dy = self.sheight * (self.END_Y - self.START_Y) / self.grid_size px = x * dx + dx / 2 py = y * dy + dy / 2 + self.START_Y * self.sheight #print(f"{x,y} becomes {px,py}") return round(px), round(py)
class AdbConnect: def __init__(self, ip): self.ip = ip self.device = None self.user_root = '/storage/emulated/0/' self.current_dir = None self.dirs = [] self.files = [] if sys.platform.startswith('win'): import winreg key = winreg.OpenKey( winreg.HKEY_CURRENT_USER, r'Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' ) self.default_download_dir = Path( winreg.QueryValueEx(key, "Desktop")[0]).as_posix() else: self.default_download_dir = os.path.expanduser('~') self.current_down_dir = self.default_download_dir def set_ip(self, ip): self.ip = ip def connect(self): with open(os.path.join(os.path.expanduser('~'), '.android/adbkey')) as f: p = f.read() signer = PythonRSASigner('', p) self.device = AdbDeviceTcp(self.ip, 5555, default_timeout_s=9.) self.device.connect(rsa_keys=[signer], auth_timeout_s=0.1) # ls -l self.ls() print('connect') def disconnect(self): if self.device is not None: self.device.close() print('close') def check_dir_or_file(self, names): detailed_names = re.split('\n', names)[1:-1] # print(detailed_names) # 7 -> support space in filename self.dirs, self.files = [], [] for e in detailed_names: if e.startswith('d'): self.dirs.append(re.split('\s+', e, maxsplit=7)[-1]) else: self.files.append(re.split('\s+', e, maxsplit=7)[-1]) def ls(self, dir_name=None): if dir_name is None: self.current_dir = self.user_root else: self.current_dir = dir_name dir_file_names = self.device.shell('ls -la "' + self.current_dir + '"') self.check_dir_or_file(dir_file_names) # print('dirs', self.dirs, 'files', self.files, sep='\n') def change_directory(self, dir_name): if dir_name == '.': # do nothing pass elif dir_name == '..': self.current_dir = Path(self.current_dir).parent.as_posix() else: self.current_dir = Path(os.path.join(self.current_dir, dir_name)).as_posix() self.ls(self.current_dir) # print(self.current_dir, 'dirs', self.dirs, 'files', self.files, sep='\n') def pull_file(self, filename): download_dir = self.current_down_dir d_thread = DownloadThread(f=self.device.pull, filename=Path( os.path.join(self.current_dir, filename)).as_posix(), local_dir=download_dir) d_thread.start() # self.device.pull( # Path(os.path.join(self.current_dir, filename)).as_posix(), # os.path.join(download_dir, filename) # ) def thread_upload(self, local_files): # remote_dir -> current dir u_thread = UploadThread(f=self.device.push, filenames=local_files, remote_dir=self.current_dir) u_thread.start()
class cADB_Helper: def __init__(self): self.aGlobalRSA_KEYS:List = [] self.oDevice:Optional[AdbDeviceTcp] = None self.uHost:str = '' self.Load_RSA_KEYS() def Connect(self,uHost:str, uPort:str ,fTimeOut:float) -> cADB_Helper: self.uHost = uHost self.oDevice = AdbDeviceTcp(uHost, int(uPort), default_transport_timeout_s=fTimeOut) self.oDevice.connect(rsa_keys=self.aGlobalRSA_KEYS, auth_timeout_s=20.1) return self def Load_RSA_KEYS(self) -> None: # default adb key path aKeyPathes:List[cPath] = [cPath(OS_GetSystemUserPath() + '.android/adbkey')] #default Android Path if Globals.uPlatform==u'android': aKeyPathes.append(cPath(OS_GetSystemUserPath()+'misc/adb/adb_keys/adbkey')) #Download path aKeyPathes.append(Globals.oPathUserDownload+"/adbkey") for oPath in aKeyPathes: if oPath.Exists(): try: with open(str(oPath)) as f: oPriv = f.read() oSigner = PythonRSASigner('', oPriv) self.aGlobalRSA_KEYS.append(oSigner) Logger.info("RSA Keyfiles loaded from "+str(oPath)) except Exception as e: Logger.error("Error Loading RSA Keys from "+str(oPath)+" "+str(e)) else: Logger.debug("No RSA Keyfiles at "+oPath) # noinspection PyUnusedLocal def Shell(self, uCommand:str, fTimeOut:float=1.0) -> str: """Run command on the device, returning the output.""" return self.oDevice.shell(uCommand) def GetAppList(self, uCommand:str, fTimeOut:float=1.0) -> str: """fetches the list of all installed apps from an Android device The result will be saved for later calls """ uAppList: str = GetVar("ADB_APPLIST_" + self.uHost) if uAppList=='': uAppList = self.Shell(uCommand=uCommand, fTimeOut=fTimeOut) # Logger.debug("GetAppList:"+str(uAppList)) # print (uAppList) SetVar("ADB_APPLIST_" + self.uHost, uAppList) return uAppList def GetAppName(self, uCommand:str, fTimeOut:float=1.0) -> str: uAppList:str=GetVar("ADB_APPLIST_"+self.uHost) return self.FindPackageName(uAppName=uCommand,uAppList=uAppList) # noinspection PyMethodMayBeStatic def GetAppIntent(self, uCommand:str, fTimeOut:float=1.0) ->str: uAppName:str uAppDump:str uIntent:str = u'' uSearch:str uAppName,uAppDump = uCommand.split("|||") uSearch = uAppName+"/." aDumpLines:List[str] = uAppDump.splitlines() for uLine in aDumpLines: if uSearch in uLine: uIntent = uLine if "android.intent.category.LAUNCHER" in uLine or "android.intent.category.LEANBACK_LAUNCHER" in uLine: break if uIntent: uIntent = uIntent.strip().split(" ")[1] else: uSearch = uAppName for uLine in aDumpLines: if uSearch in uLine: uIntent = uLine if "android.intent.category.LAUNCHER" in uLine or "android.intent.category.LEANBACK_LAUNCHER" in uLine: break if uIntent: if "cmp=" in uIntent: uIntent = uIntent.strip().split("cmp=")[1] uIntent = uIntent[:-1] # something hacky until we identify why this is wrong identified from the dump uIntent=uIntent.replace(".nvidia","") else: uIntent = uIntent.strip().split(" ")[1] return uIntent def Close(self) -> None: self.oDevice.close() # noinspection PyMethodMayBeStatic def FindPackageName(self,*,uAppList:str,uAppName:str) -> str: """ ^(?=.*amazon)(?=.*music).*$ """ uPackageName:str = uAppName try: oResult=re.compile(uAppName,re.MULTILINE).search(uAppList) if oResult: # noinspection PyUnresolvedReferences uResult=uAppList[oResult.regs[0][0]:oResult.regs[0][1]] uPackageName = uResult.split(u"=")[-1] except Exception as e: Logger.info("FindPackageName: couldn't validate Appname as regex, returning the default value: %s (%s)" % (uAppName,str(e))) return uPackageName
class SX5_Manager(object): """ """ # ************************************************* # # **************** Private Methods **************** # # ************************************************* # def __init__(self): """ Constructor""" self._sx5_shell_values_dict = { 'SupercapVoltage_mV': { 'command': 'cat /sys/bus/platform/devices/vendor:supercap/voltage', 'value:': "" }, 'CapokFlag': { 'command': 'cat /sys/bus/platform/devices/vendor:supercap/capok', 'value': "" }, 'BatteryCharge_%': { 'command': 'cat sys/class/power_supply/bq27750-0/capacity', 'value': "" }, 'BatteryVoltage_uV': { 'command': 'cat sys/class/power_supply/bq27750-0/voltage_now', 'value': "" } } self._sx5_config_dict: dict self._sx5_device: AdbDeviceTcp pass # ---------------------------------------------------------------- # # ----------------------- Private Methods ------------------------ # # ---------------------------------------------------------------- # def _parse_config_file(self): """""" self._sx5_config_dict = XmlDictConfig( ElementTree.parse('Config.xml').getroot())['LoopTest'] pass def _adb_init(self): """""" count = 0 res = -1 num_of_try = 60 # Start Timer timer = Timer() timer.start() while (count < num_of_try): if timer.elapsed_time_s(2) >= 1: print("- Waiting for SX5 device " + "." * count, end='\r') res = subprocess.run( "adb devices", text=True, capture_output=True).stdout.find( self._sx5_config_dict['SX5']['device']) if (res != -1): timer.stop() break else: timer.reset() count += 1 if res == -1: sys.stdout.write("\033[K") print("No Device Found") raise ADB_Error else: sys.stdout.write("\033[K") self._sx5_device = AdbDeviceTcp( host=self._sx5_config_dict['SX5']['ip'], port=int(self._sx5_config_dict['SX5']['port']), default_transport_timeout_s=9.) pass def _adb_tcp_connect(self): """""" try: self._sx5_device.connect(auth_timeout_s=0.1) print("- SX5 Connected") except: print( "Timeout expired: check if device is on and if the IP is correct" ) raise pass def _adb_tcp_disconnect(self): """""" self._sx5_device.close() pass # ---------------------------------------------------------------- # # ------------------------ Public Methods ------------------------ # # ---------------------------------------------------------------- # def init(self): """ Initialize class and connect SX5 to ADB over TCP-IP protocol """ # Parse config file self._parse_config_file() # Initialize adb connection try: self._adb_init() except ADB_Error: sys.exit() try: self._adb_tcp_connect() except: subprocess.run("adb disconnect") subprocess.run("adb tcpip {port}".format( port=int(self._sx5_config_dict['SX5']['port']))) #self._adb_init() self._adb_tcp_connect() pass def _read_shell(self, cmd: str): """""" max_attempt = 10 attempt_count = 0 stdout = "" while attempt_count < max_attempt: try: stdout = self._sx5_device.shell(cmd) except: # Try to establish a new connection if no response... try: self._sx5_device = AdbDeviceTcp( host=self._sx5_config_dict['SX5']['ip'], port=int(self._sx5_config_dict['SX5']['port']), default_transport_timeout_s=9.) except: pass try: self._sx5_device.connect(auth_timeout_s=0.2) except: pass attempt_count += 1 else: break if attempt_count >= max_attempt: raise exceptions.TcpTimeoutException pass return stdout def read_sx5_value(self, value: str): """ """ if value in self._sx5_shell_values_dict.keys(): self._sx5_shell_values_dict[value]['value'] = self._read_shell( self._sx5_shell_values_dict[value]['command']) return def update_all_sx5_values(self): """ """ # Variables concat_cmd = "" stdout = "" for key in self._sx5_shell_values_dict: concat_cmd += self._sx5_shell_values_dict[key]['command'] + " && " # Delete last "&&" concat_cmd = concat_cmd[:-3] # Read output from adb shell stdout = self._read_shell(concat_cmd) keys_list = list(self._sx5_shell_values_dict.keys()) stdout_splitted = stdout.split("\n")[:-1] # Add each element of stdout to the proper value of shel dictionary for index, item in enumerate(stdout_splitted): self._sx5_shell_values_dict[ keys_list[index]]["value"] = stdout_splitted[index] return @property def supercap_voltage_mV(self): return int(self._sx5_shell_values_dict['SupercapVoltage_mV'] ['value'].strip(" mV\n")) @property def capok_flag(self): return bool( int(self._sx5_shell_values_dict['CapokFlag']['value'].strip("\n"))) @property def battery_charge_pct(self): return int(self._sx5_shell_values_dict['BatteryCharge_%'] ['value'].strip("\n")) @property def battery_voltage_uV(self): return int(self._sx5_shell_values_dict['BatteryVoltage_uV'] ['value'].strip(" mV\n")) @property def battery_voltage_mV(self): return self.battery_voltage_uV / 1000
class AdbConnector: """ wraps adb_shell functions for simpler automation """ def __init__(self, ip=None, need_auth=True, device_name="", auto_reconnect_seconds=60, verbose=False): self.ip = ip self.device_name = device_name self.need_auth = need_auth self.auto_reconnect_seconds = auto_reconnect_seconds self.verbose=verbose self._event_handler = AdbConnectorEventManager() self._last_connected_time = time.time() self._device_resolution = None self._device = None self.width = self.screen_width() self.height = self.screen_height() print(f">>> Target phone sreen size is [{self.width} x {self.height}]") def _connect(self): """ in my experience, it was better to connect, send commands, disconnect right away rather than connect once and send many commands for hours => getting very slow at some point Now added auto-reconnect feature with argument auto_reconnect_seconds defaulting to 1 min """ now = time.time() if self._device and self._last_connected_time + self.auto_reconnect_seconds < now: self._disconnect() if self._device: return if self.verbose: print(f">>> connecting to {self.ip}") self._last_connected_time = now self._device = AdbDeviceTcp(self.ip, default_timeout_s=self.auto_reconnect_seconds) if not self.need_auth: self._device.connect(auth_timeout_s=0.1) else: with open(os.path.expanduser('~/.android/adbkey')) as f: private_key = f.read() signer = PythonRSASigner('', private_key) self._device.connect(rsa_keys=[signer], auth_timeout_s=0.1) if self.verbose: print(f">>> connected") def _disconnect(self): self._device.close() self._device = None if self.verbose: print(">>> disconnected") def _shell(self, command, **kwargs): if self.verbose: print(f">>> shell {command}") return self._device.shell(command, **kwargs) def _pull(self, from_file, to_file): if self.verbose: print(f">>> pull {from_file} to {to_file}") return self._device.pull(from_file, to_file) def tap(self, x, y, wait_ms = 100): """ tap the screen and force wait 100ms by default to simulate real taps if several in a row :param x: :param y: :param wait_ms: :return: """ assert 0 <= x < self.width, "out of bound" assert 0 <= y < self.height, "out of bound" self._connect() self._shell(f'{CMD_SHELL_TAP} {x:.0f} {y:.0f}') self.wait(wait_ms) def press(self, x, y, time_ms = 0, wait_ms = 0): """ long tap, implemented with a static swipe :param x: :param y: :param time_ms: :param wait_ms: :return: """ self._connect() self._shell(f'{CMD_SHELL_SWIPE} {x:.0f} {y:.0f} {x:.0f} {y:.0f} {time_ms}') self.wait(wait_ms) def swipe(self, x1, y1, x2, y2, time_ms = 0, wait_ms = 0): """ swipe from point (x1, y1) to point (x2, y2) in the span of time_ms careful, swipe has inertia, so if it is used to scroll a screen for example, screen will most likely keep scrolling at the end of the swipe To avoid that effect, use the event_move() instead :param x1: :param y1: :param x2: :param y2: :param time_ms: :param wait_ms: :return: """ self._connect() print(x1, y1, x2, y2) self._shell(f'{CMD_SHELL_SWIPE} {x1:.0f} {y1:.0f} {x2:.0f} {y2:.0f} {time_ms}') self.wait(wait_ms) @staticmethod def wait(time_ms): """ to wait some time. Not relying on adb, just convenient :param time_ms: :return: """ if time_ms > 0: time.sleep(time_ms / 1000.0) @DeprecationWarning def event_press(self, slot, x, y): self._event_handler.event_press(slot, x, y) @DeprecationWarning def event_move(self, slot, x, y): self._event_handler.event_move(slot, x, y) @DeprecationWarning def event_release(self, slot): self._event_handler.event_release(slot) @DeprecationWarning def event_flush(self): self._connect() self._device.shell(self._event_handler.event_flush(self.device_name)) # too much info: debug only def print_all_process_info(self): self._connect() processes_with_focus = self._shell(CMD_WINDOWS_DUMP) print(processes_with_focus) # todo: does not work anymore with latest android versions? @DeprecationWarning def process_has_focus(self, process_name): self._connect() all_processes = self._shell(f"{CMD_WINDOWS_DUMP} | grep -i {process_name} | grep -i mcurrentfocus") return len(all_processes) > 0 # todo: needs PR merged in adb_shell module def listen(self): self._connect() for line in self._device.streaming_shell(CMD_GET_EVENT): print(line) def _get_screen_resolution(self): if not self._device_resolution: self._connect() header_width_x_height = self._shell(CMD_WM_SIZE) self._device_resolution = tuple(map(int, re.findall("\d+", header_width_x_height))) return self._device_resolution def screen_width(self): return self._get_screen_resolution()[0] def screen_height(self): return self._get_screen_resolution()[1] def get_screenshot(self, raw=False, pull=True) -> Image: if pull: if raw: return self._get_screenshot_raw_pull_file() return self._get_screenshot_png_pull_file() if raw: raise Exception("Not implemented") return self._get_screenshot_png_stream() @staticmethod def get_temp_remote_filepath(extension): random_part = next(tempfile._get_candidate_names()) return os.path.join(REMOTE_SCREENSHOT_DIRECTORY, f"screenshot_adb_{random_part}.{extension}") @staticmethod def get_temp_local_filepath(extension): if not os.path.exists(OUTPUT_DIR): os.mkdir(OUTPUT_DIR) return os.path.realpath(f"{OUTPUT_DIR}/screenshot.{extension}") def _get_screenshot_png_pull_file(self) -> Image: self._connect() png_remote_filepath = self.get_temp_remote_filepath("png") png_local_filepath = self.get_temp_local_filepath("png") self._shell(f"{CMD_SCREENSHOT_PNG} {png_remote_filepath}") self._pull(png_remote_filepath, png_local_filepath) self._shell(f"rm {png_remote_filepath}") return Image.open(png_local_filepath) def _get_screenshot_raw_pull_file(self) -> Image: self._connect() raw_remote_filepath = self.get_temp_remote_filepath("raw") raw_local_filepath = self.get_temp_local_filepath("raw") self._shell(f"{CMD_SCREENSHOT_RAW} {raw_remote_filepath}") self._pull(raw_remote_filepath, raw_local_filepath) self._shell(f"rm {raw_remote_filepath}") with open(raw_local_filepath, 'rb') as f: raw = f.read() return Image.frombuffer('RGBA', (self.screen_height(), self.screen_width()), raw[12:], 'raw', 'RGBX', 0, 1) # todo: use exec-out instead of shell def _get_screenshot_png_stream(self) -> Image: self._connect() raw = self._shell(CMD_SCREENSHOT_PNG, decode=False) image = Image.open(io.BytesIO(raw)) return image