commit cc59e8b8da169448826643aabe624d3089dc6dcb Author: YiLin <482244139@qq.com> Date: Mon Feb 2 11:47:53 2026 +0800 first commit diff --git a/__pycache__/permissions.cpython-312.pyc b/__pycache__/permissions.cpython-312.pyc new file mode 100644 index 0000000..23118b8 Binary files /dev/null and b/__pycache__/permissions.cpython-312.pyc differ diff --git a/appium_automation.log b/appium_automation.log new file mode 100644 index 0000000..e69de29 diff --git a/globals/__pycache__/alarm.cpython-312.pyc b/globals/__pycache__/alarm.cpython-312.pyc new file mode 100644 index 0000000..2739171 Binary files /dev/null and b/globals/__pycache__/alarm.cpython-312.pyc differ diff --git a/globals/__pycache__/apis.cpython-312.pyc b/globals/__pycache__/apis.cpython-312.pyc new file mode 100644 index 0000000..3e0e4db Binary files /dev/null and b/globals/__pycache__/apis.cpython-312.pyc differ diff --git a/globals/__pycache__/driver_utils.cpython-312.pyc b/globals/__pycache__/driver_utils.cpython-312.pyc new file mode 100644 index 0000000..81dc29d Binary files /dev/null and b/globals/__pycache__/driver_utils.cpython-312.pyc differ diff --git a/globals/__pycache__/ex_apis.cpython-312.pyc b/globals/__pycache__/ex_apis.cpython-312.pyc new file mode 100644 index 0000000..ce0fc74 Binary files /dev/null and b/globals/__pycache__/ex_apis.cpython-312.pyc differ diff --git a/globals/__pycache__/global_variable.cpython-312.pyc b/globals/__pycache__/global_variable.cpython-312.pyc new file mode 100644 index 0000000..6e5b65c Binary files /dev/null and b/globals/__pycache__/global_variable.cpython-312.pyc differ diff --git a/globals/__pycache__/ids.cpython-312.pyc b/globals/__pycache__/ids.cpython-312.pyc new file mode 100644 index 0000000..e351c53 Binary files /dev/null and b/globals/__pycache__/ids.cpython-312.pyc differ diff --git a/globals/alarm.py b/globals/alarm.py new file mode 100644 index 0000000..447bb22 --- /dev/null +++ b/globals/alarm.py @@ -0,0 +1,88 @@ +import subprocess +import os +import logging + + +def push_audio_to_phone(device_id, local_audio_path, phone_audio_path): # 移除默认值 + """ + 将本地音频文件推送到手机指定路径 + """ + # 检查本地文件是否存在 + if not os.path.exists(local_audio_path): + logging.error(f"本地音频文件不存在: {local_audio_path}") + return False + + # 执行ADB推送命令 + cmd = ["adb", "-s", device_id, "push", local_audio_path, phone_audio_path] + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + + if result.returncode == 0: + logging.info(f"音频文件已推送到手机: {phone_audio_path}") + return True + else: + logging.error(f"推送音频文件失败: {result.stderr.strip()}") + return False + except subprocess.TimeoutExpired: + logging.error("推送音频文件超时") + return False + +def play_audio_on_phone(device_id, phone_audio_path): + """ + 控制手机播放指定路径的音频文件 + """ + # 检查文件是否存在于手机 + check_cmd = ["adb", "-s", device_id, "shell", "ls", phone_audio_path] + check_result = subprocess.run(check_cmd, capture_output=True, text=True) + + if check_result.returncode != 0: + logging.error(f"音频文件在手机上不存在: {phone_audio_path}") + return False + + # 使用am命令播放音频 + cmd = [ + "adb", "-s", device_id, "shell", + "am", "start", + "-a", "android.intent.action.VIEW", + "-t", "audio/*", + "-d", f"file://{phone_audio_path}" + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + logging.info(f"已成功触发手机播放音频: {phone_audio_path}") + return True + else: + logging.error(f"播放音频失败: {result.stderr.strip()}") + return False + except subprocess.TimeoutExpired: + logging.error("播放音频命令超时") + return False + +def play_system_alarm(device_id): + """ + 播放系统内置的警报声 + """ + try: + # 方法1:使用系统铃声URI + cmd = [ + "adb", "-s", device_id, "shell", + "am", "start", + "-a", "android.intent.action.VIEW", + "-d", "content://settings/system/alarm_alert" + ] + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + logging.info("成功触发系统警报声") + return True + else: + logging.error(f"播放系统警报失败: {result.stderr.strip()}") + return False + + except Exception as e: + logging.error(f"播放系统警报时出错: {str(e)}") + return False \ No newline at end of file diff --git a/globals/apis.py b/globals/apis.py new file mode 100644 index 0000000..223ff0d --- /dev/null +++ b/globals/apis.py @@ -0,0 +1,479 @@ +import requests +import json +import logging +import socket +from typing import Optional, Dict, Any +import globals.global_variable as global_variable + +def send_tcp_command(command="StartMultiple", host="127.0.0.1", port=8888, timeout=10): + """ + 使用TCP协议发送命令到指定地址和端口 + + 参数: + command: 要发送的命令字符串(默认:"StartMultiple") + host: 目标主机地址(默认:"127.0.0.1") + port: 目标端口(默认:8888) + timeout: 连接超时时间(秒,默认:10) + + 返回: + 成功返回服务器响应(字符串),失败返回None + """ + # 创建TCP套接字 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + # 设置超时时间 + sock.settimeout(timeout) + + # 连接到目标服务器 + sock.connect((host, port)) + logging.info(f"已成功连接到 {host}:{port}") + + # 发送命令(注意:需要根据服务器要求的编码格式发送,这里用UTF-8) + sock.sendall(command.encode('utf-8')) + logging.info(f"已发送命令: {command}") + + # 接收服务器响应(缓冲区大小1024字节,可根据实际情况调整) + response = sock.recv(1024) + if response: + response_str = response.decode('utf-8') + logging.info(f"收到响应: {response_str}") + return response_str + else: + logging.info("未收到服务器响应") + return None + + except ConnectionRefusedError: + logging.info(f"连接被拒绝,请检查 {host}:{port} 是否开启服务") + return None + except socket.timeout: + logging.info(f"连接超时({timeout}秒)") + return None + except Exception as e: + logging.info(f"发送命令时发生错误: {str(e)}") + return None + +def get_breakpoint_list(): + """ + 获取需要处理的断点列表 + """ + # 请求参数 + params = { + 'user_name': global_variable.GLOBAL_USERNAME + } + + # 请求地址 + url = "https://engineering.yuxindazhineng.com/index/index/get_name_all" + + try: + # 发送GET请求 + response = requests.get(url, params=params, timeout=30) + + # 检查请求是否成功 + if response.status_code == 200: + result = response.json() + + # 检查接口返回状态 + if result.get('code') == 0: + data = result.get('data', []) + logging.info("成功获取断点列表,数据条数:", len(data)) + + # 打印断点信息 + # for item in data: + # logging.info(f"线路编码: {item.get('line_num')}, " + # f"线路名称: {item.get('line_name')}, " + # f"状态: {item.get('status')}, " + # f"用户: {item.get('name')}") + + return data + else: + logging.info(f"接口返回错误: {result.get('code')}") + return [{"id": 37, + "user_name": "wangshun", + "name": "wangshun", + "line_num": "L193588", + "line_name": "CDWZQ-2标-155号路基左线-461221-461570-155左-平原", + "status": 3 + }] + else: + logging.info(f"请求失败,状态码: {response.status_code}") + return [] + + except requests.exceptions.RequestException as e: + logging.info(f"请求异常: {e}") + return [] + except ValueError as e: + logging.info(f"JSON解析错误: {e}") + return [] + +def get_measurement_task(): + """ + 获取测量任务 + 返回: 如果有状态为1的数据返回任务信息,否则返回None + """ + try: + url = "https://engineering.yuxindazhineng.com/index/index/getOne" + + # 获取用户名 + user_name = global_variable.GLOBAL_USERNAME + if not user_name: + logging.error("未设置用户名,无法获取测量任务") + return None + + # 构造请求参数 + data = { + "user_name": user_name + } + + logging.info(f"请求参数: user_name={user_name}") + response = requests.post(url, data=data, timeout=10) + response.raise_for_status() + + data = response.json() + logging.info(f"接口返回数据: {data}") + + if data.get('code') == 0 and data.get('data'): + task_data = data['data'] + if task_data.get('status') == 1: + logging.info(f"获取到测量任务: {task_data}") + return task_data + else: + logging.info("获取到的任务状态不为1,不执行测量") + return None + else: + logging.warning("未获取到有效任务数据") + return None + + except Exception as e: + logging.error(f"获取测量任务失败: {str(e)}") + return None + +def get_end_with_num(): + """ + 根据线路编码获取测量任务 + 返回: 如果有状态为1的数据返回任务信息,否则返回None + """ + try: + url = "https://engineering.yuxindazhineng.com/index/index/getOne3" + + # 获取用户名 + user_name = global_variable.GLOBAL_USERNAME + line_num = global_variable.GLOBAL_LINE_NUM + if not line_num: + logging.error("未设置线路编码,无法获取测量任务") + return None + if not user_name: + logging.error("未设置用户名,无法获取测量任务") + return None + + # 构造请求参数 + data = { + "user_name": user_name, + "line_num": line_num + } + + # logging.info(f"请求参数: user_name={user_name}, line_num={line_num}") + response = requests.post(url, data=data, timeout=10) + response.raise_for_status() + + data = response.json() + logging.info(f"接口返回数据: {data}") + + if data.get('code') == 0 and data.get('data'): + task_data = data['data'] + if task_data.get('status') == 3: + logging.info(f"获取到测量任务: {task_data}") + return task_data + else: + logging.info("获取到的任务状态不为3,不执行测量") + return None + else: + # logging.warning("未获取到有效任务数据") + return None + + except Exception as e: + logging.error(f"获取测量任务失败: {str(e)}") + return None + + + + + +def get_work_conditions_by_linecode(linecode: str) -> Optional[Dict[str, Dict]]: + """ + 通过线路编码获取工况信息 + + Args: + linecode: 线路编码,如 "L118134" + + Returns: + 返回字典,格式为 {point_id: {"sjName": "", "workinfoname": "", "work_type": ""}} + 如果请求失败返回None + """ + url="http://www.yuxindazhineng.com:3002/api/comprehensive_data/get_settlement_by_linecode" + max_retries = 3 # 最大重试次数 + retry_count = 0 # 当前重试计数 + while retry_count < max_retries: + try: + # 准备请求参数 + payload = {"linecode": linecode} + headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + } + + logging.info(f"发送POST请求到: {url}") + logging.info(f"请求参数: {payload}") + + # 发送POST请求 + response = requests.post( + url, + json=payload, + headers=headers, + timeout=30 + ) + + # 检查响应状态 + if response.status_code != 200: + logging.error(f"HTTP请求失败,状态码: {response.status_code}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + continue # 继续重试 + + # 解析响应数据 + try: + result = response.json() + except json.JSONDecodeError as e: + logging.error(f"JSON解析失败: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + continue # 继续重试 + + + # 检查API返回码 + if result.get('code') != 0: + logging.error(f"API返回错误: {result.get('message', '未知错误')}") + return None + + # 提取数据 + data_list = result.get('data', []) + if not data_list: + logging.warning("未找到工况数据") + return {} + + # 处理数据,提取所需字段 + work_conditions = {} + for item in data_list: + point_id = item.get('aname') + if point_id: + work_conditions[point_id] = { + "sjName": item.get('sjName', ''), + "workinfoname": item.get('next_workinfo', ''), + "work_type": item.get('work_type', '') + } + + logging.info(f"成功提取 {len(work_conditions)} 个测点的工况信息") + return work_conditions + + except requests.exceptions.RequestException as e: + logging.error(f"网络请求异常: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + except json.JSONDecodeError as e: + logging.error(f"JSON解析失败: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + except Exception as e: + logging.error(f"获取工况信息时发生未知错误: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)") + # 达到最大重试次数仍失败 + logging.error(f"已达到最大重试次数 ({max_retries} 次),请求失败") + return None + +def get_user_max_variation(username: str) -> Optional[int]: + """ + 调用POST接口根据用户名获取用户的max_variation信息 + + Args: + username: 目标用户名,如 "chzq02-02guoyu" + + Returns: + 成功:返回用户的max_variation整数值 + 失败:返回None + """ + # 接口基础配置 + api_url = "http://www.yuxindazhineng.com:3002/api/accounts/get" + timeout = 30 # 超时时间(避免请求长时间阻塞) + + # 1. 准备请求参数与头部 + # 接口要求的POST参数(JSON格式) + payload = {"username": username} + # 请求头部:指定JSON格式,模拟浏览器UA避免被接口拦截 + headers = { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + try: + # 2. 发送POST请求 + + response = requests.post( + url=api_url, + json=payload, # 自动将字典转为JSON字符串,无需手动json.dumps() + headers=headers, + timeout=timeout + ) + + # 3. 检查HTTP响应状态(200表示请求成功到达服务器) + response.raise_for_status() # 若状态码非200(如404、500),直接抛出HTTPError + logging.info(f"接口请求成功,HTTP状态码:{response.status_code}") + + # 4. 解析JSON响应(处理文档中提到的"网页解析失败"风险) + try: + response_data = response.json() + except json.JSONDecodeError as e: + logging.error(f"接口返回数据非JSON格式,解析失败:{str(e)}") + logging.error(f"接口原始返回内容:{response.text[:500]}") # 打印前500字符便于排查 + return None + + # 5. 检查接口业务逻辑是否成功(按需求中"code=0表示查询成功") + if response_data.get("code") != 0: + logging.error(f"接口查询失败,业务错误信息:{response_data.get('message', '未知错误')}") + return None + + # 6. 验证返回数据结构并提取max_variation + data_list = response_data.get("data", []) + if not data_list: + logging.warning(f"查询到用户名 {username},但未返回账号数据") + return None + + # 检查第一条数据是否包含max_variation + first_user = data_list[0] + if "max_variation" not in first_user: + logging.warning(f"用户 {username} 的返回数据中缺少 max_variation 字段") + return None + + max_variation = first_user["max_variation"] + logging.info(f"成功查询到用户 {username} 的 max_variation:{max_variation}") + + # 7. 直接返回max_variation的值 + return max_variation + + # 处理请求过程中的异常(网络问题、超时等) + except requests.exceptions.RequestException as e: + logging.error(f"接口请求异常(网络/超时/服务器不可达):{str(e)}") + # 若为连接错误,提示检查文档中提到的"不支持的网页类型"或域名有效性 + if "ConnectionRefusedError" in str(e) or "Failed to establish a new connection" in str(e): + logging.error(f"建议排查:1. 接口域名 {api_url} 是否可访问;2. 服务器是否正常运行;3. 端口3002是否开放") + return None + + # 处理其他未知异常 + except Exception as e: + logging.error(f"获取用户 {username} 的 max_variation 时发生未知错误:{str(e)}") + return None + +requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) +def get_line_info_and_save_global(user_name: str) -> bool: + """ + 调用get_name_all接口,提取status=3的line_num和line_name存入全局字典 + :param user_name: 接口请求参数,如"wangshun" + :return: 执行成功返回True,失败/异常返回False + """ + # 接口基础配置 + api_url = "https://engineering.yuxindazhineng.com/index/index/get_name_all" + request_params = {"user_name": user_name} # GET请求参数 + timeout = 10 # 请求超时时间(秒),避免卡进程 + + try: + # 1. 发送GET请求 + response = requests.get( + url=api_url, + params=request_params, # GET参数用params传递,自动拼接到URL后,规范且防乱码 + timeout=timeout, + verify=False # 禁用SSL验证,适配HTTPS接口 + ) + + # 2. 校验HTTP状态码(先确保请求本身成功) + if response.status_code != 200: + logging.error(f"接口请求失败,HTTP状态码异常:{response.status_code},响应内容:{response.text}") + return False + + # 3. 解析JSON响应(接口返回是JSON格式,需解析为字典) + try: + response_data = response.json() + except Exception as e: + logging.error(f"接口返回内容非合法JSON,无法解析:{response.text},错误:{str(e)}") + return False + + # 4. 校验业务状态码(接口约定:code=0成功,-1失败) + business_code = response_data.get("code") + if business_code == 0: + logging.info("接口业务请求成功,开始解析数据") + elif business_code == -1: + logging.error(f"接口业务请求失败,业务状态码code=-1,返回数据:{response_data}") + return False + else: + logging.warning(f"接口返回未知业务状态码:{business_code},请确认接口文档") + return False + + # 5. 提取data字段,校验数据是否存在 + api_data_list = response_data.get("data") + if not api_data_list: + logging.warning("接口业务成功,但data字段为空或无数据") + return False + + # 6. 校验data是否为列表类型 + if not isinstance(api_data_list, list): + logging.error(f"data字段不是列表类型,实际类型:{type(api_data_list)},内容:{api_data_list}") + return False + + found_valid_data = False + + # 7. 遍历列表,提取所有status=3的数据 + for item in api_data_list: + # 确保每个item是字典 + if not isinstance(item, dict): + logging.warning(f"列表中的元素不是字典类型,跳过:{item}") + continue + + # 获取字段值 + data_status = item.get("status") + line_num = item.get("line_num") + line_name = item.get("line_name") + + # 校验status是否为3,且目标字段非空 + if data_status == 3 and line_num and line_name: + # # 存入全局字典:key=line_num,value=line_name + # global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_num] = line_name + # 存入全局字典:key=line_name,value=line_num + global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_name] = line_num + + # 如果line_name不在列表中,则添加 + if line_name not in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST: + global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST.append(line_name) + + logging.info(f"找到status=3的线路信息:line_num={line_num}, line_name={line_name}") + found_valid_data = True + + if found_valid_data: + logging.info(f"成功提取所有status=3的线路信息,当前全局字典数据:{global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT}") + return True + else: + logging.warning("data列表中未找到任何status=3且字段完整的线路信息") + return False + + # 捕获所有请求相关异常(超时、连接失败、网络异常等) + except requests.exceptions.Timeout: + logging.error(f"调用get_name_all接口超时,超时时间:{timeout}秒,请求参数:{request_params}") + return False + except requests.exceptions.ConnectionError: + logging.error(f"调用get_name_all接口连接失败,检查网络或接口地址是否正确:{api_url}") + return False + except Exception as e: + logging.error(f"调用get_name_all接口时发生未知异常:{str(e)}", exc_info=True) # exc_info=True打印异常堆栈,方便排查 + return False \ No newline at end of file diff --git a/globals/driver_utils.py b/globals/driver_utils.py new file mode 100644 index 0000000..6954ad6 --- /dev/null +++ b/globals/driver_utils.py @@ -0,0 +1,763 @@ +import logging +import time +import subprocess +import traceback +import socket +import os +import requests +from appium import webdriver +from appium.webdriver.common.appiumby import AppiumBy +from appium.webdriver.appium_service import AppiumService +from appium.options.android import UiAutomator2Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException, InvalidSessionIdException, WebDriverException +import globals.global_variable as global_variable + + +def init_appium_driver(device_id, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"): + """ + 初始化Appium驱动的全局函数 + + 参数: + device_id: 设备ID + app_package: 应用包名,默认为"com.bjjw.cjgc" + app_activity: 应用启动Activity,默认为".activity.LoginActivity" + + 返回: + (driver, wait): (WebDriver实例, WebDriverWait实例),如果初始化失败则抛出异常 + """ + logging.info(f"设备 {device_id} 开始初始化Appium驱动") + + # 创建并配置Appium选项 + options = UiAutomator2Options() + options.platform_name = "Android" + options.device_name = device_id + options.app_package = app_package + options.app_activity = app_activity + options.automation_name = "UiAutomator2" + options.no_reset = True + options.auto_grant_permissions = True + options.new_command_timeout = 28800 + options.udid = device_id + + # 增加uiautomator2服务器启动超时时间 + options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒 + # 增加连接超时设置 + options.set_capability('connection_timeout', 120000) # 120秒 + + try: + # 连接Appium服务器 + driver_url = "http://127.0.0.1:4723/wd/hub" + logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}") + driver = webdriver.Remote(driver_url, options=options) + logging.info(f"设备 {device_id} Appium服务器连接成功") + + # 初始化等待对象 + wait = WebDriverWait(driver, 20) + logging.info(f"设备 {device_id} WebDriverWait初始化成功") + + # 等待应用稳定 + time.sleep(2) + + logging.info(f"设备 {device_id} Appium驱动初始化完成") + return driver, wait + + except Exception as e: + logging.error(f"设备 {device_id} 初始化驱动失败: {str(e)}") + logging.error(f"错误类型: {type(e).__name__}") + logging.error(f"错误堆栈: {traceback.format_exc()}") + # 如果驱动已创建,尝试关闭 + if 'driver' in locals() and driver: + try: + driver.quit() + except: + pass + raise + +def check_session_valid(driver, device_id=None): + """ + 检查当前会话是否有效 + + 参数: + driver: WebDriver实例 + device_id: 设备ID(可选) + + 返回: + bool: 会话有效返回True,否则返回False + """ + if device_id is None: + device_id = global_variable.GLOBAL_DEVICE_ID + device_str = f"设备 {device_id} " if device_id else "" + + if not driver: + logging.debug(f"{device_str}驱动实例为空") + return False + + try: + # 首先检查driver是否有session_id属性 + if not hasattr(driver, 'session_id') or not driver.session_id: + logging.debug(f"{device_str}驱动缺少有效的session_id") + return False + + # 尝试获取当前上下文 + current_context = driver.current_context + logging.debug(f"{device_str}会话检查通过,当前上下文: {current_context}") + return True + + except InvalidSessionIdException: + logging.debug(f"{device_str}会话已失效") + return False + + except WebDriverException as e: + error_msg = str(e).lower() + + # 明确的会话失效错误 + if any(phrase in error_msg for phrase in [ + "session is either terminated or not started", + "could not proxy command to the remote server", + "socket hang up", + "connection refused", + "max retries exceeded" + ]): + logging.debug(f"{device_str}会话连接错误: {error_msg[:100]}") + return False + else: + logging.debug(f"{device_str}WebDriver异常但可能不是会话失效: {error_msg[:100]}") + return True + + except (ConnectionError, ConnectionRefusedError, ConnectionResetError) as e: + logging.debug(f"{device_str}网络连接错误: {str(e)}") + return False + + except Exception as e: + error_msg = str(e) + # 检查是否是连接相关错误 + if any(phrase in error_msg.lower() for phrase in [ + "10054", "10061", "connection", "connect", "refused", "urllib3" + ]): + logging.debug(f"{device_str}连接相关异常: {error_msg[:100]}") + return False + else: + logging.debug(f"{device_str}检查会话时出现其他异常: {error_msg[:100]}") + return True # 对于真正的未知异常,保守返回True + + +def reconnect_driver(device_id, old_driver=None, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"): + """ + 重新连接Appium驱动,不重新启动应用 + + 参数: + device_id: 设备ID + old_driver: 旧的WebDriver实例(可选) + app_package: 应用包名 + app_activity: 应用启动Activity + + 返回: + (driver, wait): 新的WebDriver和WebDriverWait实例 + """ + # 使用传入的device_id或从全局变量获取 + if not device_id: + device_id = global_variable.GLOBAL_DEVICE_ID + + # 修复device_id参数类型问题并使用全局设备ID作为备用 + actual_device_id = device_id + + # 检查device_id是否为有效的字符串格式 + if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)): + # 尝试从old_driver获取设备ID + if old_driver and hasattr(old_driver, 'capabilities'): + capability_device_id = old_driver.capabilities.get('udid') + if capability_device_id: + actual_device_id = capability_device_id + logging.warning(f"检测到device_id参数无效,已从old_driver中提取设备ID: {actual_device_id}") + + # 如果仍然没有有效的设备ID,使用全局变量 + if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)): + actual_device_id = global_variable.GLOBAL_DEVICE_ID + logging.warning(f"无法获取有效设备ID,使用全局变量GLOBAL_DEVICE_ID: {actual_device_id}") + + device_id = actual_device_id # 使用修正后的设备ID + logging.info(f"设备 {device_id} 开始重新连接驱动(不重启应用)") + + # # 首先安全关闭旧驱动 + # if old_driver: + # safe_quit_driver(old_driver, device_id) + + max_reconnect_attempts = 3 + reconnect_delay = 5 # 秒 + + for attempt in range(max_reconnect_attempts): + try: + logging.info(f"设备 {device_id} 第{attempt + 1}次尝试重新连接") + + # 确保Appium服务器运行 + if not ensure_appium_server_running(): + logging.warning(f"设备 {device_id} Appium服务器未运行,尝试启动") + time.sleep(reconnect_delay) + continue + + # 创建并配置Appium选项 - 重点:设置 autoLaunch=False 不自动启动应用 + options = UiAutomator2Options() + options.platform_name = "Android" + options.device_name = device_id + options.app_package = app_package + options.app_activity = app_activity + options.automation_name = "UiAutomator2" + options.no_reset = True + options.auto_grant_permissions = True + options.new_command_timeout = 3600 + options.udid = device_id + + # 关键设置:不自动启动应用 + options.set_capability('autoLaunch', False) + options.set_capability('skipUnlock', True) + options.set_capability('skipServerInstallation', True) + options.set_capability('skipDeviceInitialization', True) + + # 增加uiautomator2服务器启动超时时间 + options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒 + # 增加连接超时设置 + options.set_capability('connection_timeout', 120000) # 120秒 + + # 连接Appium服务器 + logging.info(f"设备 {device_id} 正在连接Appium服务器(不启动应用)") + driver = webdriver.Remote("http://localhost:4723", options=options) + logging.info(f"设备 {device_id} Appium服务器连接成功") + + # 初始化等待对象 + wait = WebDriverWait(driver, 20) + logging.info(f"设备 {device_id} WebDriverWait初始化成功") + + # 不启动应用,直接附加到当前运行的应用 + try: + # 获取当前运行的应用 + current_package = driver.current_package + logging.info(f"设备 {device_id} 当前运行的应用: {current_package}") + + # 如果当前运行的不是目标应用,尝试切换到目标应用 + if current_package != app_package: + logging.info(f"设备 {device_id} 当前应用不是目标应用,尝试启动目标应用") + launch_app_manually(driver, app_package, app_activity) + else: + logging.info(f"设备 {device_id} 已成功连接到运行中的目标应用") + except Exception as attach_error: + logging.warning(f"设备 {device_id} 获取当前应用信息失败: {str(attach_error)}") + # 即使获取当前应用失败,也继续使用连接 + + # 验证新会话是否有效 + if check_session_valid(driver, device_id): + logging.info(f"设备 {device_id} 重新连接成功") + return driver, wait + else: + logging.warning(f"设备 {device_id} 新创建的会话无效,将重试") + safe_quit_driver(driver, device_id) + + except Exception as e: + logging.error(f"设备 {device_id} 第{attempt + 1}次重新连接失败: {str(e)}") + if attempt < max_reconnect_attempts - 1: + wait_time = reconnect_delay * (attempt + 1) + logging.info(f"设备 {device_id} 将在{wait_time}秒后重试重新连接") + time.sleep(wait_time) + else: + logging.error(f"设备 {device_id} 所有重新连接尝试均失败") + # 所有尝试都失败后才关闭旧驱动 + if old_driver: + safe_quit_driver(old_driver, device_id) + raise + + # 所有尝试都失败 + raise Exception(f"设备 {device_id} 重新连接失败,已尝试{max_reconnect_attempts}次") + + + +# def ensure_appium_server_running(port=4723): +# """使用完整的环境变量启动Appium,解决路径缺失问题""" +# try: +# # 获取当前用户的环境变量 +# env = os.environ.copy() + +# # ========== 仅修改这部分:Windows下Node.js/Appium常见路径 ========== +# additional_paths = [ +# # Windows默认Node.js安装路径(64位) +# os.path.join("C:\\", "Program Files\\nodejs"), +# # Windows32位Node.js路径 +# os.path.join("C:\\", "Program Files (x86)\\nodejs"), +# # npm全局安装路径(Windows核心,appium一般装在这里) +# os.path.expanduser("~\\AppData\\Roaming\\npm"), +# # 系统默认路径(防止基础命令缺失) +# os.path.join("C:\\", "Windows\\System32"), +# os.path.join("C:\\", "Windows\\System"), +# # 自定义Node.js/npm路径(可选,根据你的实际安装路径加) +# # "D:\\Program Files\\nodejs", # 若你装在D盘,解开注释并修改 +# ] +# # ========== Windows路径修改结束 ========== + +# # 更新PATH环境变量(跨系统通用,os.pathsep自动适配Windows的;和macOS的:) +# current_path = env.get('PATH', '') +# new_path = current_path + os.pathsep + os.pathsep.join(additional_paths) +# env['PATH'] = new_path + +# # 构建启动命令(和原函数一致,无修改) +# appium_cmd = f"appium -p {port} --log-level error" + +# # 使用完整环境启动(跨系统通用,Windows下正常执行) +# process = subprocess.Popen( +# appium_cmd, +# shell=True, # Windows下字符串命令必须开启,和macOS一致 +# env=env, # 传入补全后的Windows环境变量(核心) +# stdout=subprocess.PIPE, # 捕获输出,控制台不刷屏 +# stderr=subprocess.PIPE, +# text=True # 输出为字符串,无需手动解码(跨系统通用) +# ) + +# logging.info(f"Appium启动进程已创建,PID: {process.pid}") +# # 等待并校验服务启动成功(需确保wait_for_appium_start已定义,timeout有值) +# return wait_for_appium_start(port) + +# except Exception as e: +# logging.error(f"Windows下启动Appium时出错: {str(e)}") +# return False + +def is_port_in_use(port): + """检查端口是否被占用""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(('127.0.0.1', port)) == 0 + +def kill_system_process(process_name): + """杀掉系统进程""" + try: + # Windows + subprocess.run(f"taskkill /F /IM {process_name}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + except Exception: + pass + +def start_appium_server(port=4723): + """启动 Appium 服务,强制指定路径兼容性""" + # 1. 先尝试清理可能占用的 node 进程 + if is_port_in_use(port): + logging.warning(f"端口 {port} 被占用,尝试清理 node.exe...") + kill_system_process("node.exe") + time.sleep(2) + + # 2. 构造启动命令 + # 注意:这里增加了 --base-path /wd/hub 解决 404 问题 + # --allow-cors 允许跨域,有时候能解决连接问题 + appium_cmd = f"appium -p {port} --base-path /wd/hub --allow-cors" + + logging.info(f"正在启动 Appium: {appium_cmd}") + try: + # 使用 shell=True 在 Windows 上更稳定 + # creationflags=subprocess.CREATE_NEW_CONSOLE 可以让它在后台运行不弹出窗口 + subprocess.Popen(appium_cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logging.info("Appium 启动命令已发送,等待服务就绪...") + except Exception as e: + logging.error(f"启动 Appium 进程失败: {e}") + +def check_server_status(port): + """检测服务器状态,兼容 Appium 1.x 和 2.x 路径""" + base_url = f"http://127.0.0.1:{port}" + check_paths = ["/wd/hub/status", "/status"] # 优先检查 /wd/hub + + for path in check_paths: + try: + url = f"{base_url}{path}" + response = requests.get(url, timeout=1) + if response.status_code == 200: + return True + except: + pass + return False + +def ensure_appium_server_running(port=4723): + """确保 Appium 服务器正在运行,如果没运行则启动它""" + + # 1. 第一次快速检测 + if check_server_status(port): + logging.info(f"Appium 服务已在端口 {port} 运行") + return True + + # 2. 如果没运行,启动它 + logging.warning(f"Appium 未在端口 {port} 运行,准备启动...") + start_appium_server(port) + + # 3. 循环等待启动成功(最多等待 20 秒) + max_retries = 20 + for i in range(max_retries): + if check_server_status(port): + logging.info("Appium 服务启动成功并已就绪!") + return True + + time.sleep(1) + if i % 5 == 0: + logging.info(f"等待 Appium 启动中... ({i}/{max_retries})") + + logging.error("Appium 服务启动超时!请检查 appium 命令是否在命令行可直接运行。") + return False + +# ... (保留 init_appium_driver, safe_quit_driver 等其他函数) ... + +def wait_for_appium_start(port, timeout=10): + """ + 检测指定端口的Appium服务是否真正启动并可用 + :param port: Appium服务端口 + :param timeout: 最大等待时间(秒) + :return: 服务就绪返回True,超时/失败返回False + """ + # Appium官方状态查询接口 + check_url = f"http://localhost:{port}/wd/hub/status" + # 检测开始时间 + start_check_time = time.time() + logging.info(f"开始检测Appium服务是否就绪,端口:{port},最大等待{timeout}秒") + + while time.time() - start_check_time < timeout: + try: + # 发送HTTP请求,超时1秒(避免单次检测卡太久) + response = requests.get( + url=check_url, + timeout=1, + verify=False # 本地接口,禁用SSL验证 + ) + # 接口返回200(HTTP成功状态码),且JSON中status=0(Appium服务正常) + if response.status_code == 200 and response.json().get("status") == 0: + logging.info(f"Appium服务检测成功,端口{port}已就绪") + return True + except Exception as e: + # 捕获所有异常(连接拒绝、超时、JSON解析失败等),说明服务未就绪 + logging.debug(f"本次检测Appium服务未就绪:{str(e)}") # 调试日志,不刷屏 + + # 检测失败,休眠1秒后重试 + time.sleep(1) + + # 循环结束→超时 + logging.error(f"检测超时!{timeout}秒内Appium服务端口{port}仍未就绪") + return False + +def safe_quit_driver(driver, device_id=None): + """ + 安全关闭驱动的全局函数 + + 参数: + driver: WebDriver实例 + device_id: 设备ID(可选) + """ + if device_id is None: + device_id = global_variable.GLOBAL_DEVICE_ID + device_str = f"设备 {device_id} " if device_id else "" + logging.info(f"{device_str}开始关闭驱动") + + if not driver: + logging.info(f"{device_str}没有可关闭的驱动实例") + return + + # 检查driver是否为WebDriver实例或是否有quit方法 + if not hasattr(driver, 'quit'): + logging.warning(f"{device_str}驱动对象类型无效,不具有quit方法: {type(driver).__name__}") + return + + max_quit_attempts = 3 + for attempt in range(max_quit_attempts): + try: + logging.info(f"{device_str}尝试关闭驱动 (尝试 {attempt + 1}/{max_quit_attempts})") + driver.quit() + logging.info(f"{device_str}驱动已成功关闭") + return + except Exception as e: + logging.error(f"{device_str}关闭驱动时出错 (尝试 {attempt + 1}/{max_quit_attempts}): {str(e)}") + if attempt < max_quit_attempts - 1: + # 等待一段时间后重试 + wait_time = 2 + logging.info(f"{device_str}将在 {wait_time} 秒后重试") + time.sleep(wait_time) + else: + logging.critical(f"{device_str}尝试多次关闭驱动失败,可能导致资源泄漏") + + +def is_app_launched(driver, package_name="com.bjjw.cjgc"): + """ + 检查应用是否已启动 + + 参数: + driver: WebDriver实例 + package_name: 应用包名,默认为"com.bjjw.cjgc" + + 返回: + bool: 如果应用已启动则返回True,否则返回False + """ + try: + # 通过检查当前活动的包名来确认应用是否已启动 + current_package = driver.current_package + return current_package == package_name + except Exception as e: + logging.error(f"检查应用启动状态时出错: {str(e)}") + return False + + +def launch_app_manually(driver, device_id, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"): + """ + 手动启动应用 + + 参数: + driver: WebDriver实例 + package_name: 应用包名,默认为"com.bjjw.cjgc" + activity: 应用启动Activity,默认为".activity.LoginActivity" + """ + try: + if not device_id: + device_id = global_variable.GLOBAL_DEVICE_ID + # 尝试从driver获取设备ID + if driver and hasattr(driver, 'capabilities'): + device_id = driver.capabilities.get('udid') + device_str = f"设备 {device_id} " if device_id else "" + else: + device_str = "" + + logging.info(f"{device_str}尝试手动启动应用: {package_name}/{activity}") + + # 首先使用ADB命令退出应用 + if device_id: + try: + # 使用ADB命令强制停止应用 + stop_cmd = [ + "adb", "-s", device_id, + "shell", "am", "force-stop", + package_name + ] + stop_result = subprocess.run(stop_cmd, capture_output=True, text=True, timeout=15) + if stop_result.returncode == 0: + logging.info(f"{device_str}已使用ADB命令成功退出应用") + else: + logging.warning(f"{device_str}ADB退出应用失败: {stop_result.stderr}") + except Exception as stop_error: + logging.warning(f"{device_str}退出应用时出错: {str(stop_error)}") + + # 首先尝试使用driver的execute_script方法启动应用 + try: + if driver: + driver.execute_script("mobile: startActivity", { + "intent": f"{package_name}/{activity}" + }) + logging.info(f"{device_str}已使用Appium startActivity命令启动应用") + except Exception as inner_e: + logging.warning(f"{device_str}使用Appium startActivity命令失败: {str(inner_e)},尝试使用ADB命令") + + # 如果device_id可用,使用ADB命令启动应用 + if device_id: + cmd = [ + "adb", "-s", device_id, + "shell", "am", "start", + "-n", f"{package_name}/{activity}" + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + + if result.returncode == 0: + logging.info(f"{device_str}已使用ADB命令成功启动应用") + else: + logging.error(f"{device_str}ADB启动应用失败: {result.stderr}") + else: + logging.warning("无法获取设备ID,无法使用ADB命令启动应用") + + # 设置屏幕永不休眠 + if device_id: + try: + # 使用ADB命令设置屏幕永不休眠 + screen_timeout_cmd = [ + "adb", "-s", device_id, + "shell", "settings", "put", "system", "screen_off_timeout", "0" + ] + timeout_result = subprocess.run(screen_timeout_cmd, capture_output=True, text=True, timeout=15) + if timeout_result.returncode == 0: + logging.info(f"{device_str}已成功设置屏幕永不休眠") + else: + logging.warning(f"{device_str}设置屏幕永不休眠失败: {timeout_result.stderr}") + except Exception as timeout_error: + logging.warning(f"{device_str}设置屏幕永不休眠时出错: {str(timeout_error)}") + + # 等待应用启动 + time.sleep(5) + except Exception as e: + logging.error(f"手动启动应用时出错: {str(e)}") + logging.error(f"错误堆栈: {traceback.format_exc()}") + +def go_main_click_tabber_button(driver, device_id, tabber_button_text, max_retries=3): + """ + 跳转到主页面并点击对应的导航菜单按钮(带重试机制) + + 参数: + driver: WebDriver实例 + device_id: 设备ID + tabber_button_text: 导航菜单按钮的文本 + max_retries: 最大重试次数 + + 返回: + bool: 成功返回True,失败返回False + """ + retry_count = 0 + + while retry_count < max_retries: + try: + logging.info(f"设备 {device_id} 第 {retry_count + 1} 次尝试执行导航操作") + + if not check_session_valid(driver, device_id): + logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") + try: + # 重新连接,获取新的driver + new_driver, _ = reconnect_driver(device_id, driver) + driver = new_driver # 更新driver引用 + logging.info(f"设备 {device_id} 驱动重连成功") + except Exception as e: + logging.error(f"设备 {device_id} 驱动重连失败: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + time.sleep(2) + continue + else: + return False + + # 检查当前是否已经在主页面 + current_activity = driver.current_activity + logging.info(f"设备 {device_id} 当前Activity: {current_activity}") + + if ".activity.MainActivity" in current_activity: + logging.info(f"设备 {device_id} 已在主页面") + else: + logging.info(f"设备 {device_id} 当前不在主页面,循环点击返回按钮,直到在主页面") + + max_back_presses = 10 # 最大返回键次数 + back_press_count = 0 + + while ".activity.MainActivity" not in current_activity and back_press_count < max_back_presses: + try: + if not check_session_valid(driver, device_id): + logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(device_id, driver): + logging.error(f"设备 {device_id} 驱动重连失败") + # 点击返回按钮 + driver.back() + back_press_count += 1 + time.sleep(1) + + # 更新当前Activity + current_activity = driver.current_activity + logging.info(f"设备 {device_id} 点击返回按钮 {back_press_count} 次后,当前Activity: {current_activity}") + + except Exception as inner_e: + logging.warning(f"设备 {device_id} 点击返回按钮时出错: {str(inner_e)}") + break + + # 检查是否成功回到主页面 + if ".activity.MainActivity" not in current_activity: + logging.warning(f"设备 {device_id} 无法回到主页面,当前Activity: {current_activity}") + # 不立即返回,继续重试逻辑 + retry_count += 1 + if retry_count < max_retries: + logging.info(f"设备 {device_id} 等待2秒后重试...") + time.sleep(2) + continue + else: + logging.error(f"设备 {device_id} 达到最大重试次数,无法回到主页面") + return False + + # 现在已经在主页面,点击指定的导航菜单按钮 + # logging.info(f"设备 {device_id} 已在主页面,尝试点击导航菜单按钮: {tabber_button_text}") + + try: + tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text) + # 点击按钮 + tabber_button.click() + logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}") + + # 等待页面加载 + time.sleep(2) + + # 验证操作是否成功 + # 可以添加一些验证逻辑,比如检查是否跳转到目标页面 + new_activity = driver.current_activity + logging.info(f"设备 {device_id} 点击后当前Activity: {new_activity}") + + return True + + except TimeoutException: + logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时") + except Exception as e: + logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}") + + # 检查会话有效性并尝试重连 + if not check_session_valid(driver, device_id): + logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...") + if reconnect_driver(device_id, driver): + logging.info(f"设备 {device_id} 驱动重连成功,继续重试") + # 重连成功后继续循环 + retry_count += 1 + if retry_count < max_retries: + time.sleep(2) + continue + else: + logging.error(f"设备 {device_id} 驱动重连失败") + return False + else: + # 会话有效但点击失败,可能是页面元素问题 + logging.warning(f"设备 {device_id} 会话有效但点击失败,可能是页面加载问题") + + # 如果点击按钮失败,增加重试计数 + retry_count += 1 + if retry_count < max_retries: + logging.info(f"设备 {device_id} 点击按钮失败,等待2秒后第 {retry_count + 1} 次重试...") + time.sleep(2) + else: + logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败") + return False + + except Exception as e: + logging.error(f"设备 {device_id} 第 {retry_count + 1} 次尝试时出错: {str(e)}") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"设备 {device_id} 等待2秒后重试...") + time.sleep(2) + else: + logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败") + return False + + return False + +def check_connection_error(exception): + """检查是否为连接拒绝错误""" + error_str = str(exception) + connection_errors = [ + '远程主机强迫关闭了一个现有的连接', + '由于目标计算机积极拒绝,无法连接', + 'ConnectionResetError', + 'NewConnectionError', + '10054', + '10061' + ] + return any(error in error_str for error in connection_errors) + +def restart_appium_server(port=4723): + """重启Appium服务器""" + try: + # 杀死可能存在的Appium进程 + subprocess.run(['taskkill', '/f', '/im', 'node.exe'], + capture_output=True, shell=True) + time.sleep(2) + + # 启动Appium服务器 + # appium_command = f'appium -p {port}' + appium_command = f"appium -p {port} --base-path /wd/hub" + subprocess.Popen(appium_command, shell=True) + + # 等待Appium启动 + time.sleep(10) + return True + except Exception as e: + print(f"重启Appium服务器失败: {str(e)}") + return False + +def is_appium_running(port=4723): + """检查Appium服务器是否在运行""" + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + result = s.connect_ex(('127.0.0.1', port)) + return result == 0 + except: + return False diff --git a/globals/ex_apis.py b/globals/ex_apis.py new file mode 100644 index 0000000..ff5632b --- /dev/null +++ b/globals/ex_apis.py @@ -0,0 +1,298 @@ +# external_apis.py +import requests +import json +import logging +import random +from typing import Dict, Tuple, Optional +import time + +class WeatherAPI: + def __init__(self): + self.logger = logging.getLogger(__name__) + # 使用腾讯天气API + self.base_url = "https://wis.qq.com/weather/common" + + def parse_city(self, city_string: str) -> Tuple[str, str, str]: + """ + 解析城市字符串,返回省份、城市、区县 + + 参数: + city_string: 完整的地址字符串 + + 返回: + (province, city, county) + """ + # 匹配省份或自治区 + province_regex = r"(.*?)(省|自治区)" + # 匹配城市或州 + city_regex = r"(.*?省|.*?自治区)(.*?市|.*?州)" + # 匹配区、县或镇 + county_regex = r"(.*?市|.*?州)(.*?)(区|县|镇)" + + province = "" + city = "" + county = "" + + import re + + # 先尝试匹配省份或自治区 + province_match = re.search(province_regex, city_string) + if province_match: + province = province_match.group(1).strip() + + # 然后尝试匹配城市或州 + city_match = re.search(city_regex, city_string) + if city_match: + city = city_match.group(2).strip() + else: + # 如果没有匹配到城市,则可能是直辖市或者直接是区/县 + city = city_string + + # 最后尝试匹配区、县或镇 + county_match = re.search(county_regex, city_string) + if county_match: + county = county_match.group(2).strip() + # 如果有区、县或镇,那么前面的城市部分需要重新解析 + if city_match: + city = city_match.group(2).strip() + + # 特殊情况处理,去除重复的省市名称 + if city and province and city.startswith(province): + city = city.replace(province, "").strip() + if county and city and county.startswith(city): + county = county.replace(city, "").strip() + + # 去除后缀 + city = city.rstrip('市州') + if county: + county = county.rstrip('区县镇') + + # self.logger.info(f"解析结果 - 省份: {province}, 城市: {city}, 区县: {county}") + return province, city, county + + def get_weather_by_qq_api(self, province: str, city: str, county: str) -> Optional[Dict]: + """ + 使用腾讯天气API获取天气信息 + + 参数: + province: 省份 + city: 城市 + county: 区县 + + 返回: + 天气信息字典 or None + """ + try: + params = { + 'source': 'pc', + 'weather_type': 'observe', + 'province': province, + 'city': city, + 'county': county + } + + response = requests.get(self.base_url, params=params, timeout=10) + response.raise_for_status() + + data = response.json() + + if data.get('status') == 200: + observe_data = data.get('data', {}).get('observe', {}) + + return { + 'weather': observe_data.get('weather', ''), + 'temperature': observe_data.get('degree', ''), + 'pressure': observe_data.get('pressure', '1013') + } + else: + self.logger.error(f"腾讯天气API错误: {data.get('message')}") + return None + + except Exception as e: + self.logger.error(f"腾讯天气API调用失败: {str(e)}") + return None + + def normalize_weather_text(self, weather_text: str) -> str: + """ + 将天气描述标准化为: 晴;阴;雨;雪;风;其他 + + 参数: + weather_text: 原始天气描述 + + 返回: + 标准化后的天气文本 + """ + if not weather_text: + return '其他' + + weather_text_lower = weather_text.lower() + + # 晴 + if any(word in weather_text_lower for word in ['晴', 'sunny', 'clear']): + return '晴' + # 阴 + elif any(word in weather_text_lower for word in ['阴', '多云', 'cloudy', 'overcast']): + return '阴' + # 雨 + elif any(word in weather_text_lower for word in ['雨', 'rain', 'drizzle', 'shower']): + return '雨' + # 雪 + elif any(word in weather_text_lower for word in ['雪', 'snow']): + return '雪' + # 风 + elif any(word in weather_text_lower for word in ['风', 'wind']): + return '风' + # 其他 + else: + return '其他' + + def adjust_pressure(self, pressure: float) -> float: + """ + 调整气压值:低于700时,填700-750之间随机一个;高于700就按实际情况填 + + 参数: + pressure: 原始气压值 + + 返回: + 调整后的气压值 + """ + try: + pressure_float = float(pressure) + if pressure_float < 700: + adjusted_pressure = random.randint(700, 750) + self.logger.info(f"气压值 {pressure_float} 低于700,调整为: {adjusted_pressure:.1f}") + return round(adjusted_pressure, 1) + else: + self.logger.info(f"使用实际气压值: {pressure_float}") + return round(pressure_float, 1) + except (ValueError, TypeError): + self.logger.warning(f"气压值格式错误: {pressure},使用默认值720") + return round(random.randint(700, 750), 1) + + def get_weather_by_address(self, address: str, max_retries: int = 2) -> Optional[Dict]: + """ + 根据地址获取天气信息 + + 参数: + address: 地址字符串 + max_retries: 最大重试次数 + + 返回: + { + 'weather': '晴/阴/雨/雪/风/其他', + 'temperature': 温度值, + 'pressure': 气压值 + } or None + """ + # self.logger.info(f"开始获取地址 '{address}' 的天气信息") + + # 首先解析地址 + province, city, county = self.parse_city(address) + + if not province and not city: + self.logger.error("无法解析地址") + return self.get_fallback_weather() + + # 获取天气信息 + weather_data = None + for attempt in range(max_retries): + try: + # self.logger.info(f"尝试获取天气信息 (第{attempt + 1}次)") + weather_data = self.get_weather_by_qq_api(province, city, county) + if weather_data: + break + time.sleep(1) # 短暂延迟后重试 + except Exception as e: + self.logger.warning(f"第{attempt + 1}次尝试失败: {str(e)}") + time.sleep(1) + + if not weather_data: + self.logger.warning("获取天气信息失败,使用备用数据") + return self.get_fallback_weather() + + # 处理天气数据 + try: + # 标准化天气文本 + normalized_weather = self.normalize_weather_text(weather_data['weather']) + + # 调整气压值 + adjusted_pressure = self.adjust_pressure(weather_data['pressure']) + + # 处理温度 + temperature = float(weather_data['temperature']) + + result = { + 'weather': normalized_weather, + 'temperature': round(temperature, 1), + 'pressure': adjusted_pressure + } + + self.logger.info(f"成功获取天气信息: {result}") + return result + + except Exception as e: + self.logger.error(f"处理天气数据时出错: {str(e)}") + return self.get_fallback_weather() + + def get_fallback_weather(self) -> Dict: + """ + 获取备用天气数据(当所有API都失败时使用) + + 返回: + 默认天气数据 + """ + self.logger.info("使用备用天气数据") + return { + 'weather': '阴', + 'temperature': round(random.randint(15, 30), 1), + 'pressure': round(random.randint(700, 750), 1) + } + + def get_weather_simple(self, address: str) -> Tuple[str, float, float]: + """ + 简化接口:直接返回天气、温度、气压 + + 参数: + address: 地址字符串 + + 返回: + (weather, temperature, pressure) + """ + weather_data = self.get_weather_by_address(address) + if weather_data: + return weather_data['weather'], weather_data['temperature'], weather_data['pressure'] + else: + fallback = self.get_fallback_weather() + return fallback['weather'], fallback['temperature'], fallback['pressure'] + +# 创建全局实例 +weather_api = WeatherAPI() + +# 直接可用的函数 +def get_weather_by_address(address: str) -> Optional[Dict]: + """ + 根据地址获取天气信息(直接调用函数) + + 参数: + address: 地址字符串 + + 返回: + { + 'weather': '晴/阴/雨/雪/风/其他', + 'temperature': 温度值, + 'pressure': 气压值 + } or None + """ + return weather_api.get_weather_by_address(address) + +def get_weather_simple(address: str) -> Tuple[str, float, float]: + """ + 简化接口:直接返回天气、温度、气压 + + 参数: + address: 地址字符串 + + 返回: + (weather, temperature, pressure) + """ + return weather_api.get_weather_simple(address) diff --git a/globals/global_variable.py b/globals/global_variable.py new file mode 100644 index 0000000..86549ce --- /dev/null +++ b/globals/global_variable.py @@ -0,0 +1,17 @@ +# 全局变量 +GLOBAL_DEVICE_ID = "" # 设备ID +GLOBAL_USERNAME = "czyuzongwen" # 用户名 +GLOBAL_CURRENT_PROJECT_NAME = "" # 当前测试项目名称 +GLOBAL_LINE_NUM = "" # 线路编码 +GLOBAL_BREAKPOINT_STATUS_CODES = [0,3] # 要获取的断点状态码列表 +GLOBAL_UPLOAD_BREAKPOINT_LIST = [] +GLOBAL_UPLOAD_BREAKPOINT_DICT = {} +GLOBAL_TESTED_BREAKPOINT_LIST = [] # 测量结束的断点列表 +LINE_TIME_MAPPING_DICT = {} # 存储所有线路编码和对应的时间的全局字典 +GLOBAL_BREAKPOINT_DICT = {} # 存储测量结束的断点名称和对应的线路编码的全局字典 +GLOBAL_NAME_TO_ID_MAP = {} # 存储所有数据员姓名和对应的身份证号的全局字典 +GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST = [] # 上传成功的`断点列表 + + + + diff --git a/globals/ids.py b/globals/ids.py new file mode 100644 index 0000000..33cbb60 --- /dev/null +++ b/globals/ids.py @@ -0,0 +1,59 @@ +# ids.py +# 登录界面 +LOGIN_USERNAME = "com.bjjw.cjgc:id/et_user_name" +LOGIN_PASSWORD = "com.bjjw.cjgc:id/et_user_psw" +LOGIN_BTN = "com.bjjw.cjgc:id/btn_login" + +# 更新相关 +UPDATE_WORK_BASE = "com.bjjw.cjgc:id/btn_update_basepoint" +UPDATE_LEVEL_LINE = "com.bjjw.cjgc:id/btn_update_line" +UPDATE_LEVEL_LINE_CONFIRM = "com.bjjw.cjgc:id/commit" + +# 弹窗 & 加载 +ALERT_DIALOG = "android:id/content" +LOADING_DIALOG = "android:id/custom" + +# 底部导航栏 +DOWNLOAD_TABBAR_ID = "com.bjjw.cjgc:id/img_1_layout" +MEASURE_TABBAR_ID = "com.bjjw.cjgc:id/img_3_layout" + +# 测量相关 +MEASURE_BTN_ID = "com.bjjw.cjgc:id/select_point_update_tip_tv" +MEASURE_LIST_ID = "com.bjjw.cjgc:id/line_list" +MEASURE_LISTVIEW_ID = "com.bjjw.cjgc:id/itemContainer" +MEASURE_NAME_TEXT_ID = "com.bjjw.cjgc:id/title" +MEASURE_NAME_ID = "com.bjjw.cjgc:id/sectName" +MEASURE_BACK_ID = "com.bjjw.cjgc:id/btn_back" +MEASURE_BACK_ID_2 = "com.bjjw.cjgc:id/stop_measure_btn" + +# 天气、观测类型 +MEASURE_TITLE_ID = "com.bjjw.cjgc:id/title_bar" +MEASURE_WEATHER_ID = "com.bjjw.cjgc:id/point_list_weather_sp" +MEASURE_TYPE_ID = "com.bjjw.cjgc:id/point_list_mtype_sp" +MEASURE_SELECT_ID = "android:id/select_dialog_listview" +SELECT_DIALOG_TEXT1_ID = "android:id/text1" +MEASURE_PRESSURE_ID = "com.bjjw.cjgc:id/point_list_barometric_et" +MEASURE_TEMPERATURE_ID = "com.bjjw.cjgc:id/point_list_temperature_et" +MEASURE_SAVE_ID = "com.bjjw.cjgc:id/select_point_order_save_btn" + +# 日期选择器 +DATE_START = "com.bjjw.cjgc:id/date" +DATE_END = "com.bjjw.cjgc:id/date_end" +SCRCOLL_YEAR = "com.bjjw.cjgc:id/wheelView1" +SCRCOLL_MONTH = "com.bjjw.cjgc:id/wheelView2" +SCRCOLL_DAY = "com.bjjw.cjgc:id/wheelView3" +SCRCOLL_CONFIRM = "com.bjjw.cjgc:id/okBtn" +SCRCOLL_CANCEL = "com.bjjw.cjgc:id/cancelBtn" + +# 其他 +CONNECT_LEVEL_METER = "com.bjjw.cjgc:id/point_conn_level_btn" +PINGCHAR_PROCESS = "com.bjjw.cjgc:id/point_measure_btn" +LEVEL_METER_MANAGER = "com.bjjw.cjgc:id/btn_back" +LEVEL_METER_MANAGER_LIST = "com.bjjw.cjgc:id/total_station_expandlist" +START_MEASURE = "com.bjjw.cjgc:id/btn_control_begin_or_end" +ALTER_MEASURE = "com.bjjw.cjgc:id/order_title" +MATCH_VISIABLE = "com.bjjw.cjgc:id/title_paired_devices" +BLUETOOTH_PAIR = "com.bjjw.cjgc:id/paired_devices" +REPID_MEASURE = "com.bjjw.cjgc:id/measure_remeasure_all_btn" +ONE = "com.bjjw.cjgc:id/auto_measure_all_station_text" + diff --git a/main.py b/main.py new file mode 100644 index 0000000..803a05b --- /dev/null +++ b/main.py @@ -0,0 +1,478 @@ +# actions.py 主自动化脚本 +import os +import logging +import time +import subprocess +from appium import webdriver +from appium.options.android import UiAutomator2Options +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException + +import globals.ids as ids +import globals.global_variable as global_variable # 导入全局变量模块 +import permissions # 导入权限处理模块 +import globals.apis as apis +from globals.driver_utils import init_appium_driver, ensure_appium_server_running, safe_quit_driver, is_app_launched, launch_app_manually +from page_objects.login_page import LoginPage +from page_objects.download_tabbar_page import DownloadTabbarPage +from page_objects.screenshot_page import ScreenshotPage +from page_objects.upload_config_page import UploadConfigPage +from page_objects.more_download_page import MoreDownloadPage + + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(levelname)s: %(message)s", + handlers=[ + logging.FileHandler("appium_automation.log"), + logging.StreamHandler() + ] +) + +class DeviceAutomation: + @staticmethod + def get_device_id() -> str: + + # """ + # 获取设备ID,优先使用已连接设备,否则使用全局配置 + # """ + # try: + # # 检查已连接设备 + # result = subprocess.run( + # ["adb", "devices"], + # capture_output=True, + # text=True, + # timeout=10 + # ) + + # # 解析设备列表 + # for line in result.stdout.strip().split('\n')[1:]: + # if line.strip() and "device" in line and "offline" not in line: + # device_id = line.split('\t')[0] + # logging.info(f"使用已连接设备: {device_id}") + # global_variable.GLOBAL_DEVICE_ID = device_id + # return device_id + + # except Exception as e: + # logging.warning(f"设备检测失败: {e}") + + """ + 获取设备ID,优先使用无线连接设备,否则尝试开启无线调试,最后使用全局配置 + """ + try: + # 检查已连接设备 + result = subprocess.run( + ["adb", "devices"], + capture_output=True, + text=True, + timeout=10 + ) + + # 解析设备列表 + wireless_device_id = None + usb_device_id = None + + # 先查找无线连接的设备(IP:端口格式) + for line in result.stdout.strip().split('\n')[1:]: + if line.strip() and "device" in line and "offline" not in line: + current_device = line.split('\t')[0] + # 检查是否为IP:端口格式的无线连接 + if ":" in current_device and any(char.isdigit() for char in current_device): + wireless_device_id = current_device + logging.info(f"使用无线连接设备: {wireless_device_id}") + global_variable.GLOBAL_DEVICE_ID = wireless_device_id + return wireless_device_id + else: + # 记录第一个USB连接的设备 + if not usb_device_id: + usb_device_id = current_device + + # 如果没有找到无线连接的设备,尝试使用USB设备开启无线调试 + if not wireless_device_id and usb_device_id: + logging.info(f"未找到无线连接设备,尝试使用USB设备 {usb_device_id} 开启无线调试") + + # 尝试获取设备IP地址 + try: + import re + import time + + ip_result = subprocess.run( + ["adb", "-s", usb_device_id, "shell", "ip", "-f", "inet", "addr", "show", "wlan0"], + capture_output=True, + text=True, + timeout=10 + ) + + # 解析IP地址 + ip_output = ip_result.stdout + if "inet " in ip_output: + # 提取IP地址 + ip_match = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', ip_output) + if ip_match: + device_ip = ip_match.group(1) + logging.info(f"获取到设备IP地址: {device_ip}") + + # 开启无线调试 + tcpip_result = subprocess.run( + ["adb", "-s", usb_device_id, "tcpip", "5555"], + capture_output=True, + text=True, + timeout=10 + ) + + if "restarting in TCP mode port: 5555" in tcpip_result.stdout: + logging.info("无线调试已开启,端口: 5555") + + # 等待几秒钟让设备准备好 + time.sleep(3) + + # 连接到无线设备 + connect_result = subprocess.run( + ["adb", "connect", f"{device_ip}:5555"], + capture_output=True, + text=True, + timeout=10 + ) + + if "connected to" in connect_result.stdout: + logging.info(f"成功连接到无线设备: {device_ip}:5555") + global_variable.GLOBAL_DEVICE_ID = f"{device_ip}:5555" + return f"{device_ip}:5555" + else: + logging.warning(f"连接无线设备失败: {connect_result.stderr}") + logging.info(f"使用USB设备: {usb_device_id}") + global_variable.GLOBAL_DEVICE_ID = usb_device_id + return usb_device_id + else: + logging.warning(f"开启无线调试失败: {tcpip_result.stderr}") + logging.info(f"使用USB设备: {usb_device_id}") + global_variable.GLOBAL_DEVICE_ID = usb_device_id + return usb_device_id + else: + logging.warning("未找到设备IP地址") + logging.info(f"使用USB设备: {usb_device_id}") + global_variable.GLOBAL_DEVICE_ID = usb_device_id + return usb_device_id + else: + logging.warning("无法获取设备IP地址,可能设备未连接到WiFi") + logging.info(f"使用USB设备: {usb_device_id}") + global_variable.GLOBAL_DEVICE_ID = usb_device_id + return usb_device_id + except Exception as e: + logging.warning(f"开启无线调试时出错: {str(e)}") + logging.info(f"使用USB设备: {usb_device_id}") + global_variable.GLOBAL_DEVICE_ID = usb_device_id + return usb_device_id + + except Exception as e: + logging.warning(f"设备检测失败: {e}") + + + # 使用全局配置 + device_id = global_variable.GLOBAL_DEVICE_ID + logging.info(f"使用全局配置设备: {device_id}") + return device_id + + def __init__(self, device_id=None): + # 如果没有提供设备ID,则自动获取 + if device_id is None: + self.device_id = self.get_device_id() + else: + self.device_id = device_id + + # 初始化权限 + if permissions.grant_appium_permissions(self.device_id): + logging.info(f"设备 {self.device_id} 权限授予成功") + else: + logging.warning(f"设备 {self.device_id} 权限授予失败") + + # 确保Appium服务器正在运行 + ensure_appium_server_running(4723) + + # 初始化驱动 + self.init_driver() + # 先拼接,后创建测试结果目录 + self.results_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_results') + os.makedirs(self.results_dir, exist_ok=True) + + + def init_driver(self): + """初始化Appium驱动""" + try: + # 使用全局函数初始化驱动 + self.driver, self.wait = init_appium_driver(self.device_id) + # 初始化页面对象 + logging.info(f"设备 {self.device_id} 开始初始化页面对象") + self.login_page = LoginPage(self.driver, self.wait) + self.download_tabbar_page = DownloadTabbarPage(self.driver, self.wait, self.device_id) + self.screenshot_page = ScreenshotPage(self.driver, self.wait, self.device_id) + self.upload_config_page = UploadConfigPage(self.driver, self.wait, self.device_id) + self.more_download_page = MoreDownloadPage(self.driver, self.wait,self.device_id) + logging.info(f"设备 {self.device_id} 所有页面对象初始化完成") + + # 检查应用是否成功启动 + if is_app_launched(self.driver): + logging.info(f"设备 {self.device_id} 沉降观测App已成功启动") + else: + logging.warning(f"设备 {self.device_id} 应用可能未正确启动") + # 手动启动应用 + launch_app_manually(self.driver, self.device_id) + + except Exception as e: + logging.error(f"设备 {self.device_id} 初始化驱动失败: {str(e)}") + raise + + def is_app_launched(self): + """检查应用是否已启动""" + try: + return is_app_launched(self.driver) + except Exception as e: + logging.error(f"设备 {self.device_id} 检查应用启动状态时出错: {str(e)}") + return False + + def is_element_present(self, by, value): + """检查元素是否存在""" + try: + self.driver.find_element(by, value) + return True + except NoSuchElementException: + return False + + def handle_app_state(self): + """根据当前应用状态处理相应的操作""" + try: + login_btn_exists = self.login_page.is_login_page() + if not login_btn_exists: + logging.error(f"设备 {self.device_id} 未知应用状态,无法确定当前页面,跳转到登录页面") + if self.navigate_to_login_page(self.driver, self.device_id): + logging.info(f"设备 {self.device_id} 成功跳转到登录页面") + return self.handle_app_state() # 递归调用处理登录后的状态 + else: + logging.error(f"设备 {self.device_id} 跳转到登录页面失败") + return False + + # 处理登录页面状态 + logging.info(f"设备 {self.device_id} 检测到登录页面,执行登录操作") + max_retries = 1 + login_success = False + + for attempt in range(max_retries + 1): + if self.login_page.login(): + login_success = True + break + else: + if attempt < max_retries: + logging.warning(f"设备 {self.device_id} 登录失败,准备重试 ({attempt + 1}/{max_retries})") + time.sleep(2) # 等待2秒后重试 + else: + logging.error(f"设备 {self.device_id} 登录失败,已达到最大重试次数") + + if not login_success: + return False + + logging.info(f"设备 {self.device_id} 登录成功,继续执行更新操作") + time.sleep(1) + + # 执行更新操作 + if not self.download_tabbar_page.download_tabbar_page_manager(): + logging.error(f"设备 {self.device_id} 更新操作执行失败") + return False + + # 获取状态为3的线路。 + apis.get_line_info_and_save_global(user_name=global_variable.GLOBAL_USERNAME); + # # 虚拟数据替代 + # global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT = {'CDWZQ-2标-龙骨湾右线大桥-0-7号墩-平原': 'L156372', 'CDWZQ-2标-蓝家湾特大 桥-31-31-平原': 'L159206'} + # global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST = list(global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT.keys()) + + # 点击测量导航栏按钮 + measure_page_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_3_layout")) + ) + measure_page_btn.click() + + + # 处理平差 + self.screenshot_page.screenshot_page_manager(self.device_id) + + + # 检查是否有需要上传的断点 + if not global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST: + logging.info(f"设备 {self.device_id} 断点列表为空,无需执行上传操作") + return False + + # 点击上传导航栏按钮 + upload_page_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout")) + ) + upload_page_btn.click() + + + # 遍历断点列表,逐个执行上传操作 + upload_success_count = 0 + for breakpoint_name in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST: + try: + logging.info(f"设备 {self.device_id} 开始处理断点 '{breakpoint_name}' 的上传") + + # 安全地获取断点信息 + line_num = global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT.get(breakpoint_name) + if line_num is None: + logging.warning(f"设备 {self.device_id} 断点 '{breakpoint_name}' 在字典中未找到,跳过上传") + continue + if not line_num: + logging.warning(f"设备 {self.device_id} 断点 '{breakpoint_name}' 未获取到line_num,跳过上传") + continue + + # 执行上传配置管理,传入当前断点名称 + if self.upload_config_page.upload_config_page_manager(self.results_dir, breakpoint_name, line_num): + logging.info(f"设备 {self.device_id} 断点 '{breakpoint_name}' 上传成功") + upload_success_count += 1 + else: + logging.error(f"设备 {self.device_id} 断点 '{breakpoint_name}' 上传失败") + + except Exception as e: + logging.error(f"设备 {self.device_id} 处理断点 '{breakpoint_name}' 时发生异常: {str(e)}") + + logging.info(f"设备 {self.device_id} 上传配置管理执行完成,成功上传 {upload_success_count}/{len(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)} 个断点") + + # 如果所有断点都上传成功,返回True;否则返回False + all_upload_success = upload_success_count == len(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST) + if all_upload_success: + logging.info(f"设备 {self.device_id} 所有断点上传成功") + # 把上传成功的断点写入日志文件"上传成功的断点.txt" + with open(os.path.join(self.results_dir, "上传成功的断点.txt"), "w", encoding='utf-8') as f: + for bp in global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST: + f.write(f"{bp}\n") + else: + logging.warning(f"设备 {self.device_id} 部分断点上传失败") + # 把上传成功的断点写入日志文件"上传成功的断点.txt" + with open(os.path.join(self.results_dir, "上传成功的断点.txt"), "w", encoding='utf-8') as f: + for bp in global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST: + f.write(f"{bp}\n") + # 把上传失败的断点写入日志文件"上传失败的断点.txt" + with open(os.path.join(self.results_dir, "上传失败的断点.txt"), "w", encoding='utf-8') as f: + for bp in set(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)-set(global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST): + f.write(f"{bp}\n") + + return all_upload_success + + except Exception as e: + logging.error(f"设备 {self.device_id} 处理应用状态时出错: {str(e)}") + return False + + + + def check_and_click_confirm_popup_appium(self): + """ + 适用于Appium的弹窗检测函数 + + Returns: + bool: 是否成功处理弹窗 + """ + try: + from appium.webdriver.common.appiumby import AppiumBy + + # 使用self.driver而不是参数 + if not hasattr(self, 'driver') or self.driver is None: + logging.warning("driver未初始化,无法检测弹窗") + return False + + # 检查弹窗消息 + message_elements = self.driver.find_elements(AppiumBy.XPATH, '//android.widget.TextView[@text="是否退出测量界面?"]') + + if message_elements: + logging.info("检测到退出测量界面弹窗") + + # 点击"是"按钮 + confirm_buttons = self.driver.find_elements(AppiumBy.XPATH, '//android.widget.Button[@text="是" and @resource-id="android:id/button1"]') + if confirm_buttons: + confirm_buttons[0].click() + logging.info("已点击'是'按钮") + time.sleep(1) + return True + else: + logging.warning("未找到'是'按钮") + return False + else: + return False + + except Exception as e: + logging.error(f"Appium检测弹窗时发生错误: {str(e)}") + return False + + + + def navigate_to_login_page(self, driver, device_id): + """ + 补充的跳转页面函数:当设备处于未知状态时,尝试跳转到登录页面 + + 参数: + driver: 已初始化的Appium WebDriver对象 + device_id: 设备ID,用于日志记录 + """ + try: + target_package = 'com.bjjw.cjgc' + target_activity = '.activity.LoginActivity' + # 使用ADB命令启动Activity + try: + logging.info(f"尝试使用ADB命令启动LoginActivity: {target_package}/{target_activity}") + adb_command = f"adb -s {device_id} shell am start -n {target_package}/{target_activity}" + result = subprocess.run(adb_command, shell=True, capture_output=True, text=True) + if result.returncode == 0: + logging.info(f"使用ADB命令启动LoginActivity成功") + time.sleep(2) # 等待Activity启动 + return True + else: + logging.warning(f"ADB命令执行失败: {result.stderr}") + except Exception as adb_error: + logging.warning(f"执行ADB命令时出错: {adb_error}") + except Exception as e: + logging.error(f"跳转到登录页面过程中发生未预期错误: {e}") + + # 所有尝试都失败 + return False + + + + + def run_automation(self): + """运行自动化流程""" + try: + success = self.handle_app_state() + # success = self.test_handle_app_state() + if success: + logging.info(f"设备 {self.device_id} 自动化流程执行成功") + else: + logging.error(f"设备 {self.device_id} 自动化流程执行失败") + return success + except Exception as e: + logging.error(f"设备 {self.device_id} 自动化执行过程中发生错误: {str(e)}") + return False + finally: + self.quit() + + def quit(self): + """关闭驱动""" + safe_quit_driver(getattr(self, 'driver', None), self.device_id) + +# 主执行逻辑 +if __name__ == "__main__": + + # 单个设备配置 - 现在DeviceAutomation会自动获取设备ID + + try: + automation = DeviceAutomation() + success = automation.run_automation() + + if success: + logging.info(f"设备自动化流程执行成功") + else: + logging.error(f"设备自动化流程执行失败") + except Exception as e: + logging.error(f"设备执行出错: {str(e)}") + + + + \ No newline at end of file diff --git a/music/901135.wav b/music/901135.wav new file mode 100644 index 0000000..1464ac2 Binary files /dev/null and b/music/901135.wav differ diff --git a/page_objects/__init__.py b/page_objects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/page_objects/__pycache__/__init__.cpython-312.pyc b/page_objects/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b3050e1 Binary files /dev/null and b/page_objects/__pycache__/__init__.cpython-312.pyc differ diff --git a/page_objects/__pycache__/download_tabbar_page.cpython-312.pyc b/page_objects/__pycache__/download_tabbar_page.cpython-312.pyc new file mode 100644 index 0000000..02ad58c Binary files /dev/null and b/page_objects/__pycache__/download_tabbar_page.cpython-312.pyc differ diff --git a/page_objects/__pycache__/login_page.cpython-312.pyc b/page_objects/__pycache__/login_page.cpython-312.pyc new file mode 100644 index 0000000..691de42 Binary files /dev/null and b/page_objects/__pycache__/login_page.cpython-312.pyc differ diff --git a/page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc b/page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc new file mode 100644 index 0000000..8425108 Binary files /dev/null and b/page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc differ diff --git a/page_objects/__pycache__/more_download_page.cpython-312.pyc b/page_objects/__pycache__/more_download_page.cpython-312.pyc new file mode 100644 index 0000000..b2e6785 Binary files /dev/null and b/page_objects/__pycache__/more_download_page.cpython-312.pyc differ diff --git a/page_objects/__pycache__/screenshot_page.cpython-312.pyc b/page_objects/__pycache__/screenshot_page.cpython-312.pyc new file mode 100644 index 0000000..76bb38a Binary files /dev/null and b/page_objects/__pycache__/screenshot_page.cpython-312.pyc differ diff --git a/page_objects/__pycache__/section_mileage_config_page.cpython-312.pyc b/page_objects/__pycache__/section_mileage_config_page.cpython-312.pyc new file mode 100644 index 0000000..742ec86 Binary files /dev/null and b/page_objects/__pycache__/section_mileage_config_page.cpython-312.pyc differ diff --git a/page_objects/__pycache__/upload_config_page.cpython-312.pyc b/page_objects/__pycache__/upload_config_page.cpython-312.pyc new file mode 100644 index 0000000..8cc26d1 Binary files /dev/null and b/page_objects/__pycache__/upload_config_page.cpython-312.pyc differ diff --git a/page_objects/call_xie.py b/page_objects/call_xie.py new file mode 100644 index 0000000..84e46b2 --- /dev/null +++ b/page_objects/call_xie.py @@ -0,0 +1,46 @@ +import socket + +def send_tcp_command(command: str, host: str = '127.0.0.1', port: int = 8888, encoding: str = 'utf-8') -> bool: + """ + 向指定TCP端口发送指令 + + 参数: + command: 要发送的指令字符串 + host: 目标主机地址(默认127.0.0.1) + port: 目标端口(默认8888) + encoding: 字符串编码格式(默认utf-8) + + 返回: + 发送成功返回True,失败返回False + """ + # 创建TCP socket并自动关闭(with语句确保资源释放) + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + try: + # 连接服务器(超时时间5秒,避免无限阻塞) + sock.settimeout(5.0) + sock.connect((host, port)) + + # 发送指令(转换为字节流) + sock.sendall(command.encode(encoding)) + print(f"指令 '{command}' 发送成功") + return True + + except ConnectionRefusedError: + print(f"连接失败:{host}:{port} 未监听或不可达") + except socket.timeout: + print(f"连接超时:超过5秒未连接到 {host}:{port}") + except UnicodeEncodeError: + print(f"编码失败:指令包含{encoding}无法编码的字符") + except Exception as e: + print(f"发送失败:{str(e)}") + + return False + + +# 使用示例 +if __name__ == "__main__": + # 发送StartConnect指令 + send_tcp_command("StartConnect") + + # 也可以发送其他指令,例如: + # send_tcp_command("StopConnect") \ No newline at end of file diff --git a/page_objects/download_tabbar_page.py b/page_objects/download_tabbar_page.py new file mode 100644 index 0000000..e753889 --- /dev/null +++ b/page_objects/download_tabbar_page.py @@ -0,0 +1,319 @@ +# 更新基站页面操作 +# page_objects/download_tabbar_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException, StaleElementReferenceException +import logging +import time +from datetime import datetime + +import globals.ids as ids # 导入元素ID +import globals.global_variable as global_variable # 导入全局变量 +from globals.driver_utils import check_session_valid, reconnect_driver + +class DownloadTabbarPage: + def __init__(self, driver, wait, device_id): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + # 添加默认的目标日期值 + self.target_year = 2022 + self.target_month = 9 + self.target_day = 22 + + def is_download_tabbar_visible(self): + """检查下载标签栏是否可见""" + try: + return self.driver.find_element(AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID).is_displayed() + except NoSuchElementException: + self.logger.warning("下载标签栏元素未找到") + return False + except Exception as e: + self.logger.error(f"检查下载标签栏可见性时发生意外错误: {str(e)}") + return False + + def click_download_tabbar(self): + """点击下载标签栏""" + try: + download_tab = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID)) + ) + download_tab.click() + self.logger.info("已点击下载标签栏") + + # 使用显式等待替代固定等待 + self.wait.until( + lambda driver: self.is_download_tabbar_visible() + ) + return True + except TimeoutException: + self.logger.error("等待下载标签栏可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载标签栏时出错: {str(e)}") + return False + + def update_work_base(self): + """更新工作基点""" + try: + # 点击更新工作基点 + update_work_base = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_WORK_BASE)) + ) + update_work_base.click() + self.logger.info("已点击更新工作基点") + + # 等待更新完成 - 可以添加更具体的等待条件 + # 例如等待某个进度条消失或成功提示出现 + time.sleep(2) # 暂时保留,但建议替换为显式等待 + return True + except TimeoutException: + self.logger.error("等待更新工作基点按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"更新工作基点时出错: {str(e)}") + return False + + def _get_current_date(self): + """获取当前开始日期控件的日期值,支持多种格式解析""" + try: + date_element = self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.DATE_START)) + ) + date_text = date_element.text.strip() + self.logger.info(f"获取到当前开始日期: {date_text}") + + # 尝试多种日期格式解析 + date_formats = [ + "%Y-%m-%d", # 匹配 '2025-08-12' 格式 + "%Y年%m月%d日", # 匹配 '2025年08月12日' 格式 + "%Y/%m/%d" # 可选:添加其他可能的格式 + ] + + for fmt in date_formats: + try: + return datetime.strptime(date_text, fmt) + except ValueError: + continue # 尝试下一种格式 + + # 如果所有格式都匹配失败 + self.logger.error(f"日期格式解析错误: 无法识别的格式,日期文本: {date_text}") + return None + + except TimeoutException: + self.logger.error("获取当前日期超时") + return None + except Exception as e: + self.logger.error(f"获取当前日期失败: {str(e)}") + return None + + def update_level_line(self): + """更新水准线路,修改为设置2022年9月22日""" + try: + # 点击更新水准线路 + update_level_line = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE)) + ) + update_level_line.click() + self.logger.info("已点击更新水准线路") + + # 获取原始开始日期 + original_date = self._get_current_date() + if not original_date: + self.logger.error("无法获取原始开始日期,更新水准线路失败") + return False + + # 点击开始日期 + date_start = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.DATE_START)) + ) + date_start.click() + self.logger.info("已点击开始日期控件") + + # 处理时间选择器,设置为2022年9月22日 + if not self.handle_time_selector(2022, 9, 22, original_date): + self.logger.error("处理时间选择失败") + return False + + return True + except TimeoutException: + self.logger.error("等待更新水准线路按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"更新水准线路时出错: {str(e)}") + return False + + def _scroll_to_value(self, picker_id, target_value, original_value, max_attempts=20): + """滚动选择器到目标值,基于原始值计算滚动次数""" + try: + # 计算需要滚动的次数(绝对值) + scroll_count = abs(int(target_value) - int(original_value)) + self.logger.info(f"需要滚动{scroll_count}次将{picker_id}从{original_value}调整到{target_value}") + + # 确定滚动方向 + direction = "down" if int(target_value) > int(original_value) else "up" + + # 获取选择器元素 + picker = self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, picker_id)) + ) + + # 计算滚动坐标 + x = picker.location['x'] + picker.size['width'] // 2 + self.logger.info(f"水平位置x为{x}") + y_center = picker.location['y'] + picker.size['height'] // 2 + self.logger.info(f"垂直位置中点y_center为{y_center}") + # start_y = y_center if direction == "down" else picker.location['y'] + # end_y = picker.location['y'] if direction == "down" else y_center + # 关键修改:计算选择器高度的五分之一(滑动距离) + height_fifth = picker.size['height'] // 5 # 1/5高度 + + # 根据方向计算起点和终点,确保滑动距离为 height_fifth + if direction == "down": + # 向下滚动:从中心点向上滑动1/5高度 + start_y = y_center + end_y = y_center - height_fifth # 终点 = 中心点 - 1/5高度 + self.logger.info(f"down垂直开始位置start_y为{y_center},垂直结束位置end_y为{end_y}") + + else: + # 向上滚动:从中心点向下滑动1/5高度 + start_y = y_center + end_y = y_center + height_fifth # 终点 = 中心点 + 1/5高度 + self.logger.info(f"up垂直开始位置start_y为{y_center},垂直结束位置end_y为{end_y}") + # 执行滚动操作 + for _ in range(scroll_count): + self.driver.swipe(x, start_y, x, end_y, 500) + time.sleep(0.5) # 等待滚动稳定 + return True # 循环scroll_count次后直接返回 + # # 验证当前值 + # current_value = picker.text + # if current_value == str(target_value): + # self.logger.info(f"{picker_id}已达到目标值: {target_value}") + # return True + + # 最终验证 + # final_value = picker.text + # if final_value == str(target_value): + # self.logger.info(f"{picker_id}已达到目标值: {target_value}") + # return True + # else: + # self.logger.error(f"{picker_id}滚动{scroll_count}次后未达到目标值,当前值: {final_value}") + # return False + + except StaleElementReferenceException: + self.logger.warning("元素状态已过期,重新获取") + return False + except Exception as e: + self.logger.error(f"滚动选择器出错: {str(e)}") + return False + + def handle_time_selector(self, target_year, target_month, target_day, original_date=None): + """处理时间选择器,选择起始时间并确认""" + self.logger.info(f"传入handle_time_selector的初始日期: {original_date}") + try: + # 等待时间选择器出现 + self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.ALERT_DIALOG)) + ) + self.logger.info("时间选择对话框已出现") + + # 如果没有提供原始日期,使用当前日期控件的值 + if not original_date: + original_date = self._get_current_date() + if not original_date: + self.logger.error("无法获取原始日期,处理时间选择失败") + return False + + # 滚动选择年份 + if not self._scroll_to_value(ids.SCRCOLL_YEAR, target_year, original_date.year): + self.logger.error("滚动选择年份失败") + return False + + # 滚动选择月份 + if not self._scroll_to_value(ids.SCRCOLL_MONTH, target_month, original_date.month): + self.logger.error("滚动选择月份失败") + return False + + # 滚动选择日期 + if not self._scroll_to_value(ids.SCRCOLL_DAY, target_day, original_date.day): + self.logger.error("滚动选择日期失败") + return False + + # 点击确认按钮 + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.SCRCOLL_CONFIRM)) # 日期选择器确认按钮 + ) + confirm_btn.click() + self.logger.info("已确认时间选择") + + # 点击对话框确认按钮(UPDATE_LEVEL_LINE_CONFIRM) + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE_CONFIRM)) # 选择日期对话框确认按钮 + ) + confirm_btn.click() + self.logger.info("已点击对话框确认按钮commit") + + # 新增:等待加载对话框出现 + custom_dialog = self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG)) + ) + self.logger.info("检测到loading对话框出现") + + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(self.device_id, self.driver): + self.logger.error(f"设备 {self.device_id} 驱动重连失败") + + # 新增:等待加载对话框消失(表示更新完成) + WebDriverWait(self.driver, 300).until( + EC.invisibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG)) + ) + self.logger.info("loading对话框已消失,更新完成") + + '''点击commit确认按钮后,loading弹窗会出现,等待其加载完成后关闭 + 检测导航栏中的测量tabbar是否出现来确定是否返回True + ''' + # measure_tabbar_btn = self.wait.until( + # EC.visibility_of_element_located((AppiumBy.ID, ids.MEASURE_TABBAR_ID)) + # ) + # self.logger.info("检测测量tabbar按钮出现") + + return True + + except TimeoutException as e: + # 明确超时发生在哪个环节 + self.logger.error("处理时间选择时超时:可能是等待对话框出现、日期选择器元素或确认按钮超时", exc_info=True) + return False + except Exception as e: + # 细分不同环节的异常 + self.logger.error(f"处理时间选择对话框时出错:可能是日期滚动、选择器确认或对话框确认环节失败 - {str(e)}", exc_info=True) + return False + + def download_tabbar_page_manager(self): + """执行基础更新操作""" + try: + # 执行基础更新流程 + self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 开始执行更新流程") + + # 点击下载标签栏 + if not self.click_download_tabbar(): + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 点击下载标签栏失败") + return False + + # 更新工作基点 + if not self.update_work_base(): + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新工作基点失败") + return False + + # 更新水准线路 + if not self.update_level_line(): + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新水准线路失败") + return False + + self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新操作执行成功") + return True + except Exception as e: + self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 执行更新操作时出错: {str(e)}") + return False \ No newline at end of file diff --git a/page_objects/login_page.py b/page_objects/login_page.py new file mode 100644 index 0000000..f02b3a2 --- /dev/null +++ b/page_objects/login_page.py @@ -0,0 +1,78 @@ + # 登录页面操作 +# page_objects/login_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +import logging +import time + +import globals.ids as ids +import globals.global_variable as global_variable # 导入全局变量模块 + +class LoginPage: + def __init__(self, driver, wait): + self.driver = driver + self.wait = wait + self.logger = logging.getLogger(__name__) + + def is_login_page(self): + """检查当前是否为登录页面""" + try: + return self.driver.find_element(AppiumBy.ID, ids.LOGIN_BTN).is_displayed() + except NoSuchElementException: + return False + + def login(self): + """执行登录操作""" + try: + self.logger.info("正在执行登录操作...") + + # 获取文本框中已有的用户名 + username_field = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_USERNAME)) + ) + + # 读取文本框内已有的用户名(.text属性获取元素显示的文本内容) + existing_username = username_field.text + # 3. 将获取到的用户名写入全局变量中 + global_variable.GLOBAL_USERNAME = existing_username # 关键:给全局变量赋值 + + # 日志记录获取到的已有用户名(若为空,也需明确记录,避免后续误解) + if existing_username.strip(): # 去除空格后判断是否有有效内容 + self.logger.info(f"已获取文本框中的已有用户名: {existing_username}") + else: + self.logger.info("文本框中未检测到已有用户名(内容为空)") + + # 点击登录按钮 + login_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_BTN)) + ) + login_btn.click() + self.logger.info("已点击登录按钮") + + # 等待登录完成 + time.sleep(3) + + # 检查是否登录成功 + if self.is_login_successful(): + self.logger.info("登录成功") + return True + else: + self.logger.warning("登录后未检测到主页面元素") + return False + + except Exception as e: + self.logger.error(f"登录过程中出错: {str(e)}") + return False + + def is_login_successful(self): + """检查登录是否成功""" + try: + # 等待主页面元素出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID)) + ) + return True + except TimeoutException: + return False \ No newline at end of file diff --git a/page_objects/more_download_page.py b/page_objects/more_download_page.py new file mode 100644 index 0000000..cb4c08a --- /dev/null +++ b/page_objects/more_download_page.py @@ -0,0 +1,506 @@ +# test_more_download_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +import logging +import time +from globals.driver_utils import launch_app_manually +from page_objects.login_page import LoginPage + +class MoreDownloadPage: + def __init__(self, driver, wait,device_id): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + + def is_on_more_download_page(self): + """通过下载历史数据按钮来判断是否在更多下载页面""" + try: + # 使用下载历史数据按钮的resource-id来检查 + download_history_locator = (AppiumBy.ID, "com.bjjw.cjgc:id/download_history") + self.wait.until(EC.presence_of_element_located(download_history_locator)) + self.logger.info("已确认在更多下载页面") + return True + except TimeoutException: + self.logger.warning("未找到下载历史数据按钮,不在更多下载页面") + return False + except Exception as e: + self.logger.error(f"检查更多下载页面时发生意外错误: {str(e)}") + return False + + def click_download_button(self): + """点击下载按钮""" + try: + # 点击下载历史数据按钮 + download_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_history")) + ) + download_button.click() + self.logger.info("已点击下载历史数据按钮") + + # 等待下载操作开始 + # time.sleep(3) + + return True + + except TimeoutException: + self.logger.error("等待下载按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载按钮时出错: {str(e)}") + return False + + def click_download_original_data(self): + """点击下载原始数据按钮并处理日期选择""" + try: + # 点击下载原始数据按钮 + download_original_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_org")) + ) + download_original_btn.click() + self.logger.info("已点击下载原始数据按钮") + + # 等待日期选择弹窗出现 + # time.sleep(2) + + # 点击选择开始日期 + start_date_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date")) + ) + start_date_btn.click() + self.logger.info("已点击选择开始日期") + + # 等待日期选择器出现 + # time.sleep(2) + + # 滑动年份选择器 - 向上滑动1/5的距离 + if not self._swipe_year_wheel(): + self.logger.error("滑动年份选择器失败") + return False + + # 点击日期选择器的确定按钮 + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn")) + ) + confirm_btn.click() + self.logger.info("已确认日期选择") + + # 等待日期选择器关闭 + # time.sleep(2) + + # 假设弹窗有确定按钮,点击它开始下载 + try: + # 尝试查找并点击下载弹窗的确定按钮 + download_confirm_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]")) + ) + download_confirm_btn.click() + self.logger.info("已点击下载确认按钮") + except TimeoutException: + self.logger.warning("未找到下载确认按钮,可能不需要确认") + + # 等待下载开始 + # time.sleep(3) + + return True + + except TimeoutException: + self.logger.error("等待下载原始数据按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载原始数据时出错: {str(e)}") + return False + + def click_download_result_data(self): + """点击下载成果数据按钮并处理日期选择""" + try: + # 点击下载成果数据按钮 + download_result_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_result")) + ) + download_result_btn.click() + self.logger.info("已点击下载成果数据按钮") + + # 等待日期选择弹窗出现 + # time.sleep(2) + + # 点击选择开始日期 + start_date_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date")) + ) + start_date_btn.click() + self.logger.info("已点击选择开始日期") + + # 等待日期选择器出现 + # time.sleep(2) + + # 滑动年份选择器 - 向上滑动1/5的距离 + if not self._swipe_year_wheel(): + self.logger.error("滑动年份选择器失败") + return False + + # 点击日期选择器的确定按钮 + confirm_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn")) + ) + confirm_btn.click() + self.logger.info("已确认日期选择") + + # 等待日期选择器关闭 + # time.sleep(2) + + # 假设弹窗有确定按钮,点击它开始下载 + try: + # 尝试查找并点击下载弹窗的确定按钮 + download_confirm_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]")) + ) + download_confirm_btn.click() + self.logger.info("已点击下载确认按钮") + except TimeoutException: + self.logger.warning("未找到下载确认按钮,可能不需要确认") + + # 等待下载开始 + # time.sleep(3) + + return True + + except TimeoutException: + self.logger.error("等待下载成果数据按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击下载成果数据时出错: {str(e)}") + return False + + def _swipe_year_wheel(self): + """滑动年份选择器的滚轮""" + try: + # 获取年份选择器滚轮元素 + year_wheel = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/wheelView2") + + # 获取滚轮的位置和尺寸 + location = year_wheel.location + size = year_wheel.size + + # 计算滚轮中心点坐标 + center_x = location['x'] + size['width'] // 2 + center_y = location['y'] + size['height'] // 2 + + # 计算滑动距离 - 滚轮高度的1/5 + swipe_distance = size['height'] // 5 + + for i in range(3): + # 执行滑动操作 - 从中心向上滑动1/5高度 + self.driver.swipe(center_x, center_y - swipe_distance, center_x, center_y, 500) + + self.logger.info(f"已{i}次滑动月份选择器") + return True + + except Exception as e: + self.logger.error(f"滑动年份选择器时出错: {str(e)}") + return False + + # def wait_for_loading_dialog(self, timeout=900, download_type="unknown"): + # """ + # 检查特定结构的加载弹窗的出现和消失 + + # 参数: + # timeout: 最大等待时间,默认10分钟(600秒) + + # 返回: + # bool: 如果加载弹窗出现并消失返回True,否则返回False + # """ + # try: + # self.logger.info(f"开始检查{download_type}加载弹窗...") + + # # 首先检查加载弹窗是否出现 + # start_time = time.time() + # loading_appeared = False + + # # 等待加载弹窗出现(最多等待30秒) + # while time.time() - start_time < 30: + # try: + # # 根据提供的结构查找加载弹窗 + # # 查找包含ProgressBar和"loading..."文本的弹窗 + # loading_indicators = [ + # (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']/android.widget.LinearLayout[@resource-id='android:id/parentPanel']//android.widget.ProgressBar"), + # (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"), + # (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"), + # (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]") + # ] + + # for by, value in loading_indicators: + # try: + # element = self.driver.find_element(by, value) + # if element.is_displayed(): + # loading_appeared = True + # self.logger.info("数据下载已开始") + # self.logger.info("检测到加载弹窗出现") + # break + # except: + # continue + + # if loading_appeared: + # break + + # except Exception as e: + # pass + + # time.sleep(1) + + # # 如果加载弹窗没有出现,直接返回True + # if not loading_appeared: + # self.logger.info("未检测到加载弹窗,继续执行") + # return True + + # # 等待加载弹窗消失 + # self.logger.info("等待加载弹窗消失...") + # disappearance_start_time = time.time() + + # while time.time() - disappearance_start_time < timeout: + # try: + # # 检查加载弹窗是否还存在 + # loading_still_exists = False + + # for by, value in loading_indicators: + # try: + # element = self.driver.find_element(by, value) + # if element.is_displayed(): + # loading_still_exists = True + # break + # except: + # continue + + # if not loading_still_exists: + # self.logger.info("加载弹窗已消失") + # return True + + # # 每1分钟记录一次状态 + # if int(time.time() - disappearance_start_time) % 60 == 0: + # elapsed_time = int(time.time() - disappearance_start_time) + # self.logger.info(f"加载弹窗仍在显示,已等待{elapsed_time//60}分钟") + + # except Exception as e: + # # 如果出现异常,可能弹窗已经消失 + # self.logger.info("加载弹窗可能已消失") + # return True + + # time.sleep(1) + + # # 如果超时,记录错误并返回False + # self.logger.error(f"加载弹窗在{timeout}秒后仍未消失") + # # return False + # # 检查是否有loading加载窗口 + # try: + # self.logger.info(f"检查{download_type}下载是否有loading加载窗口...") + # loading_indicators = [ + # (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']/android.widget.LinearLayout[@resource-id='android:id/parentPanel']//android.widget.ProgressBar"), + # (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"), + # (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"), + # (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]") + # ] + + # loading_exists = False + # for by, value in loading_indicators: + # try: + # element = self.driver.find_element(by, value) + # if element.is_displayed(): + # loading_exists = True + # break + # except: + # continue + + # if loading_exists: + # self.logger.info(f"检测到{download_type}下载的loading加载窗口,执行重新打开应用操作") + + + # # 手动启动应用 + # launch_app_manually(self.driver, self.device_id, "com.bjjw.cjgc", ".activity.LoginActivity") + # self.logger.info("已重新启动沉降观测应用") + + # # 点击登录 + # login_page = LoginPage(self.driver, self.wait) + # if login_page.is_login_page(): + # if login_page.login(): + # self.logger.info("登录成功") + # else: + # self.logger.error("登录失败") + # return False + # else: + # self.logger.info("应用已登录,无需重复登录") + + # self.logger.info(f"{download_type}下载超时处理完成,应用已重启") + # # 点击img_5_layout(更多下载按钮) + # more_download_btn = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout")) + # ) + # more_download_btn.click() + # self.logger.info("已点击更多下载按钮") + # self.more_download_page_manager_2(download_type) + # # 等待页面加载 + # time.sleep(1) + # else: + # self.logger.info(f"未检测到{download_type}下载的loading加载窗口") + # except Exception as e: + # self.logger.error(f"检查{download_type}下载的loading加载窗口时出错: {str(e)}") + # # 出错时继续执行,不影响主流程 + + # return False + + # except Exception as e: + # self.logger.error(f"检查加载弹窗时出错: {str(e)}") + # return False + + def _is_loading_present(self): + """私有辅助方法:检测当前页面是否存在加载弹窗""" + loading_indicators = [ + (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"), + (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"), + (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]") + ] + for by, value in loading_indicators: + try: + element = self.driver.find_element(by, value) + if element.is_displayed(): + return True + except: + continue + return False + + def wait_for_loading_dialog(self, timeout=900, download_type="unknown", retry_count=0): + """ + 检查加载弹窗的出现和消失,支持最多1次重试(总共执行2次) + """ + try: + self.logger.info(f"开始检查 {download_type} 加载弹窗 (尝试次数: {retry_count + 1})...") + + # 1. 等待加载弹窗出现(最多30秒) + start_time = time.time() + loading_appeared = False + while time.time() - start_time < 30: + if self._is_loading_present(): + loading_appeared = True + self.logger.info(f"检测到 {download_type} 加载弹窗出现") + break + time.sleep(1) + + if not loading_appeared: + self.logger.info(f"未检测到 {download_type} 加载弹窗,视为直接通过") + return True + + # 2. 等待加载弹窗消失 + disappearance_start_time = time.time() + while time.time() - disappearance_start_time < timeout: + if not self._is_loading_present(): + self.logger.info(f"{download_type} 加载弹窗已消失") + return True + + # 每60秒打印一次日志 + if int(time.time() - disappearance_start_time) % 60 == 0: + self.logger.info(f"等待中...已耗时 {int(time.time() - disappearance_start_time)//60} 分钟") + time.sleep(2) + + # 3. 超时处理逻辑 + self.logger.error(f"{download_type} 加载弹窗在 {timeout} 秒后仍未消失") + + # 检查是否还可以重试(retry_count=0 时执行重试,即第二次执行) + if retry_count < 1: + self.logger.warning(f"检测到超时,准备进行第 {retry_count + 2} 次尝试(重启应用)...") + + # 执行重启逻辑 + from globals.driver_utils import launch_app_manually + from page_objects.login_page import LoginPage + + launch_app_manually(self.driver, self.device_id, "com.bjjw.cjgc", ".activity.LoginActivity") + login_page = LoginPage(self.driver, self.wait) + if login_page.is_login_page(): + login_page.login() + + # 重新导航到下载页面 + more_download_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout")) + ) + more_download_btn.click() + + # 递归调用:增加 retry_count + return self.more_download_page_manager_2(download_type, retry_count = retry_count + 1) + else: + self.logger.error(f"{download_type} 已达到最大重试次数,操作失败") + return False + + except Exception as e: + self.logger.error(f"检查加载弹窗时发生异常: {str(e)}") + return False + + def more_download_page_manager_2(self, download_type, retry_count=0): + """ + 修改后的 manager_2,透传 retry_count 参数 + """ + try: + if not self.is_on_more_download_page(): + return False + + self.click_download_button() + + if download_type in ["原始数据", "历史数据"]: + self.click_download_original_data() + return self.wait_for_loading_dialog(download_type="原始数据", retry_count=retry_count) + + elif download_type == "成果数据": + self.click_download_result_data() + return self.wait_for_loading_dialog(download_type="成果数据", retry_count=retry_count) + + return False + except Exception as e: + self.logger.error(f"manager_2 执行出错: {str(e)}") + return False + + def more_download_page_manager(self): + """执行更多下载页面管理操作""" + try: + self.logger.info("开始执行更多下载页面操作") + + # 检查是否在更多下载页面 + if not self.is_on_more_download_page(): + self.logger.error("不在更多下载页面") + return False + + # 点击下载历史数据按钮 + if not self.click_download_button(): + self.logger.error("点击下载历史数据按钮失败") + return False + + # 等待下载历史数据页面加载完成 + # time.sleep(3) + + # 点击下载原始数据按钮 + if not self.click_download_original_data(): + self.logger.error("点击下载原始数据按钮失败") + return False + + # 等待下载操作完成 + time.sleep(1) + + # 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失 + if not self.wait_for_loading_dialog(download_type="原始数据"): + self.logger.warning("下载过程中的加载弹窗未在预期时间内消失,但操作已完成") + + # 等待一段时间,确保原始数据下载完成 + time.sleep(1) + + # 点击下载成果数据按钮 + if not self.click_download_result_data(): + self.logger.error("点击下载成果数据按钮失败") + return False + + # 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失 + if not self.wait_for_loading_dialog(download_type="成果数据"): + self.logger.warning("成果数据下载过程中的加载弹窗未在预期时间内消失,但操作已完成") + + self.logger.info("更多下载页面操作执行完成") + return True + except Exception as e: + self.logger.error(f"执行更多下载页面操作时出错: {str(e)}") + return False \ No newline at end of file diff --git a/page_objects/screenshot_page.py b/page_objects/screenshot_page.py new file mode 100644 index 0000000..15e3a83 --- /dev/null +++ b/page_objects/screenshot_page.py @@ -0,0 +1,1471 @@ +# screenshot_page.py +import subprocess +import logging +import time +import re +import os +from datetime import datetime +from appium.webdriver.common.appiumby import AppiumBy +from selenium.common.exceptions import NoSuchElementException, TimeoutException +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import globals.global_variable as global_variable # 导入全局变量模块 +import globals.ids as ids +# 导入全局驱动工具函数 +from globals.driver_utils import check_session_valid, reconnect_driver +import globals.driver_utils as driver_utils +import globals.global_variable as global_variable # 导入全局变量模块 + +class ScreenshotPage: + def __init__(self, driver, wait, device_id=None): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + self.all_items = set() + + def scroll_list(self, direction="down"): + """滑动列表以加载更多项目 + + Args: + direction: 滑动方向,"down"表示向下滑动,"up"表示向上滑动 + + Returns: + bool: 滑动是否成功执行,对于向上滑动,如果滑动到顶则返回False + """ + try: + # 获取列表容器 + list_container = self.driver.find_element(AppiumBy.ID, ids.MEASURE_LIST_ID) + + # 计算滑动坐标 + start_x = list_container.location['x'] + list_container.size['width'] // 2 + + if direction == "down": + # 向下滑动 + start_y = list_container.location['y'] + list_container.size['height'] * 0.95 + end_y = list_container.location['y'] + list_container.size['height'] * 0.05 + self.logger.info("向下滑动列表") + else: + # 向上滑动 + # 记录滑动前的项目,用于判断是否滑动到顶 + before_scroll_items = self.get_current_items() + start_y = list_container.location['y'] + list_container.size['height'] * 0.05 + end_y = list_container.location['y'] + list_container.size['height'] * 0.95 + self.logger.info("向上滑动列表") + + # 执行滑动 + self.driver.swipe(start_x, start_y, start_x, end_y, 1000) + + + # 向上滑动时,检查是否滑动到顶 + if direction == "up": + after_scroll_items = self.get_current_items() + # 如果滑动后的项目与滑动前的项目相同,说明已经滑动到顶 + if after_scroll_items == before_scroll_items: + self.logger.info("已滑动到列表顶部,列表内容不变") + return False + + return True + except Exception as e: + self.logger.error(f"滑动列表失败: {str(e)}") + return False + + + def get_current_items(self): + """获取当前页面中的所有项目文本""" + try: + items = self.driver.find_elements(AppiumBy.ID, ids.MEASURE_LISTVIEW_ID) + item_texts = [] + + for item in items: + try: + title_element = item.find_element(AppiumBy.ID, ids.MEASURE_NAME_TEXT_ID) + if title_element and title_element.text: + item_texts.append(title_element.text) + except NoSuchElementException: + continue + + return item_texts + except Exception as e: + self.logger.error(f"获取当前项目失败: {str(e)}") + return [] + + def click_item_by_text(self, text): + """点击指定文本的项目""" + try: + # 查找包含指定文本的项目 + items = self.driver.find_elements(AppiumBy.ID, ids.MEASURE_LISTVIEW_ID) + + for item in items: + try: + title_element = item.find_element(AppiumBy.ID, ids.MEASURE_NAME_TEXT_ID) + if title_element and title_element.text == text: + title_element.click() + self.logger.info(f"已点击项目: {text}") + return True + except NoSuchElementException: + continue + + self.logger.warning(f"未找到可点击的项目: {text}") + return False + except Exception as e: + self.logger.error(f"点击项目失败: {str(e)}") + return False + + + + def find_keyword(self, fixed_filename): + """查找指定关键词并点击,支持向下和向上滑动查找""" + try: + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + if not reconnect_driver(self.device_id, self.driver): + self.logger.error(f"设备 {self.device_id} 驱动重连失败") + + # 等待线路列表容器出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_LIST_ID)) + ) + self.logger.info("线路列表容器已找到") + + max_scroll_attempts = 100 # 最大滚动尝试次数 + scroll_count = 0 + previous_items = set() # 记录前一次获取的项目集合,用于检测是否到达边界 + + # 首先尝试向下滑动查找 + while scroll_count < max_scroll_attempts: + # 获取当前页面中的所有项目 + current_items = self.get_current_items() + # self.logger.info(f"当前页面找到 {len(current_items)} 个项目: {current_items}") + + # 检查目标文件是否在当前页面中 + if fixed_filename in current_items: + self.logger.info(f"找到目标文件: {fixed_filename}") + # 点击目标文件 + if self.click_item_by_text(fixed_filename): + return True + else: + self.logger.error(f"点击目标文件失败: {fixed_filename}") + return False + + # 检查是否到达底部:连续两次获取的项目相同 + if current_items == previous_items and len(current_items) > 0: + self.logger.info("连续两次获取的项目相同,已到达列表底部") + break + + # 更新前一次项目集合 + previous_items = current_items.copy() + + # 向下滑动列表以加载更多项目 + if not self.scroll_list(direction="down"): + self.logger.error("向下滑动列表失败") + return False + + scroll_count += 1 + self.logger.info(f"第 {scroll_count} 次向下滑动,继续查找...") + + # 如果向下滑动未找到,尝试向上滑动查找 + self.logger.info("向下滑动未找到目标,开始向上滑动查找") + + # 重置滚动计数 + scroll_count = 0 + + while scroll_count < max_scroll_attempts: + # 向上滑动列表 + # 如果返回False,说明已经滑动到顶 + if not self.scroll_list(direction="up"): + # 检查是否是因为滑动到顶而返回False + if "已滑动到列表顶部" in self.logger.handlers[0].buffer[-1].message: + self.logger.info("已滑动到列表顶部,停止向上滑动") + break + else: + self.logger.error("向上滑动列表失败") + return False + + # 获取当前页面中的所有项目 + current_items = self.get_current_items() + # self.logger.info(f"向上滑动后找到 {len(current_items)} 个项目: {current_items}") + + # 检查目标文件是否在当前页面中 + if fixed_filename in current_items: + self.logger.info(f"找到目标文件: {fixed_filename}") + # 点击目标文件 + if self.click_item_by_text(fixed_filename): + return True + else: + self.logger.error(f"点击目标文件失败: {fixed_filename}") + return False + + scroll_count += 1 + self.logger.info(f"第 {scroll_count} 次向上滑动,继续查找...") + + self.logger.warning(f"经过 {max_scroll_attempts * 2} 次滑动仍未找到目标文件") + return False + + except TimeoutException: + self.logger.error("等待线路列表元素超时") + return False + except Exception as e: + self.logger.error(f"查找关键词时出错: {str(e)}") + return False + + def handle_measurement_dialog(self): + """处理测量弹窗 - 选择继续测量""" + try: + self.logger.info("检查测量弹窗...") + + # 直接尝试点击"继续测量"按钮 + continue_btn = WebDriverWait(self.driver, 2).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/measure_continue_btn")) + ) + continue_btn.click() + self.logger.info("已点击'继续测量'按钮") + return True + + except TimeoutException: + self.logger.info("未找到继续测量按钮,可能没有弹窗") + return True # 没有弹窗也认为是成功的 + except Exception as e: + self.logger.error(f"点击继续测量按钮时出错: {str(e)}") + return False + + # 检查有没有平差处理按钮 + def check_apply_btn(self): + """检查是否有平差处理按钮""" + try: + apply_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/point_measure_btn")) + ) + if apply_btn.is_displayed(): + logging.info("进入平差页面") + else: + self.logger.info("没有找到'平差处理'按钮") + return True + except TimeoutException: + self.logger.info("未找到平差处理按钮") + return False # 没有弹窗也认为是成功的 + except Exception as e: + self.logger.error(f"点击平差处理按钮时出错: {str(e)}") + return False + + + def get_line_end_time(self, line_code): + """ + 从全局字典中获取线路编码对应的结束时间 + 参数: + line_code: 线路编码 + 返回: + tuple: (date_str, time_str) 日期和时间字符串 + """ + + if line_code in global_variable.LINE_TIME_MAPPING_DICT: + end_time = global_variable.LINE_TIME_MAPPING_DICT[line_code] + date_str = end_time.strftime("%Y-%m-%d") + time_str = end_time.strftime("%H:%M:%S") + return (date_str, time_str) + else: + self.logger.warning(f"未找到线路编码 {line_code} 的结束时间") + return (None, None) + + def show_line_time_mapping(self): + """ + 显示当前全局字典中的所有线路编码和时间 + """ + + self.logger.info("\n当前全局字典内容:") + if global_variable.LINE_TIME_MAPPING_DICT: + for line_code, end_time in sorted(global_variable.LINE_TIME_MAPPING_DICT.items()): + date_str = end_time.strftime("%Y-%m-%d") + time_str = end_time.strftime("%H:%M:%S") + self.logger.info(f" {line_code}: {date_str} {time_str}") + else: + self.logger.info(" 全局字典为空") + self.logger.info(f"总计: {len(global_variable.LINE_TIME_MAPPING_DICT)} 条记录\n") + def clear_line_time_mapping(self): + """ + 清空全局字典 + """ + global_variable.LINE_TIME_MAPPING_DICT.clear() + self.logger.info("已清空全局字典") + + def set_device_time(self, device_id, time_str=None, date_str=None, disable_auto_sync=True): + """ + 通过ADB设置设备时间(带管理员权限) + + 参数: + device_id: 设备ID + time_str: 时间字符串,格式 "HH:MM:SS" (例如: "14:30:00") + date_str: 日期字符串,格式 "YYYY-MM-DD" (例如: "2024-10-15") + disable_auto_sync: 是否禁用自动时间同步(防止设置的时间被网络时间覆盖) + + 返回: + bool: 操作是否成功 + """ + try: + if time_str is None and date_str is None: + return True + + # 首先尝试获取设备的root权限 + self.logger.info(f"尝试获取设备 {device_id} 的root权限...") + root_result = subprocess.run( + ["adb", "-s", device_id, "root"], + capture_output=True, + text=True, + timeout=10 + ) + + # 检查root权限获取是否成功(有些设备可能返回非0但实际已获取权限) + if root_result.returncode != 0: + self.logger.warning(f"获取root权限返回非0状态码,但继续尝试操作: {root_result.stderr.strip()}") + + now = datetime.now() + hour, minute, second = map(int, (time_str or f"{now.hour}:{now.minute}:{now.second}").split(":")) + year, month, day = map(int, (date_str or f"{now.year}-{now.month}-{now.day}").split("-")) + + # 禁用自动同步 + if disable_auto_sync: + # 使用su命令以root权限执行设置 + subprocess.run( + ["adb", "-s", device_id, "shell", "su", "-c", + "settings put global auto_time 0"], + timeout=5 + ) + subprocess.run( + ["adb", "-s", device_id, "shell", "su", "-c", + "settings put global auto_time_zone 0"], + timeout=5 + ) + + # 优先尝试旧格式 (MMDDhhmmYYYY.ss) + adb_time_str_old = f"{month:02d}{day:02d}{hour:02d}{minute:02d}{year:04d}.{second:02d}" + cmd_old = [ + "adb", "-s", device_id, "shell", "su", "-c", + f"date {adb_time_str_old}" + ] + result = subprocess.run(cmd_old, capture_output=True, text=True, timeout=10) + + if result.returncode != 0: + self.logger.warning(f"旧格式失败,尝试新格式设置日期时间") + + # 尝试新格式(Toybox),使用su -c确保以root权限执行 + adb_time_str_new = f"{year:04d}{month:02d}{day:02d}.{hour:02d}{minute:02d}{second:02d}" + cmd_new = [ + "adb", "-s", device_id, "shell", "su", "-c", + f"date {adb_time_str_new}" + ] + result = subprocess.run(cmd_new, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + self.logger.info(f"设备 {device_id} 时间设置成功: {year}-{month}-{day} {hour}:{minute}:{second}") + return True + else: + self.logger.error(f"设备 {device_id} 设置时间失败: {result.stderr.strip()}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 设置时间命令执行超时") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 设置时间时发生异常: {str(e)}") + return False + + def disable_wifi(self, device_id): + """ + 通过ADB关闭设备WiFi + + 返回: + bool: 操作是否成功 + """ + try: + # 关闭WiFi + cmd_disable_wifi = [ + "adb", "-s", device_id, + "shell", "svc", "wifi", "disable" + ] + + result = subprocess.run(cmd_disable_wifi, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + self.logger.info(f"设备 {device_id} WiFi已关闭") + time.sleep(1) # 等待WiFi完全关闭 + return True + else: + self.logger.error(f"设备 {device_id} 关闭WiFi失败: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 关闭WiFi命令执行超时") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 关闭WiFi时发生错误: {str(e)}") + return False + + def enable_wifi(self, device_id): + """ + 通过ADB打开设备WiFi + + 返回: + bool: 操作是否成功 + """ + try: + # 打开WiFi + cmd_enable_wifi = [ + "adb", "-s", device_id, + "shell", "svc", "wifi", "enable" + ] + + result = subprocess.run(cmd_enable_wifi, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + self.logger.info(f"设备 {device_id} WiFi已打开") + time.sleep(3) # 等待WiFi完全连接 + return True + else: + self.logger.error(f"设备 {device_id} 打开WiFi失败: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 打开WiFi命令执行超时") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 打开WiFi时发生错误: {str(e)}") + return False + + def get_current_time(self, device_id): + """ + 获取设备当前时间 + + 返回: + str: 设备当前时间字符串,如果获取失败则返回None + """ + try: + cmd_get_time = [ + "adb", "-s", device_id, + "shell", "date" + ] + + result = subprocess.run(cmd_get_time, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + current_time = result.stdout.strip() + self.logger.info(f"设备 {device_id} 当前时间: {current_time}") + return current_time + else: + self.logger.error(f"设备 {device_id} 获取时间失败: {result.stderr}") + return None + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 获取时间命令执行超时") + return None + except Exception as e: + self.logger.error(f"设备 {device_id} 获取时间时发生错误: {str(e)}") + return None + + def check_wifi_status(self, device_id): + """ + 检查设备WiFi状态 + + 返回: + str: "enabled"表示已开启, "disabled"表示已关闭, None表示获取失败 + """ + try: + cmd_check_wifi = [ + "adb", "-s", device_id, + "shell", "dumpsys", "wifi", "|", "grep", "Wi-Fi" + ] + + result = subprocess.run(cmd_check_wifi, capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + wifi_status = result.stdout.strip() + if "enabled" in wifi_status.lower(): + self.logger.info(f"设备 {device_id} WiFi状态: 已开启") + return "enabled" + elif "disabled" in wifi_status.lower(): + self.logger.info(f"设备 {device_id} WiFi状态: 已关闭") + return "disabled" + else: + self.logger.warning(f"设备 {device_id} 无法确定WiFi状态: {wifi_status}") + return None + else: + # 尝试另一种方法检查WiFi状态 + cmd_check_wifi_alt = [ + "adb", "-s", device_id, + "shell", "settings", "get", "global", "wifi_on" + ] + + result_alt = subprocess.run(cmd_check_wifi_alt, capture_output=True, text=True, timeout=10) + + if result_alt.returncode == 0: + wifi_on = result_alt.stdout.strip() + if wifi_on == "1": + self.logger.info(f"设备 {device_id} WiFi状态: 已开启") + return "enabled" + elif wifi_on == "0": + self.logger.info(f"设备 {device_id} WiFi状态: 已关闭") + return "disabled" + else: + self.logger.warning(f"设备 {device_id} 无法确定WiFi状态: {wifi_on}") + return None + else: + self.logger.error(f"设备 {device_id} 检查WiFi状态失败: {result_alt.stderr}") + return None + + except subprocess.TimeoutExpired: + self.logger.error(f"设备 {device_id} 检查WiFi状态命令执行超时") + return None + except Exception as e: + self.logger.error(f"设备 {device_id} 检查WiFi状态时发生错误: {str(e)}") + return None + + def take_screenshot(self, filename_prefix="screenshot"): + """ + 通过Appium驱动截取设备屏幕 + + 参数: + filename_prefix: 截图文件前缀 + + 返回: + bool: 操作是否成功 + """ + try: + # 创建测试结果目录 + screenshots_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../test_results/screenshots') + if not os.path.exists(screenshots_dir): + os.makedirs(screenshots_dir) + self.logger.info(f"创建截图目录: {screenshots_dir}") + + # 截图保存 + screenshot_file = os.path.join( + screenshots_dir, + f"{filename_prefix}_{datetime.now().strftime('%Y%m%d')}.png" + ) + self.driver.save_screenshot(screenshot_file) + self.logger.info(f"截图已保存: {screenshot_file}") + return True + + except Exception as e: + self.logger.error(f"截图时发生错误: {str(e)}") + return False + + def wait_for_measurement_end(self, timeout=900): + """ + 等待按钮变成"测量结束",最多15分钟,包含驱动重新初始化机制 + + Args: + timeout: 超时时间,默认900秒(15分钟) + + Returns: + bool: 是否成功等到测量结束按钮 + """ + try: + # 更新WebDriverWait等待时间为900秒 + self.wait = WebDriverWait(self.driver, 900) + self.logger.info(f"设备等待测量结束按钮出现,最多等待 {timeout} 秒") + + start_time = time.time() + reinit_attempts = 0 + max_reinit_attempts = 3 # 最大重新初始化次数 + + while time.time() - start_time < timeout: + try: + # 使用XPath查找文本为"测量结束"的按钮 + measurement_end_button = self.driver.find_element( + AppiumBy.XPATH, + "//android.widget.Button[@text='测量结束']" + ) + + if measurement_end_button.is_displayed() and measurement_end_button.is_enabled(): + self.logger.info(f"设备检测到测量结束按钮") + return True + + except NoSuchElementException: + # 按钮未找到,继续等待 + pass + except Exception as e: + error_msg = str(e) + self.logger.warning(f"设备查找测量结束按钮时出现异常: {error_msg}") + + # 检测是否是UiAutomator2服务崩溃 + if 'UiAutomator2 server' in error_msg and 'instrumentation process is not running' in error_msg and reinit_attempts < max_reinit_attempts: + reinit_attempts += 1 + self.logger.info(f"设备检测到UiAutomator2服务崩溃,尝试第 {reinit_attempts} 次重新初始化驱动") + + # 尝试重新初始化驱动 + if self._reinit_driver(): + self.logger.info(f"设备驱动重新初始化成功") + else: + self.logger.error(f"设备驱动重新初始化失败") + # 继续尝试,而不是立即失败 + + # 等待一段时间后再次检查 + time.sleep(3) + + # 每30秒输出一次等待状态 + if int(time.time() - start_time) % 30 == 0: + elapsed = int(time.time() - start_time) + self.logger.info(f"设备 {self.device_id} 已等待 {elapsed} 秒,仍在等待测量结束...") + + self.logger.error(f"设备 {self.device_id} 等待测量结束按钮超时") + return False + + except Exception as e: + self.logger.error(f"设备 {self.device_id} 等待测量结束时发生错误: {str(e)}") + return False + + def _reinit_driver(self): + """ + 重新初始化Appium驱动 + + Returns: + bool: 是否成功重新初始化 + """ + try: + # 首先尝试关闭现有的驱动 + if hasattr(self, 'driver') and self.driver: + try: + self.driver.quit() + except: + self.logger.warning("关闭现有驱动时出现异常") + + # 导入必要的模块 + from appium import webdriver + from appium.options.android import UiAutomator2Options + + # 重新创建驱动配置 + options = UiAutomator2Options() + options.platform_name = "Android" + options.device_name = self.device_id + options.app_package = "com.bjjw.cjgc" + options.app_activity = ".activity.LoginActivity" + options.automation_name = "UiAutomator2" + options.no_reset = True + options.auto_grant_permissions = True + options.new_command_timeout = 300 + options.udid = self.device_id + + # 重新连接驱动 + self.logger.info(f"正在重新初始化设备 {self.device_id} 的驱动...") + self.driver = webdriver.Remote("http://localhost:4723", options=options) + + # 重新初始化等待对象 + from selenium.webdriver.support.ui import WebDriverWait + self.wait = WebDriverWait(self.driver, 1) + + self.logger.info(f"设备 {self.device_id} 驱动重新初始化完成") + return True + + except Exception as e: + self.logger.error(f"设备 {self.device_id} 驱动重新初始化失败: {str(e)}") + return False + + def handle_confirmation_dialog(self, device_id, timeout=2): + """ + 处理确认弹窗,点击"是"按钮 + + Args: + device_id: 设备ID + timeout: 等待弹窗的超时时间 + + Returns: + bool: 是否成功处理弹窗 + """ + # 等待弹窗出现(最多等待2秒) + try: + dialog_message = WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located((AppiumBy.XPATH, "//android.widget.TextView[@text='是否退出测量界面?']")) + ) + + self.logger.info(f"设备 {device_id} 检测到确认弹窗") + + # 查找并点击"是"按钮 + confirm_button = self.driver.find_element( + AppiumBy.XPATH, + "//android.widget.Button[@text='是' and @resource-id='android:id/button1']" + ) + + if confirm_button.is_displayed() and confirm_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击确认弹窗的'是'按钮") + confirm_button.click() + time.sleep(0.5) + return True + else: + self.logger.error(f"设备 {device_id} '是'按钮不可点击") + return False + + except TimeoutException: + # 超时未找到弹窗,认为没有弹窗,返回成功 + self.logger.info(f"设备 {device_id} 等待 {timeout} 秒未发现确认弹窗,可能没有弹窗,返回成功") + return True + + def click_back_button(self, device_id): + """点击手机系统返回按钮""" + try: + self.driver.back() + self.logger.info("已点击手机系统返回按钮") + return True + except Exception as e: + self.logger.error(f"点击手机系统返回按钮失败: {str(e)}") + return False + + def handle_back_button_with_confirmation(self, device_id, timeout=10): + """ + 处理返回按钮的确认弹窗 + + Args: + device_id: 设备ID + timeout: 等待弹窗的超时时间 + + Returns: + bool: 是否成功处理返回确认弹窗 + """ + logging.info(f"进入handle_back_button_with_confirmation函数") + try: + self.logger.info(f"设备 {device_id} 等待返回确认弹窗出现") + + start_time = time.time() + while time.time() - start_time < timeout: + try: + # 检查是否存在确认弹窗 - 使用多种定位策略提高兼容性 + dialog_selectors = [ + "//android.widget.TextView[@text='是否退出测量界面?']", + "//android.widget.TextView[contains(@text, '退出测量界面')]", + "//android.widget.TextView[contains(@text, '是否退出')]" + ] + + dialog_message = None + for selector in dialog_selectors: + try: + dialog_message = self.driver.find_element(AppiumBy.XPATH, selector) + if dialog_message.is_displayed(): + break + except NoSuchElementException: + continue + + if dialog_message and dialog_message.is_displayed(): + self.logger.info(f"设备 {device_id} 检测到返回确认弹窗") + + # 查找并点击"是"按钮 - 使用多种定位策略 + confirm_selectors = [ + "//android.widget.Button[@text='是' and @resource-id='android:id/button1']", + "//android.widget.Button[@text='是']", + "//android.widget.Button[@resource-id='android:id/button1']", + "//android.widget.Button[contains(@text, '是')]" + ] + + confirm_button = None + for selector in confirm_selectors: + try: + confirm_button = self.driver.find_element(AppiumBy.XPATH, selector) + if confirm_button.is_displayed() and confirm_button.is_enabled(): + break + except NoSuchElementException: + continue + + if confirm_button and confirm_button.is_displayed() and confirm_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击确认弹窗的'是'按钮") + confirm_button.click() + time.sleep(1) + + # 验证弹窗是否消失 + try: + self.driver.find_element(AppiumBy.XPATH, "//android.widget.TextView[@text='是否退出测量界面?']") + self.logger.warning(f"设备 {device_id} 确认弹窗可能未正确关闭") + return False + except NoSuchElementException: + self.logger.info(f"设备 {device_id} 确认弹窗已成功关闭") + return True + else: + self.logger.error(f"设备 {device_id} 未找到可点击的'是'按钮") + return False + + except NoSuchElementException: + # 弹窗未找到,继续等待 + pass + except Exception as e: + self.logger.warning(f"设备 {device_id} 查找确认弹窗时出现异常: {str(e)}") + + time.sleep(1) + + self.logger.error(f"设备 {device_id} 等待返回确认弹窗超时") + return False + + except Exception as e: + self.logger.error(f"设备 {device_id} 处理返回确认弹窗时发生错误: {str(e)}") + return False + + def handle_adjustment_result_dialog(self): + """处理平差结果确认弹窗""" + try: + self.logger.info("开始检测平差结果弹窗") + + # 等待弹窗出现(最多等待5秒) + warning_dialog = WebDriverWait(self.driver, 5).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/parentPanel")) + ) + + # 验证弹窗内容 + alert_title = warning_dialog.find_element(AppiumBy.ID, "android:id/alertTitle") + alert_message = warning_dialog.find_element(AppiumBy.ID, "android:id/message") + + self.logger.info(f"检测到弹窗 - 标题: {alert_title.text}, 消息: {alert_message.text}") + + # 确认是目标弹窗 + if "警告" in alert_title.text and "是否保留测量成果" in alert_message.text: + self.logger.info("确认是平差结果确认弹窗") + + # 点击"是 保留成果"按钮 + yes_button = warning_dialog.find_element(AppiumBy.ID, "android:id/button1") + if yes_button.text == "是 保留成果": + yes_button.click() + self.logger.info("已点击'是 保留成果'按钮") + + # 等待弹窗消失 + WebDriverWait(self.driver, 5).until( + EC.invisibility_of_element_located((AppiumBy.ID, "android:id/parentPanel")) + ) + self.logger.info("弹窗已关闭") + return True + else: + self.logger.error(f"按钮文本不匹配,期望'是 保留成果',实际: {yes_button.text}") + return False + else: + self.logger.warning("弹窗内容不匹配,不是目标弹窗") + return False + + except TimeoutException: + self.logger.info("未检测到平差结果弹窗,继续流程") + return True # 没有弹窗也是正常情况 + except Exception as e: + self.logger.error(f"处理平差结果弹窗时出错: {str(e)}") + return False + + def check_measurement_list(self, device_id): + """ + 检查是否存在测量列表 + + Args: + device_id: 设备ID + + Returns: + bool: 如果不存在测量列表返回True,存在返回False + """ + try: + # 等待线路列表容器出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_LIST_ID)) + ) + self.logger.info("线路列表容器已找到") + + # 如果存在MEASURE_LIST_ID,说明有测量列表,不需要执行后续步骤 + self.logger.info(f"设备 {device_id} 存在测量列表,无需执行后续返回操作") + return False + + except TimeoutException: + # 等待超时,说明没有测量列表 + self.logger.info(f"设备 {device_id} 未找到测量列表,可以继续执行后续步骤") + return True + except Exception as e: + self.logger.error(f"设备 {device_id} 检查测量列表时发生错误: {str(e)}") + return True + + def handle_back_navigation(self, breakpoint_name, device_id): + """ + 完整的返回导航处理流程 + + Args: + breakpoint_name: 断点名称 + device_id: 设备ID + + Returns: + bool: 整个返回导航流程是否成功 + """ + try: + # time.sleep(2) + self.logger.info(f"已点击平差处理按钮,检查是否在测量页面") + + # 检测是否存在测量列表(修正逻辑) + has_measurement_list = self.check_measurement_list(device_id) + if not has_measurement_list: + self.logger.info(f"设备 {device_id} 存在测量列表,重新执行平差流程") + + # 把断点名称给find_keyword + if not self.find_keyword(breakpoint_name): + self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名") + return False + + if not self.handle_measurement_dialog(): + self.logger.error(f"设备 {device_id} 处理测量弹窗失败") + return False + + if not self.check_apply_btn(): + self.logger.error(f"设备 {device_id} 检查平差处理按钮失败") + return False + + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(device_id): + self.logger.error(f"设备 {device_id} 点击平差处理按钮失败") + return False + + self.logger.info(f"重新选择断点并点击平差处理按钮成功") + return True + + else: + self.logger.info(f"不在测量页面,继续执行后续返回操作") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 处理返回导航时发生错误: {str(e)}") + return False + + def execute_back_navigation_steps(self, device_id): + """ + 执行实际的返回导航步骤 + + Args: + device_id: 设备ID + + Returns: + bool: 导航是否成功 + """ + try: + # 1. 首先点击返回按钮 + if not self.click_back_button(device_id): + self.logger.error(f"设备 {device_id} 点击返回按钮失败") + return False + + # 2. 处理返回确认弹窗 + self.logger.info(f"已点击返回按钮,等待处理返回确认弹窗") + if not self.handle_confirmation_dialog(device_id): + self.logger.error(f"设备 {device_id} 处理返回确认弹窗失败") + return False + + + # 3. 验证是否成功返回到上一页面 + time.sleep(1) # 等待页面跳转完成 + + # 可以添加页面验证逻辑,比如检查是否返回到预期的页面 + # 这里可以根据实际应用添加特定的页面元素验证 + + self.logger.info(f"设备 {device_id} 返回导航流程完成") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 执行返回导航步骤时发生错误: {str(e)}") + return False + + def scroll_to_bottom_and_screenshot(self, device_id): + """ + 检测到测量结束后,下滑列表到最底端,点击最后一个spinner,再下滑一次,点击平差处理按钮后截图 + + Args: + device_id: 设备ID + + Returns: + bool: 操作是否成功 + """ + try: + self.logger.info(f"设备 {device_id} 开始执行测量结束后的操作流程") + time.sleep(5) + + # 1. 下滑列表到最底端 + if not self.scroll_list_to_bottom(device_id): + self.logger.error(f"设备 {device_id} 下滑列表到底部失败") + return False + + # 2. 点击最后一个spinner + if not self.click_last_spinner_with_retry(device_id): + self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") + return False + + # 3. 再下滑一次 + if not self.scroll_down_once(device_id): + self.logger.warning(f"设备 {device_id} 再次下滑失败,但继续执行") + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(device_id): + self.logger.error(f"设备 {device_id} 点击平差处理按钮失败") + return False + + # # 5. 在点击平差处理按钮后截图 + # time.sleep(2) # 等待平差处理按钮点击后的界面变化 + # if not self.take_screenshot("after_adjustment_button_click"): + # self.logger.error(f"设备 {device_id} 截图失败") + # return False + + self.logger.info(f"设备 {device_id} 测量结束后操作流程完成") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 执行测量结束后操作时发生错误: {str(e)}") + return False + + def scroll_list_to_bottom(self, device_id, max_swipes=60): + """ + 下滑列表到最底端 + + Args: + device_id: 设备ID + max_swipes: 最大下滑次数 + + Returns: + bool: 是否滑动到底部 + """ + try: + self.logger.info(f"设备 {device_id} 开始下滑列表到底部") + + # 获取列表元素 + list_view = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/auto_data_list") + + same_content_count = 0 + + # 初始化第一次的子元素文本 + initial_child_elements = list_view.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + current_child_texts = "|".join([ + elem.text.strip() for elem in initial_child_elements + if elem.text and elem.text.strip() + ]) + + for i in range(max_swipes): + # 执行下滑操作 + self.driver.execute_script("mobile: scrollGesture", { + 'elementId': list_view.id, + 'direction': 'down', + 'percent': 0.8, + 'duration': 500 + }) + + time.sleep(0.5) + + # 获取滑动后的子元素文本 + new_child_elements = list_view.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + new_child_texts = "|".join([ + elem.text.strip() for elem in new_child_elements + if elem.text and elem.text.strip() + ]) + + # 判断内容是否变化:若连续3次相同,认为到达底部 + if new_child_texts == current_child_texts: + same_content_count += 1 + if same_content_count >= 2: + self.logger.info(f"设备 {device_id} 列表已滑动到底部,共滑动 {i+1} 次") + return True + else: + same_content_count = 0 # 内容变化,重置计数 + current_child_texts = new_child_texts # 更新上一次内容 + + self.logger.debug(f"设备 {device_id} 第 {i+1} 次下滑完成,当前子元素文本: {new_child_texts[:50]}...") # 打印部分文本 + + self.logger.warning(f"设备 {device_id} 达到最大下滑次数 {max_swipes},可能未完全到底部") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 下滑列表时发生错误: {str(e)}") + return False + + def click_last_spinner_with_retry(self, device_id, max_retries=2): + """带重试机制的点击方法""" + for attempt in range(max_retries): + try: + if self.click_last_spinner(device_id): + return True + self.logger.warning(f"设备 {device_id} 第{attempt + 1}次点击失败,准备重试") + time.sleep(1) # 重试前等待 + except Exception as e: + self.logger.error(f"设备 {device_id} 第{attempt + 1}次尝试失败: {str(e)}") + + self.logger.error(f"设备 {device_id} 所有重试次数已用尽") + return False + + def click_last_spinner(self, device_id): + """ + 点击最后一个spinner + + Args: + device_id: 设备ID + + Returns: + bool: 是否成功点击 + """ + try: + self.logger.info(f"设备 {device_id} 查找最后一个spinner") + + # 查找所有的spinner元素 + spinners = self.driver.find_elements(AppiumBy.ID, "com.bjjw.cjgc:id/spinner") + + if not spinners: + self.logger.error(f"设备 {device_id} 未找到任何spinner元素") + return False + + # 获取最后一个spinner + last_spinner = spinners[-1] + + if not (last_spinner.is_displayed() and last_spinner.is_enabled()): + self.logger.error(f"设备 {device_id} 最后一个spinner不可点击") + return False + + # 点击操作 + self.logger.info(f"设备 {device_id} 点击最后一个spinner") + last_spinner.click() + + # 执行额外一次下滑操作 + self.scroll_down_once(device_id) + + max_retries = 3 # 最大重试次数 + retry_count = 0 + wait_timeout = 5 # 增加等待时间到5秒 + + while retry_count < max_retries: + try: + # 确保device_id正确设置,使用全局变量作为备用 + if not hasattr(self, 'device_id') or not self.device_id: + # 优先使用传入的device_id,其次使用全局变量 + self.device_id = device_id if device_id else global_variable.GLOBAL_DEVICE_ID + + # 使用self.device_id,确保有默认值 + actual_device_id = self.device_id if self.device_id else global_variable.GLOBAL_DEVICE_ID + + if not check_session_valid(self.driver, actual_device_id): + self.logger.warning(f"设备 {actual_device_id} 会话无效,尝试重新连接驱动...") + try: + # 使用正确的设备ID进行重连 + new_driver, new_wait = reconnect_driver(actual_device_id, self.driver) + if new_driver: + self.driver = new_driver + self.wait = new_wait + self.logger.info(f"设备 {actual_device_id} 驱动重连成功") + else: + self.logger.error(f"设备 {actual_device_id} 驱动重连失败") + retry_count += 1 + continue + except Exception as e: + self.logger.error(f"设备 {actual_device_id} 驱动重连异常: {str(e)}") + retry_count += 1 + continue + + # 点击spinner(如果是重试,需要重新获取元素) + if retry_count > 0: + spinners = self.driver.find_elements(AppiumBy.CLASS_NAME, "android.widget.Spinner") + if not spinners: + self.logger.error(f"设备 {device_id} 未找到spinner元素") + retry_count += 1 + continue + last_spinner = spinners[-1] + if not (last_spinner.is_displayed() and last_spinner.is_enabled()): + self.logger.error(f"设备 {device_id} spinner不可点击") + retry_count += 1 + continue + self.logger.info(f"设备 {device_id} 重新点击spinner") + last_spinner.click() + # 重试时也执行下滑操作 + self.scroll_down_once(device_id) + + # 等待下拉菜单出现,增加等待时间到5秒 + wait = WebDriverWait(self.driver, wait_timeout) + detail_show = wait.until( + EC.presence_of_element_located((AppiumBy.ID, "com.bjjw.cjgc:id/detailshow")) + ) + + if detail_show.is_displayed(): + self.logger.info(f"设备 {device_id} spinner点击成功,下拉菜单已展开") + return True + else: + self.logger.error(f"设备 {device_id} 下拉菜单未显示") + retry_count += 1 + continue + + except Exception as wait_error: + error_msg = str(wait_error) + self.logger.error(f"设备 {device_id} 等待下拉菜单超时 (第{retry_count+1}次尝试): {error_msg}") + + # 检查是否是连接断开相关的错误 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...") + # if any(keyword in error_msg for keyword in ['socket hang up', 'Could not proxy command']): + # self.logger.warning(f"设备 {device_id} 检测到连接相关错误,尝试重连...") + if not reconnect_driver(self.device_id, self.driver): + self.logger.error(f"设备 {device_id} 驱动重连失败") + + retry_count += 1 + if retry_count < max_retries: + self.logger.info(f"设备 {device_id} 将在1秒后进行第{retry_count+1}次重试") + time.sleep(1) # 等待1秒后重试 + + self.logger.error(f"设备 {device_id} 经过{max_retries}次重试后仍无法展开下拉菜单") + return False + + except Exception as e: + self.logger.error(f"设备 {device_id} 点击最后一个spinner时发生错误: {str(e)}") + return False + + def scroll_down_once(self, device_id): + """ + 再次下滑一次 + + Args: + device_id: 设备ID + + Returns: + bool: 是否成功下滑 + """ + try: + self.logger.info(f"设备 {device_id} 执行额外一次下滑") + + # 获取列表元素 + list_view = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/auto_data_list") + + # 执行下滑操作 + self.driver.execute_script("mobile: scrollGesture", { + 'elementId': list_view.id, + 'direction': 'down', + 'percent': 0.5 + }) + + time.sleep(1) + self.logger.info(f"设备 {device_id} 额外下滑完成") + return True + + except Exception as e: + self.logger.error(f"设备 {device_id} 额外下滑时发生错误: {str(e)}") + return False + + def click_adjustment_button(self, device_id): + """ + 点击平差处理按钮 + + Args: + device_id: 设备ID + + Returns: + bool: 是否成功点击 + """ + try: + self.logger.info(f"设备 {device_id} 查找平差处理按钮") + + # 查找平差处理按钮 + adjustment_button = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/point_measure_btn") + + # 验证按钮文本 + button_text = adjustment_button.text + if "平差处理" not in button_text: + self.logger.warning(f"设备 {device_id} 按钮文本不匹配,期望'平差处理',实际: {button_text}") + + if adjustment_button.is_displayed() and adjustment_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击平差处理按钮") + adjustment_button.click() + time.sleep(3) # 等待平差处理完成 + return True + else: + self.logger.error(f"设备 {device_id} 平差处理按钮不可点击") + return False + + except NoSuchElementException: + self.logger.error(f"设备 {device_id} 未找到平差处理按钮") + return False + except Exception as e: + self.logger.error(f"设备 {device_id} 点击平差处理按钮时发生错误: {str(e)}") + return False + + def add_breakpoint_to_upload_list(self, breakpoint_name, line_num): + """添加平差完成的断点到上传列表和字典""" + if breakpoint_name and breakpoint_name not in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST: + global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST.append(breakpoint_name) + global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[breakpoint_name] = { + 'breakpoint_name': breakpoint_name, + 'line_num': line_num + } + + logging.info(f"成功添加断点 '{breakpoint_name}' 到上传列表") + logging.info(f"断点详细信息: 线路编码={line_num}") + return True + else: + logging.warning(f"断点名为空或已存在于列表中") + return False + + + def handle_confirmation_dialog_save(self, device_id, timeout=2): + """ + 处理确认弹窗,点击"是"按钮 + + Args: + device_id: 设备ID + timeout: 等待弹窗的超时时间 + + Returns: + bool: 是否成功处理弹窗 + """ + # 等待弹窗出现(最多等待2秒) + try: + dialog_message = WebDriverWait(self.driver, timeout).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/content")) + ) + + self.logger.info(f"设备 {device_id} 检测到确认弹窗") + + # 查找并点击"是"按钮 + confirm_button = self.driver.find_element( + AppiumBy.ID, "android:id/button1" + ) + + if confirm_button.is_displayed() and confirm_button.is_enabled(): + self.logger.info(f"设备 {device_id} 点击确认弹窗的'是'按钮") + confirm_button.click() + time.sleep(0.5) + return True + else: + self.logger.error(f"设备 {device_id} '是'按钮不可点击") + return False + + except TimeoutException: + # 超时未找到弹窗,认为没有弹窗,返回成功 + self.logger.info(f"设备 {device_id} 等待 {timeout} 秒未发现确认弹窗,可能没有弹窗,返回成功") + return True + + + def screenshot_page_manager(self, device_id): + """执行截图页面管理操作""" + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + try: + # 检查Appium是否运行,如果没有运行则重启 + if not driver_utils.is_appium_running(4723): + self.logger.warning("Appium服务器未运行,尝试重启...") + if not driver_utils.restart_appium_server(4723): + self.logger.error("重启Appium服务器失败") + return False + # 重新初始化driver + if not reconnect_driver(device_id): + self.logger.error("重新初始化driver失败") + return False + + # 检查GLOBAL_UPLOAD_BREAKPOINT_DICT是否为空,如果为空则初始化一些测试数据 + if not global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT: + self.logger.warning("global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT为空,正在初始化测试数据") + global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT = {'CDWZQ-2标-龙骨湾右线大桥-0-7号墩-平原': 'L156372', 'CDWZQ-2标-蓝家湾特大 桥-31-31-平原': 'L159206'} + + # 创建断点列表的副本,用于重试时重新处理 + breakpoint_names = list(global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT.keys()) + processed_breakpoints = [] + + # 开始循环处理断点 + for breakpoint_name in breakpoint_names: + if breakpoint_name in processed_breakpoints: + continue + line_code = global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[breakpoint_name] + + self.logger.info(f"开始处理要平差的断点 {breakpoint_name}") + + # 把断点名称给find_keyword + if not self.find_keyword(breakpoint_name): + self.logger.error(f"设备 {device_id} 未找到包含 {breakpoint_name} 的文件名") + continue # 继续处理下一个断点 + + if not self.handle_measurement_dialog(): + self.logger.error(f"设备 {device_id} 处理测量弹窗失败") + continue + + if not self.check_apply_btn(): + self.logger.error(f"设备 {device_id} 检查平差处理按钮失败") + self.execute_back_navigation_steps(device_id) + continue + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(device_id): + self.logger.error(f"设备 {device_id} 点击平差处理按钮失败") + continue + + # 检查是否在测量页面,在就重新执行选择断点,滑动列表到底部,点击最后一个spinner, 再下滑一次,点击平差处理按钮平差 + if not self.handle_back_navigation(breakpoint_name, device_id): + self.logger.error(f"{breakpoint_name}平差失败,未截图") + continue + + + # 检测并处理"是 保留成果"弹窗 + if not self.handle_adjustment_result_dialog(): + self.logger.error("处理平差结果弹窗失败") + continue + + # # 平差完成,将断点数据保存到上传列表中 + # if not self.add_breakpoint_to_upload_list(breakpoint_name, line_code): + # self.logger.error(f"设备 {device_id} 保存断点 {breakpoint_name} 到上传列表失败") + # continue + + # # 检查是否在测量页面,在就重新执行选择断点,滑动列表到底部,点击最后一个spinner, 再下滑一次,点击平差处理按钮平差 + # if not self.handle_back_navigation(breakpoint_name, device_id): + # self.logger.error(f"{breakpoint_name}平差失败,未截图") + # continue + + # # 检测并处理"是 保留成果"弹窗 + # if not self.handle_adjustment_result_dialog(): + # self.logger.error("处理平差结果弹窗失败") + # continue + + + # 点击返回按钮并处理弹窗 + if not self.execute_back_navigation_steps(device_id): + self.logger.error(f"设备 {device_id} 处理返回按钮确认失败") + continue + + # 成功处理完一个断点,添加到已处理列表 + processed_breakpoints.append(breakpoint_name) + self.logger.info(f"成功处理断点: {breakpoint_name}") + + # 检查是否所有断点都处理完成 + if len(processed_breakpoints) == len(breakpoint_names): + self.logger.info(f"设备 {device_id} 平差页面操作执行完成") + return True + else: + self.logger.warning(f"设备 {device_id} 部分断点处理失败,已成功处理 {len(processed_breakpoints)}/{len(breakpoint_names)} 个断点") + return True + + except Exception as e: + retry_count += 1 + self.logger.error(f"设备 {device_id} 执行截图页面操作时出错 (重试 {retry_count}/{max_retries}): {str(e)}") + + # 检查是否为连接错误 + if driver_utils.check_connection_error(e): + self.logger.warning("检测到连接错误,尝试重启Appium服务器...") + if not driver_utils.restart_appium_server(4723): + self.logger.error("重启Appium服务器失败") + else: + self.logger.info("Appium服务器重启成功,等待重新连接...") + time.sleep(10) + + # 重新初始化driver + if not reconnect_driver(device_id): + self.logger.error("重新初始化driver失败") + if retry_count >= max_retries: + break + continue + + + if retry_count >= max_retries: + self.logger.error(f"设备 {device_id} 达到最大重试次数,停止执行") + break + + self.logger.info(f"等待10秒后重试...") + time.sleep(10) + + return False + def run_automation_test(self): + # 滑动列表到底部 + if not self.scroll_list_to_bottom(self.device_id): + self.logger.error(f"设备 {self.device_id} 下滑列表到底部失败") + return False + + # 2. 点击最后一个spinner + if not self.click_last_spinner_with_retry(self.device_id): + self.logger.error(f"设备 {self.device_id} 点击最后一个spinner失败") + return False + + # 3. 再下滑一次 + if not self.scroll_down_once(self.device_id): + self.logger.warning(f"设备 {self.device_id} 再次下滑失败,但继续执行") + + # 4. 点击平差处理按钮 + if not self.click_adjustment_button(self.device_id): + self.logger.error(f"设备 {self.device_id} 点击平差处理按钮失败") + return False diff --git a/page_objects/upload_config_page.py b/page_objects/upload_config_page.py new file mode 100644 index 0000000..1ac8ef0 --- /dev/null +++ b/page_objects/upload_config_page.py @@ -0,0 +1,1875 @@ +#上传配置页面 +# \page_objects\test_upload_config_page.py +from appium.webdriver.common.appiumby import AppiumBy +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.common.exceptions import TimeoutException, NoSuchElementException +import logging +import time +import os +import re +import pandas as pd +from datetime import datetime +from typing import Dict, Optional, List + +from page_objects.more_download_page import MoreDownloadPage +from globals.driver_utils import check_session_valid, reconnect_driver, go_main_click_tabber_button # 导入会话检查和重连函数 +import globals.apis as apis +import globals.global_variable as global_variable + +class UploadConfigPage: + def __init__(self, driver, wait, device_id): + self.driver = driver + self.wait = wait + self.logger = logging.getLogger(__name__) + self.more_download_page = MoreDownloadPage(driver, wait,device_id) + self.device_id = device_id + + def go_upload_config_page(self): + """点击img_2_layout(上传页面按钮)""" + try: + # 在执行操作前检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning("会话已失效,尝试重新连接...") + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("重新连接成功") + # 首先获取当前页面信息进行调试 + try: + current_activity = self.driver.current_activity + self.logger.info(f"当前Activity: {current_activity}") + except Exception as e: + self.logger.error(f"获取当前activity时出错: {str(e)}") + + # 尝试返回到主页面(如果不在主页面) + self.logger.info("尝试返回到主页面...") + max_back_presses = 5 # 最多按返回键次数 + back_press_count = 0 + + while back_press_count < max_back_presses: + try: + # 检查是否已经在主页面(通过检查主页面特征元素) + # 先尝试查找上传页面按钮 + try: + main_page_indicator = self.driver.find_element( + AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout" + ) + if main_page_indicator.is_displayed(): + self.logger.info("已在主页面,找到上传按钮") + break + except: + # 未找到主页面元素,继续返回 + pass + + # 按返回键 + self.driver.back() + back_press_count += 1 + self.logger.info(f"已按返回键 {back_press_count} 次") + time.sleep(1) # 等待页面响应 + except Exception as e: + self.logger.error(f"按返回键时出错: {str(e)}") + break + + # 现在尝试点击上传页面按钮 + self.logger.info("尝试点击上传页面按钮") + try: + # 使用较短的等待时间,因为我们需要快速响应 + upload_page_btn = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout")) + ) + upload_page_btn.click() + self.logger.info("已点击img_2_layout,进入上传页面") + time.sleep(2) # 增加等待时间确保页面加载完成 + return True + except TimeoutException: + self.logger.warning("快速查找上传按钮超时,尝试使用更长的等待时间") + # 使用更长的等待时间再次尝试 + upload_page_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout")) + ) + upload_page_btn.click() + self.logger.info("已点击img_2_layout,进入上传页面") + time.sleep(2) # 增加等待时间确保页面加载完成 + return True + except TimeoutException: + self.logger.error("等待上传页面按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"点击上传页面按钮时出错: {str(e)}") + return False + + def check_change_amount_on_page(self): + """直接检查上传配置页面中是否包含变化量属性""" + try: + # 查找页面中是否包含"变化量"文本 + change_amount_elements = self.driver.find_elements( + AppiumBy.XPATH, + "//*[contains(@text, '变化量')]" + ) + + # 如果找到包含"变化量"文本的元素,返回True + if change_amount_elements: + self.logger.info("页面中包含变化量属性") + return True + else: + self.logger.info("页面中未找到变化量属性") + return False + + except Exception as e: + self.logger.error(f"检查变化量属性时出错: {str(e)}") + return False + + def click_upload_by_breakpoint_name(self, breakpoint_name): + """根据断点名称点击上传按钮""" + logging.info(f"需要点击上传按钮,断点名称:{breakpoint_name}") + try: + search_text = "" + # 提取关键部分进行模糊匹配 + if breakpoint_name.endswith('平原') and '-' in breakpoint_name: + # 从右边分割一次,取第一部分(去掉末尾的“-xxx”) + search_text = breakpoint_name.rsplit('-', 1)[0] + self.logger.info(f"处理后搜索文本:{search_text}") + else: + search_text = breakpoint_name + + # 找到包含指定断点名称的itemContainer + item_container_xpath = f"//android.widget.LinearLayout[@resource-id='com.bjjw.cjgc:id/itemContainer']//android.widget.TextView[@resource-id='com.bjjw.cjgc:id/title' and @text='{search_text}']/ancestor::android.widget.LinearLayout[@resource-id='com.bjjw.cjgc:id/itemContainer']" + + # 等待itemContainer出现 + item_container = self.wait.until( + EC.presence_of_element_located((AppiumBy.XPATH, item_container_xpath)) + ) + # self.logger.info(f"找到包含断点 {breakpoint_name} 的itemContainer") + + # 在itemContainer中查找上传按钮 + upload_btn = item_container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/upload_btn" + ) + + # 点击上传按钮 + upload_btn.click() + self.logger.info(f"已点击断点 {breakpoint_name} 的上传按钮") + + # 等待上传操作开始 + time.sleep(3) + + # # 检查上传是否开始 + # try: + # upload_indicator = WebDriverWait(self.driver, 20).until( + # EC.presence_of_element_located((AppiumBy.XPATH, "//*[contains(@text, '上传') or contains(@text, 'Upload')]")) + # ) + # self.logger.info(f"上传操作已开始: {upload_indicator.text}") + # except TimeoutException: + # self.logger.warning("未检测到明确的上传开始提示,但按钮点击已完成") + + return True + + except TimeoutException: + self.logger.error(f"等待断点 {breakpoint_name} 的上传按钮可点击超时") + return False + except Exception as e: + self.logger.error(f"根据断点名称点击上传按钮时出错: {str(e)}") + return False + + def handle_upload_dialog(self): + """处理上传弹窗,点击已同步按钮""" + try: + # 等待弹窗出现 + # time.sleep(2) + + # 检查弹窗是否出现 + dialog_indicators = [ + (AppiumBy.ID, "android:id/alertTitle"), + (AppiumBy.XPATH, "//android.widget.TextView[@text='提示']"), + (AppiumBy.ID, "android:id/message") + ] + + dialog_appeared = False + for by, value in dialog_indicators: + try: + element = self.driver.find_element(by, value) + if element.is_displayed(): + dialog_appeared = True + self.logger.info("检测到上传确认弹窗") + break + except: + continue + + if not dialog_appeared: + self.logger.warning("未检测到上传确认弹窗,可能不需要确认") + return True + + # 点击"已同步"按钮 + synced_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/button2")) + ) + synced_btn.click() + self.logger.info("已点击'已同步'按钮") + + # 等待弹窗消失 + time.sleep(2) + + # 检查弹窗是否消失 + try: + # 检查弹窗是否还存在 + dialog_still_exists = False + for by, value in dialog_indicators: + try: + element = self.driver.find_element(by, value) + if element.is_displayed(): + dialog_still_exists = True + break + except: + continue + + if not dialog_still_exists: + self.logger.info("上传确认弹窗已消失") + return True + else: + self.logger.warning("上传确认弹窗仍然存在") + return False + + except Exception as e: + self.logger.info("上传确认弹窗可能已消失") + return True + + except TimeoutException: + self.logger.error("等待上传对话框可点击超时") + return False + except Exception as e: + self.logger.error(f"处理上传弹窗时出错: {str(e)}") + return False + + def execute_download_operation(self): + """执行下载操作:按返回键并点击img_5_layout""" + try: + # 按一次返回键 + self.driver.back() + self.logger.info("已按返回键") + time.sleep(1) + + # 点击img_5_layout(更多下载按钮) + more_download_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout")) + ) + more_download_btn.click() + self.logger.info("已点击更多下载按钮") + + # 等待页面加载 + time.sleep(1) + + # 调用更多下载页面的方法对象 + try: + logging.info(f"设备开始执行更多下载页面测试") + + # 执行更多下载页面管理操作 + success = self.more_download_page.more_download_page_manager() + + if success: + logging.info(f"设备更多下载页面测试执行成功") + + + + + # 按一次返回键 + self.driver.back() + self.logger.info("已按返回键,返回到更多下载页面,准备点击上传导航按钮") + return True + else: + logging.error(f"设备更多下载页面测试执行失败") + return False + + except Exception as e: + logging.error(f"设备运行更多下载页面测试时出错: {str(e)}") + # self.take_screenshot("more_download_test_error.png") + return False + + + except TimeoutException: + self.logger.error("等待下载操作元素可点击超时") + return False + except Exception as e: + self.logger.error(f"执行下载操作时出错: {str(e)}") + return False + + def get_point_data(self): + """ + 获取三个测点的数据 + """ + point_data = [] + + try: + # 等待页面加载 + time.sleep(1) + + # 方法1: 通过resource-id获取所有测点名称 + point_name_elements = self.driver.find_elements( + AppiumBy.ID, + 'com.bjjw.cjgc:id/improve_point_name' + ) + + # 方法2: 通过text内容获取测点数据元素 + point_value_elements = self.driver.find_elements( + AppiumBy.ID, + 'com.bjjw.cjgc:id/point_values' + ) + + self.logger.info(f"找到 {len(point_name_elements)} 个测点") + self.logger.info(f"找到 {len(point_value_elements)} 个数据元素") + + # 提取每个测点的数据 + for i, (name_element, value_element) in enumerate(zip(point_name_elements, point_value_elements)): + point_info = {} + + # 获取测点名称 + point_name = name_element.text + point_info['point_name'] = point_name + + # 获取测点数据 + point_value = value_element.text + point_info['point_value'] = point_value + + # 解析详细数据 + try: + # 解析数据格式: "本期:13679.07623m; \n上期:13679.07621m; \n变化量:0.02mm; \n测量时间:2025-10-10 15:47:25.049" + data_parts = point_value.split(';') + parsed_data = {} + + for part in data_parts: + part = part.strip() + if '本期:' in part: + parsed_data['current_value'] = part.replace('本期:', '').strip() + elif '上期:' in part: + parsed_data['previous_value'] = part.replace('上期:', '').strip() + elif '变化量:' in part: + parsed_data['change_amount'] = part.replace('变化量:', '').strip() + elif '测量时间:' in part: + parsed_data['measurement_time'] = part.replace('测量时间:', '').strip() + + point_info['parsed_data'] = parsed_data + + except Exception as e: + self.logger.warning(f"解析数据时出错: {e}") + point_info['parsed_data'] = {} + + point_data.append(point_info) + + self.logger.info(f"测点 {i+1}: {point_name}") + # self.logger.info(f"完整数据: {point_value}") + + except Exception as e: + self.logger.error(f"获取数据时出错: {e}") + + return point_data + + def get_specific_point_data(self, point_name): + """ + 获取特定测点的数据 + """ + try: + # 通过文本内容查找特定测点 + point_name_element = self.driver.find_element( + AppiumBy.XPATH, + f'//android.widget.TextView[@resource-id="com.bjjw.cjgc:id/improve_point_name" and @text="{point_name}"]' + ) + + # 找到对应的数据元素 - 可能需要根据实际结构调整XPath + point_value_element = point_name_element.find_element( + AppiumBy.XPATH, + './following::android.widget.TextView[@resource-id="com.bjjw.cjgc:id/point_values"]' + ) + + return point_value_element.text + + except Exception as e: + self.logger.error(f"获取特定测点 {point_name} 数据时出错: {e}") + return None + + def swipe_up(self, start_y=1500, end_y=300, duration=500): + """ + 从指定起始Y坐标滑动到结束Y坐标(向上滑动页面) + + 参数: + start_y: 起始Y坐标 (默认1500) + end_y: 结束Y坐标 (默认300) + duration: 滑动持续时间(毫秒) (默认500) + """ + try: + # 获取屏幕尺寸 + window_size = self.driver.get_window_size() + screen_width = window_size['width'] + + # 计算X坐标(屏幕中间) + x = screen_width // 2 + + # 执行滑动操作 + self.driver.swipe(x, start_y, x, end_y, duration) + self.logger.info(f"页面已从Y:{start_y}滑动到Y:{end_y}") + + # 滑动后等待页面稳定 + time.sleep(1) + return True + + except Exception as e: + self.logger.error(f"滑动页面时出错: {e}") + return False + + def swipe_down(self, start_y=175, end_y=1310, duration=500): + """ + 从指定起始Y坐标滑动到结束Y坐标(向下滑动页面) + + 参数: + start_y: 起始Y坐标 (默认407) + end_y: 结束Y坐标 (默认1617) + duration: 滑动持续时间(毫秒) (默认500) + """ + try: + # 获取屏幕尺寸 + window_size = self.driver.get_window_size() + screen_width = window_size['width'] + + # 计算X坐标(屏幕中间) + x = screen_width // 2 + + # 执行滑动操作(向下滑动) + self.driver.swipe(x, start_y, x, end_y, duration) + self.logger.info(f"页面已从Y:{start_y}滑动到Y:{end_y}(向下滑动)") + + # 滑动后等待页面稳定 + time.sleep(1) + return True + + except Exception as e: + self.logger.error(f"向下滑动页面时出错: {e}") + return False + + def is_on_upload_config_page(self): + """通过"保存上传"按钮来确定是否在上传配置页面""" + try: + # 使用"保存上传"按钮的resource-id来检查 + save_upload_btn_locator = (AppiumBy.ID, "com.bjjw.cjgc:id/improve_save_btn") + self.wait.until(EC.presence_of_element_located(save_upload_btn_locator)) + self.logger.info("已确认在上传配置页面") + return True + except TimeoutException: + self.logger.warning("未找到保存上传按钮,不在上传配置页面") + return False + except Exception as e: + self.logger.error(f"检查上传配置页面时发生意外错误: {str(e)}") + return False + + def collect_all_point_data(self, results_dir): + """循环滑动收集所有测点数据,直到没有新数据出现""" + all_point_data = [] + seen_point_names = set() # 用于跟踪已经见过的测点名称 + max_scroll_attempts = 20 # 最大滑动次数,防止无限循环 + scroll_attempt = 0 + + self.logger.info("开始循环滑动收集所有测点数据...") + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次尝试获取数据...") + + # 获取当前屏幕的测点数据 + current_point_data = self.get_point_data() + + if not current_point_data: + self.logger.info("当前屏幕没有测点数据,停止滑动") + break + + # 统计新发现的测点 + new_points_count = 0 + for point in current_point_data: + point_name = point.get('point_name') + if point_name and point_name not in seen_point_names: + # 新测点,添加到结果集 + all_point_data.append(point) + seen_point_names.add(point_name) + new_points_count += 1 + + self.logger.info(f"本次获取到 {len(current_point_data)} 个测点,其中 {new_points_count} 个是新测点") + + # 如果没有新数据,停止滑动 + if new_points_count == 0: + self.logger.info("没有发现新测点,停止滑动") + break + + # 滑动到下一页 + self.logger.info("滑动到下一页...") + if not self.swipe_up(): + self.logger.warning("滑动失败,停止收集") + break + + # 等待页面稳定 + time.sleep(1) + + self.logger.info(f"数据收集完成,共获取 {len(all_point_data)} 个测点数据") + return all_point_data + + def collect_check_all_point_data(self, max_variation): + """循环滑动收集所有测点数据,直到没有新数据出现""" + all_point_data = [] + seen_point_names = set() # 用于跟踪已经见过的测点名称 + max_scroll_attempts = 100 # 最大滑动次数,防止无限循环 + scroll_attempt = 0 + + self.logger.info("开始循环滑动收集所有测点数据...") + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次尝试获取数据...") + + # 获取当前屏幕的测点数据 + current_point_data = self.get_point_data() + + if not current_point_data: + self.logger.info("当前屏幕没有测点数据,停止滑动") + break + + # 统计新发现的测点 + new_points_count = 0 + for point in current_point_data: + point_name = point.get('point_name') + if point_name and point_name not in seen_point_names: + # 新测点,添加到结果集 + all_point_data.append(point) + seen_point_names.add(point_name) + new_points_count += 1 + + self.logger.info(f"本次获取到 {len(current_point_data)} 个测点,其中 {new_points_count} 个是新测点") + + # 如果没有新数据,停止滑动 + if new_points_count == 0: + self.logger.info("没有发现新测点,停止滑动") + break + + # 滑动到下一页 + self.logger.info("滑动到下一页...") + if not self.swipe_up(): + self.logger.warning("滑动失败,停止收集") + break + + # 等待页面稳定 + time.sleep(0.2) + + self.logger.info(f"数据收集完成,共获取 {len(all_point_data)} 个测点数据") + + # 直接比对每个测点的变化量 + if max_variation is None: + self.logger.error("获取用户最大变化量失败") + max_variation = 2 + # return False + + self.logger.info(f"开始比对测点变化量,最大允许变化量: {max_variation}mm") + + for i, point in enumerate(all_point_data, 1): + point_name = point.get('point_name', '未知') + point_value = point.get('point_value', '') + + # 从完整数据中提取变化量(格式如:变化量:-0.67mm;) + change_amount_match = re.search(r'变化量:([-\d.]+)mm', point_value) + if change_amount_match: + try: + change_amount = float(change_amount_match.group(1)) + # self.logger.info(f"测点 {point_name} 变化量: {change_amount}mm, 最大允许变化量: {max_variation}mm") + + # 比较绝对值,因为变化量可能是负数 + if abs(change_amount) > max_variation: + self.logger.error(f"测点 {point_name} 变化量 {change_amount}mm 超过最大允许值 {max_variation}mm") + return False + except ValueError as e: + self.logger.error(f"解析测点 {point_name} 的变化量失败: {str(e)},原始数据: {point_value}") + return False + else: + self.logger.error(f"在测点 {point_name} 的数据中未找到变化量信息,原始数据: {point_value}") + return False + + self.logger.info(f"所有测点变化量均在允许范围内(≤{max_variation}mm)") + return True + + # def _load_user_data(self): + # """加载用户数据从Excel文件""" + # try: + # # 默认路径:当前脚本的上一级目录下的"上传人员信息.xlsx" + # current_dir = os.path.dirname(os.path.abspath(__file__)) + # parent_dir = os.path.dirname(current_dir) + # excel_path = os.path.join(parent_dir, "上传人员信息.xlsx") + + # if not os.path.exists(excel_path): + # logging.error(f"Excel文件不存在: {excel_path}") + # return False + + # # 读取Excel文件 + # df = pd.read_excel(excel_path, sheet_name='Sheet1') + + # # 处理合并单元格 - 前向填充标段列 + # df['标段'] = df['标段'].fillna(method='ffill') + + # # 清理数据:去除空行和无效数据 + # df = df.dropna(subset=['测量人员信息']) + # df = df[df['测量人员信息'].str.strip() != ''] + + # # 创建姓名到身份证的映射 + + # for _, row in df.iterrows(): + # name = row['测量人员信息'] + # id_card = str(row.iloc[4]).strip() # 第5列是身份证号 + + # # 处理身份证号格式(如果是浮点数转为整数) + # if id_card.endswith('.0'): + # id_card = id_card[:-2] + + # global_variable.GLOBAL_NAME_TO_ID_MAP[name] = id_card + + # global_variable.GLOBAL_NAME_TO_ID_MAP = df + # logging.info(f"成功加载用户数据,共 {len(df)} 条记录,{len(global_variable.GLOBAL_NAME_TO_ID_MAP)} 个唯一姓名") + # return True + + # except Exception as e: + # logging.error(f"加载用户数据失败: {str(e)}") + # return False + + def _load_user_data(self): + """加载用户数据从Excel文件,只提取名字和身份证到字典""" + try: + # 默认路径:当前脚本的上一级目录下的"上传人员信息.xlsx" + current_dir = os.path.dirname(os.path.abspath(__file__)) + parent_dir = os.path.dirname(current_dir) + excel_path = os.path.join(parent_dir, "上传人员信息.xlsx") + + if not os.path.exists(excel_path): + logging.error(f"Excel文件不存在: {excel_path}") + return False + + # 读取Excel文件 + df = pd.read_excel(excel_path, sheet_name='Sheet1') + + # 处理合并单元格 - 前向填充标段列 + # df['标段'] = df['标段'].fillna(method='ffill') + df['标段'] = df['标段'].ffill() + # 清理数据:去除空行和无效数据 + df = df.dropna(subset=['测量人员信息']) + df = df[df['测量人员信息'].str.strip() != ''] + + # 创建姓名到身份证的映射字典 + name_id_map = {} + + for _, row in df.iterrows(): + name = str(row['测量人员信息']).strip() + # 第5列是身份证号(索引为4) + id_card = str(row.iloc[4]).strip() if pd.notna(row.iloc[4]) else "" + + # 处理身份证号格式(如果是浮点数转为整数) + if id_card.endswith('.0'): + id_card = id_card[:-2] + + # 只将有效的姓名和身份证号添加到字典 + if name and id_card and len(id_card) >= 15: # 身份证号至少15位 + name_id_map[name] = id_card + else: + logging.warning(f"跳过无效数据: 姓名='{name}', 身份证='{id_card}'") + + # 将字典保存到全局变量 + global_variable.GLOBAL_NAME_TO_ID_MAP = name_id_map + + logging.info(f"成功加载用户数据,共 {len(df)} 条记录,{len(name_id_map)} 个有效姓名-身份证映射") + + # 打印前几个映射用于调试 + sample_names = list(name_id_map.keys())[:5] + for name in sample_names: + logging.debug(f"映射示例: {name} -> {name_id_map[name]}") + + return True + + except Exception as e: + logging.error(f"加载用户数据失败: {str(e)}") + return False + + def get_first_sjname_and_id(self, linecode: str, work_conditions: Dict) -> Optional[Dict]: + """ + 获取线路的第一个数据员姓名和身份证号 + + Args: + linecode: 线路编码 + + Returns: + 返回字典: {"name": 姓名, "id_card": 身份证号} + """ + if not work_conditions: + logging.error(f"无法获取线路 {linecode} 的工况信息") + return None + + # 直接取第一个测点的数据员 + first_point_info = next(iter(work_conditions.values())) + sjname = first_point_info.get('sjName') + + if not sjname: + logging.error("第一个测点没有数据员信息") + return None + + logging.info(f"使用第一个数据员: {sjname}") + + # 获取身份证号码 + id_card = global_variable.GLOBAL_NAME_TO_ID_MAP.get(sjname) + logging.info(f"id_card: {id_card}") + if not id_card: + logging.error(f"未找到数据员 {sjname} 对应的身份证号") + return None + + return {"name": sjname, "id_card": id_card} + + + # def set_all_points_and_fill_form(self, results_dir, name, user_id, condition_dict): + # """点击设置所有测点按钮并填写表单""" + # try: + # self.logger.info("开始设置所有测点并填写表单...") + + # # 点击"设置所有测点"按钮 + # set_all_points_btn = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/display_setallpoint_btn")) + # ) + # set_all_points_btn.click() + # self.logger.info("已点击'设置所有测点'按钮") + + # # 等待表单加载 + # time.sleep(1) + + # # 填写司镜人员姓名 + # name_input = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/sname")) + # ) + # name_input.clear() + # name_input.send_keys(name) + # self.logger.info(f"已填写司镜人员姓名: {name}") + + # # 填写司镜人员身份证号 + # id_card_input = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/scard")) + # ) + # id_card_input.clear() + # id_card_input.send_keys(user_id) + # self.logger.info(f"已填写司镜人员身份证号: {user_id}") + + # # 选择测点状态 + # if not self._select_point_status("正常"): + # self.logger.error("选择测点状态失败") + # return False + + + # # 选择工况信息 + # if not self._select_work_condition(condition_dict): + # self.logger.error("选择工况信息失败") + # return False + + # # 等待操作完成 + # time.sleep(1) + + # # 检查操作结果 + # try: + # # 检查是否有成功提示或错误提示 + # # 这里可以根据实际应用的上传反馈机制进行调整 + # success_indicator = WebDriverWait(self.driver, 10).until( + # EC.presence_of_element_located((AppiumBy.XPATH, "//*[contains(@text, '成功') or contains(@text, '完成')]")) + # ) + # self.logger.info(f"操作完成: {success_indicator.text}") + # except TimeoutException: + # self.logger.warning("未检测到明确的操作成功提示,但操作已完成") + + # return True + + # except TimeoutException: + # self.logger.error("等待设置所有测点表单元素可点击超时") + # return False + # except Exception as e: + # self.logger.error(f"设置所有测点并填写表单时出错: {str(e)}") + # # 保存错误截图 + # error_screenshot_file = os.path.join( + # results_dir, + # f"form_error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + # ) + # self.driver.save_screenshot(error_screenshot_file) + # self.logger.info(f"错误截图已保存: {error_screenshot_file}") + # return False + + def _select_point_status(self, status="正常"): + logging.info(f"开始选择测点状态: {status}") + """选择测点状态""" + try: + # 点击"选择测点状态"按钮 + status_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/all_point_pstate_sp")) + ) + status_button.click() + + # 选择指定的状态 + status_option = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, f"//android.widget.TextView[@resource-id='android:id/text1' and @text='{status}']")) + ) + status_option.click() + return True + except TimeoutException: + self.logger.error("等待测点状态选择超时") + return False + except Exception as e: + self.logger.error(f"选择测点状态时出错: {str(e)}") + return False + + + def set_all_points_and_fill_form(self, results_dir, name, user_id, main_condition_dict, minor_conditions_list): + """点击设置所有测点按钮并填写表单""" + try: + self.logger.info("开始设置所有测点并填写表单...") + + # 点击"设置所有测点"按钮 + set_all_points_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/display_setallpoint_btn")) + ) + set_all_points_btn.click() + self.logger.info("已点击'设置所有测点'按钮") + + # 等待表单加载 + time.sleep(1) + + # 填写司镜人员姓名 + name_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/sname")) + ) + name_input.clear() + name_input.send_keys(name) + self.logger.info(f"已填写司镜人员姓名: {name}") + + # 填写司镜人员身份证号 + id_card_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/scard")) + ) + id_card_input.clear() + id_card_input.send_keys(user_id) + self.logger.info(f"已填写司镜人员身份证号: {user_id}") + + # 选择测点状态 + if not self._select_point_status("正常"): + self.logger.error("选择测点状态失败") + return False + + # 选择工况信息 - 现在传入两个参数 + if not self._select_work_condition(main_condition_dict, minor_conditions_list): + self.logger.error("选择工况信息失败") + return False + + # 等待操作完成 + time.sleep(1) + + # 检查操作结果 + try: + success_indicator = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.XPATH, "//*[contains(@text, '成功') or contains(@text, '完成')]")) + ) + self.logger.info(f"操作完成: {success_indicator.text}") + except TimeoutException: + self.logger.warning("未检测到明确的操作成功提示,但操作已完成") + + return True + except TimeoutException: + self.logger.error("等待设置所有测点表单元素可点击超时") + return False + except Exception as e: + self.logger.error(f"设置所有测点并填写表单时出错: {str(e)}") + return False + + def _select_work_condition(self, main_condition_dict: Dict[str, List[str]], minor_conditions_list: List[Dict]): + """根据主要工况字典和次要工况列表选择工况信息""" + self.logger.info("开始选择工况信息") + self.logger.info(f"主要工况: {main_condition_dict}") + self.logger.info(f"次要工况数量: {len(minor_conditions_list)}") + + # 工点类型编码与界面控件ID的映射 + work_type_mapping = { + "1": { # 隧道 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_suidao", + "name": "隧道" + }, + "2": { # 区间路基 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_luji", + "name": "路基" + }, + "3": { # 桥 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_qiaoliang", + "name": "梁桥" + }, + "4": { # 涵洞 + "button_id": "com.bjjw.cjgc:id/all_point_workinfo_sp_handong", + "name": "涵洞" + } + } + + try: + success_count = 0 + + # 第一步:为每个工点类型选择主要工况 + for work_type, workinfo_names in main_condition_dict.items(): + work_type_str = str(work_type).strip() + if work_type_str not in work_type_mapping: + self.logger.warning(f"未知的工点类型编码: {work_type_str},跳过") + continue + + mapping = work_type_mapping[work_type_str] + button_id = mapping["button_id"] + work_type_name = mapping["name"] + + if workinfo_names: + workinfo_name = workinfo_names[0] # 主要工况 + self.logger.info(f"为{work_type_name}({work_type_str})选择主要工况: {workinfo_name}") + + try: + # 点击工况选择按钮 + condition_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, button_id)) + ) + condition_button.click() + self.logger.info(f"成功点击{work_type_name}工况选择按钮") + + # 选择主要的工况选项 + if self._select_condition_option(workinfo_name): + self.logger.info(f"成功为{work_type_name}选择主要工况: {workinfo_name}") + success_count += 1 + else: + self.logger.warning(f"未能为{work_type_name}选择主要工况: {workinfo_name}") + + except TimeoutException: + self.logger.error(f"等待{work_type_name}工况选择按钮超时") + except Exception as e: + self.logger.error(f"点击{work_type_name}工况按钮时出错: {str(e)}") + + # 第二步:如果有次要工况,滑动页面处理 + if minor_conditions_list: + self.logger.info(f"开始处理 {len(minor_conditions_list)} 个次要工况") + minor_success_count = self._handle_minor_work_conditions(minor_conditions_list) + self.logger.info(f"次要工况处理完成: 成功 {minor_success_count}/{len(minor_conditions_list)} 个") + else: + self.logger.info("没有次要工况需要处理") + + self.logger.info(f"工况选择完成: 主要工况成功{success_count}/{len(main_condition_dict)}个") + return success_count > 0 # 只要有一个主要工况成功就返回True + + except Exception as e: + self.logger.error(f"选择工况信息时发生未知错误: {str(e)}") + return False + + def _handle_minor_work_conditions(self, minor_conditions_list: List[Dict]) -> int: + """处理次要工况:滑动页面,找到对应测点并设置工况""" + success_count = 0 + processed_points = set() # 记录已处理的测点,避免重复处理 + logging.info(f"要处理的次要工况:{minor_conditions_list}") + + # 提取所有需要处理的测点ID + target_point_ids = {condition['point_id'] for condition in minor_conditions_list} + self.logger.info(f"目标测点ID: {list(target_point_ids)}") + + # 点击"关闭设置所有"按钮 + close_set_all_points_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/display_setallpoint_btn")) + ) + close_set_all_points_btn.click() + self.logger.info("已点击'关闭设置所有测点'按钮") + + try: + max_scroll_attempts = 20 # 最大滑动次数防止无限循环 + scroll_attempt = 0 + previous_points = set() # 记录上一页的测点 + remaining_conditions = minor_conditions_list.copy() + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次滑动处理次要工况...") + + # 获取当前页面的测点 + current_points = set(self.collect_points_on_page()) + self.logger.info(f"当前页面共找到 {len(current_points)} 个测点: {list(current_points)}") + + # 检查是否有新测点 + if current_points == previous_points: + self.logger.info("当前页面没有新的测点,停止滑动") + break + + # 检查当前页面是否有目标测点 + current_target_points = current_points & target_point_ids + if not current_target_points: + self.logger.info(f"当前页面没有目标测点,继续滑动查找...") + # 滑动到下一页 + if not self.swipe_down(): + self.logger.warning("滑动失败,停止处理") + break + # 等待页面稳定 + time.sleep(1) + previous_points = current_points + continue + + self.logger.info(f"当前页面找到目标测点: {list(current_target_points)}") + + # 如果当前测点列表的测点名,在minor_conditions_list中,就点击选择测点对应工况 + + # 处理当前页面的次要工况 + page_success_count = self._process_minor_conditions_on_current_page( + current_points, remaining_conditions, processed_points + ) + success_count += page_success_count + + # 更新剩余待处理的工况列表 + remaining_conditions = [ + condition for condition in remaining_conditions + if condition['point_id'] not in processed_points + ] + + self.logger.info(f"剩余待处理工况: {len(remaining_conditions)} 个") + if remaining_conditions: + remaining_points = [cond['point_id'] for cond in remaining_conditions] + self.logger.info(f"剩余测点: {remaining_points}") + + # 更新已处理的测点记录 + previous_points = current_points + + # 如果所有次要工况都已处理完成,提前退出 + if len(processed_points) >= len(minor_conditions_list): + self.logger.info(f"所有 {len(minor_conditions_list)} 个次要工况已处理完成") + break + + # 滑动到下一页 + self.logger.info("滑动到下一页继续查找...") + if not self.swipe_down(): + self.logger.warning("滑动失败,停止处理") + break + + # 等待页面稳定 + time.sleep(1) + + self.logger.info(f"次要工况处理完成: 成功 {success_count}/{len(minor_conditions_list)} 个") + return success_count + + except Exception as e: + self.logger.error(f"处理次要工况时出错: {str(e)}") + return success_count + + def _process_minor_conditions_on_current_page(self, current_points: set, minor_conditions_list: List[Dict], processed_points: set) -> int: + """处理当前页面上的次要工况""" + page_success_count = 0 + + try: + self.logger.info("开始处理当前页面上的次要工况...") + + # 1. 先定位所有测点的根容器 + point_containers = self.driver.find_elements( + AppiumBy.ID, + "com.bjjw.cjgc:id/layout_popup_top" + ) + self.logger.info(f"当前页面找到 {len(point_containers)} 个测点容器") + + if not point_containers: + self.logger.warning("当前页面未找到任何测点容器") + return 0 + + # 2. 构建当前页面测点的映射关系:测点名称 -> 容器元素 + point_container_map = {} + for container in point_containers: + try: + name_element = container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/improve_point_name" + ) + point_name = name_element.text.strip() + if point_name and point_name in current_points: + point_container_map[point_name] = container + self.logger.debug(f"映射测点: {point_name}") + except NoSuchElementException: + continue + except Exception as e: + self.logger.debug(f"解析测点容器时出错: {str(e)}") + continue + + # 3. 遍历需要处理的次要工况列表 + for minor_condition in minor_conditions_list: + point_id = minor_condition['point_id'] + workinfoname = minor_condition['workinfoname'] + work_type = minor_condition['work_type'] + + # 检查是否已经处理过 + if point_id in processed_points: + self.logger.debug(f"测点 {point_id} 已处理过,跳过") + continue + + # 检查是否在当前页面 + if point_id not in current_points: + self.logger.debug(f"测点 {point_id} 不在当前页面") + continue + + # 检查是否有对应的容器 + if point_id not in point_container_map: + self.logger.warning(f"测点 {point_id} 在当前页面但未找到对应容器") + continue + + try: + self.logger.info(f"开始处理测点 {point_id} 的次要工况: {workinfoname}") + + # 4. 在当前测点的容器内查找工况选择按钮 + container = point_container_map[point_id] + workinfo_button = container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/point_workinfo_sp" + ) + + # 验证按钮是否可见和可用 + if workinfo_button.is_displayed() and workinfo_button.is_enabled(): + workinfo_button.click() + self.logger.info(f"已点击测点 {point_id} 的工况选择按钮") + + # 选择对应的工况选项 + if self._select_minor_conditions_option(workinfoname, work_type): + page_success_count += 1 + processed_points.add(point_id) + self.logger.info(f"成功为测点 {point_id} 设置次要工况: {workinfoname}") + else: + self.logger.warning(f"为测点 {point_id} 选择工况选项失败") + else: + self.logger.warning(f"测点 {point_id} 的工况选择按钮不可用") + + except NoSuchElementException: + self.logger.warning(f"未找到测点 {point_id} 的工况选择按钮") + except Exception as e: + self.logger.error(f"处理测点 {point_id} 时出错: {str(e)}") + + self.logger.info(f"当前页面成功处理 {page_success_count} 个测点的次要工况") + return page_success_count + + + except Exception as e: + self.logger.error(f"处理当前页面次要工况时发生异常: {str(e)}") + return page_success_count + + def _select_minor_conditions_option(self, option_name: str, work_type_name: str) -> bool: + """根据工况名称选择对应的下拉列表中的选项""" + try: + self.logger.info(f"开始选择次要工况选项: {option_name}") + + # 方法1: 通过文本精确匹配 + try: + option_xpath = f"//android.widget.TextView[@text='{option_name}']" + option_element = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, option_xpath)) + ) + option_element.click() + self.logger.info(f"通过文本匹配成功选择工况: {option_name}") + return True + except TimeoutException: + self.logger.debug(f"通过文本 '{option_name}' 未找到工况选项") + + # 方法2: 通过列表项ID查找 + try: + list_items = self.driver.find_elements(AppiumBy.ID, "android:id/text1") + for item in list_items: + if item.text == option_name: + item.click() + self.logger.info(f"通过列表项成功选择工况: {option_name}") + return True + except Exception as e: + self.logger.debug(f"通过列表项查找失败: {str(e)}") + + # 方法3: 滑动查找 + max_scroll_attempts = 3 + for attempt in range(max_scroll_attempts): + try: + option_element = self.driver.find_element( + AppiumBy.XPATH, + f"//android.widget.TextView[@text='{option_name}']" + ) + option_element.click() + self.logger.info(f"通过滑动后查找成功选择工况: {option_name}") + return True + except NoSuchElementException: + self.logger.debug(f"第 {attempt + 1} 次滑动查找未找到选项: {option_name}") + # 执行滑动 + self._scroll_condition_options() + except Exception as e: + self.logger.debug(f"滑动查找时出错: {str(e)}") + break + + self.logger.error(f"所有方法都无法找到工况选项: {option_name}") + return False + + except TimeoutException: + self.logger.error(f"等待工况选项可点击超时: {option_name}") + return False + except Exception as e: + self.logger.error(f"选择工况选项时出错: {str(e)}") + return False + + def _scroll_condition_options(self): + """滑动工况选项列表""" + try: + # 获取屏幕尺寸 + window_size = self.driver.get_window_size() + start_x = window_size['width'] * 0.5 + start_y = window_size['height'] * 0.7 + end_y = window_size['height'] * 0.3 + + # 执行滑动 + self.driver.swipe(start_x, start_y, start_x, end_y, 500) + self.logger.debug("执行滑动操作查找更多工况选项") + time.sleep(1) # 等待滑动完成 + + except Exception as e: + self.logger.error(f"滑动工况选项列表时出错: {str(e)}") + + def collect_points_on_page(self) -> List[str]: + """ + 收集当前页面中存在的测点ID列表 + 要求:必须同时存在"com.bjjw.cjgc:id/improve_point_name"和"com.bjjw.cjgc:id/point_workinfo_sp"才返回 + """ + point_ids = [] + try: + self.logger.info("开始收集当前页面中的测点ID...") + + # 1. 先定位所有测点的根容器(RelativeLayout),确保在同一容器内查找子元素 + point_containers = self.driver.find_elements( + AppiumBy.ID, + "com.bjjw.cjgc:id/layout_popup_top" # 测点根容器的resource-id + ) + self.logger.info(f"找到 {len(point_containers)} 个测点根容器") + + for container in point_containers: + try: + # 2. 在当前根容器内查找测点名称元素 + name_element = container.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/improve_point_name" + ) + point_name = name_element.text + if not point_name: + self.logger.debug("测点名称为空,跳过") + continue + + # 3. 在当前根容器内查找工况按钮(按实际层级定位) + # 层级:RelativeLayout -> LinearLayout -> LinearLayout -> Button + workinfo_button = container.find_element( + AppiumBy.XPATH, + ".//android.widget.LinearLayout/android.widget.LinearLayout/android.widget.Button[@resource-id='com.bjjw.cjgc:id/point_workinfo_sp']" + ) + + # 4. 验证按钮是否可见 + if workinfo_button.is_displayed(): + point_ids.append(point_name) + self.logger.debug(f"找到有效测点: {point_name}") + + except NoSuchElementException as e: + # 若名称或工况按钮不存在,跳过当前容器 + self.logger.debug(f"测点容器中缺少必要元素: {str(e)}") + continue + except Exception as e: + self.logger.debug(f"解析测点容器时出错: {str(e)}") + continue + + self.logger.info(f"当前页面共找到 {len(point_ids)} 个有效测点: {point_ids}") + return point_ids + + except Exception as e: + self.logger.error(f"收集页面测点ID时出错: {str(e)}") + return [] + + def collect_all_points_on_page(self) -> List[Dict]: + """滑动收集页面中所有测点的信息""" + all_points = [] + seen_point_names = set() + max_scroll_attempts = 50 + scroll_attempt = 0 + + self.logger.info("开始滑动收集页面中所有测点...") + + while scroll_attempt < max_scroll_attempts: + scroll_attempt += 1 + self.logger.info(f"第 {scroll_attempt} 次滑动收集...") + + # 获取当前屏幕的测点 + current_points = self._get_current_screen_points_detail() + + if not current_points: + self.logger.info("当前屏幕没有测点数据") + break + + # 添加新发现的测点 + new_points_count = 0 + for point in current_points: + point_name = point.get('point_name') + if point_name and point_name not in seen_point_names: + all_points.append(point) + seen_point_names.add(point_name) + new_points_count += 1 + + self.logger.info(f"本次获取到 {len(current_points)} 个测点,其中 {new_points_count} 个是新测点") + + if new_points_count == 0: + self.logger.info("没有发现新测点,停止滑动") + break + + # 滑动到下一页 + if not self.swipe_up(): + self.logger.warning("滑动失败,停止收集") + break + + time.sleep(0.5) + + self.logger.info(f"共收集到 {len(all_points)} 个测点的详细信息") + return all_points + + def _get_current_screen_points_detail(self) -> List[Dict]: + """获取当前屏幕测点的详细信息""" + points = [] + + try: + # 查找所有测点容器 + point_containers = self.driver.find_elements( + AppiumBy.XPATH, + "//android.widget.LinearLayout[@resource-id='com.bjjw.cjgc:id/layout_popup_top']" + ) + + for container in point_containers: + try: + point_info = {} + + # 获取测点名称(作为point_id的替代) + name_element = container.find_element( + AppiumBy.ID, "com.bjjw.cjgc:id/improve_point_name" + ) + point_name = name_element.text + point_info['point_name'] = point_name + point_info['point_id'] = point_name # 使用名称作为ID,或者根据实际情况调整 + + # 获取测点元素引用,用于后续操作 + point_info['element'] = container + + # 获取当前工况信息 + try: + workinfo_element = container.find_element( + AppiumBy.ID, "com.bjjw.cjgc:id/point_workinfo_sp" + ) + point_info['current_workinfo'] = workinfo_element.text + point_info['workinfo_element'] = workinfo_element + except NoSuchElementException: + point_info['current_workinfo'] = "" + point_info['workinfo_element'] = None + + points.append(point_info) + + except Exception as e: + self.logger.debug(f"解析单个测点信息时出错: {str(e)}") + continue + + except Exception as e: + self.logger.error(f"获取当前屏幕测点详细信息时出错: {str(e)}") + + return points + + def _set_single_point_work_condition(self, point_data: Dict, workinfo_name: str, work_type: str) -> bool: + """为单个测点设置工况信息""" + try: + point_name = point_data.get('point_name') + self.logger.info(f"开始为测点 {point_name} 设置工况: {workinfo_name}") + + # 使用保存的元素引用点击工况选择按钮 + workinfo_element = point_data.get('workinfo_element') + if workinfo_element: + workinfo_element.click() + time.sleep(1) # 等待选项弹出 + + # 选择指定的工况 + if self._select_condition_option(workinfo_name): + self.logger.info(f"成功为测点 {point_name} 设置工况: {workinfo_name}") + return True + else: + self.logger.warning(f"为测点 {point_name} 选择工况选项失败") + return False + else: + self.logger.warning(f"未找到测点 {point_name} 的工况选择按钮") + return False + + except Exception as e: + self.logger.error(f"为测点 {point_name} 设置工况时出错: {str(e)}") + return False + + def _select_condition_option(self, condition_name: str) -> bool: + """选择具体的工况选项 + + Args: + condition_name: 工况名称 + + Returns: + bool: 是否选择成功 + """ + try: + self.logger.info(f"开始选择工况选项: {condition_name}") + + # 方法1: 通过文本查找并点击 + try: + option_xpath = f"//android.widget.TextView[@text='{condition_name}']" + option_element = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.XPATH, option_xpath)) + ) + option_element.click() + self.logger.info(f"通过文本定位成功选择工况: {condition_name}") + return True + except TimeoutException: + self.logger.debug(f"通过文本'{condition_name}'未找到工况选项") + + # 方法2: 通过列表项查找 + try: + # 假设工况选项在列表中,点击第一个可用的选项 + list_item = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/text1")) + ) + list_item.click() + self.logger.info("通过列表项选择工况") + return True + except TimeoutException: + self.logger.debug("未找到列表项形式的工况选项") + + # 方法3: 尝试点击屏幕特定位置(备选方案) + try: + # 获取屏幕尺寸 + window_size = self.driver.get_window_size() + x = window_size['width'] // 2 + y = window_size['height'] // 2 + + # 点击屏幕中央(假设选项在中间) + self.driver.tap([(x, y)]) + self.logger.info("通过点击屏幕中央选择工况") + return True + except Exception as e: + self.logger.debug(f"点击屏幕中央失败: {str(e)}") + + self.logger.error(f"所有方法都无法选择工况选项: {condition_name}") + return False + + except Exception as e: + self.logger.error(f"选择工况选项时出错: {str(e)}") + return False + + def _scroll_to_find_condition(self): + """滑动查找工况选项的辅助方法""" + try: + # 获取屏幕尺寸 + window_size = self.driver.get_window_size() + start_x = window_size['width'] * 0.5 + start_y = window_size['height'] * 0.7 + end_y = window_size['height'] * 0.3 + + # 执行滑动 + self.driver.swipe(start_x, start_y, start_x, end_y, 500) + logging.debug("执行滑动操作") + + except Exception as e: + logging.error(f"滑动操作时出错: {str(e)}") + + + def aging_down_data(self, breakpoint_name=None, retry_count=0): + """跳转上传配置页面,根据断点名称点击上传按钮,检查是否在上传配置页面 + ->变化量属性,执行下载操作""" + try: + self.logger.info("开始执行上传配置页面操作aging") + + # # 跳转到上传配置页面 + # if not self.go_upload_config_page(): + # self.logger.error("跳转上传配置页面失败") + # return False + # 跳转到上传配置页面 + if not go_main_click_tabber_button(self.driver, self.device_id, "com.bjjw.cjgc:id/img_2_layout"): + logging.error(f"设备 {self.device_id} 跳转到上传配置页面失败") + return False + + # 根据断点名称点击上传按钮 + if not self.click_upload_by_breakpoint_name(breakpoint_name): + self.logger.error("点击上传按钮失败") + return False + + if not self.handle_upload_dialog(): + self.logger.error("处理上传对话框失败") + return False + + self.logger.info("开始执行上传配置页面测试") + + # 检查是否在上传配置页面 + if not self.is_on_upload_config_page(): + self.logger.info("不在上传配置页面,尝试导航...") + return False + + # 检查是否有变化量属性 + if not self.check_change_amount_on_page(): + self.logger.info("页面中缺少变化量属性,执行下载操作") + + # 执行下载操作 + if not self.execute_download_operation(): + self.logger.error("下载操作执行失败") + return False + time.sleep(1) + self.logger.info("已返回上传配置页面") + + # 如果是第一次重试,再次调用aging_down_data + if retry_count < 1: + self.logger.info(f"第{retry_count+1}次重试,再次执行aging_down_data流程") + return self.aging_down_data(breakpoint_name, retry_count + 1) + else: + # 第二次仍然缺少变化量属性,返回错误 + self.logger.error("已执行两次下载操作,页面仍然缺少变化量属性") + return False + else: + self.logger.info("页面中包含变化量属性,继续执行后续操作") + return True + except Exception as e: + self.logger.error(f"设备执行上传重复下载数据操作失败:{e}") + return False + + def click_save_upload_and_handle_dialogs(self): + """点击保存上传并处理弹窗""" + try: + self.logger.info("开始点击保存上传并处理弹窗") + + # 点击保存上传按钮 + save_upload_btn = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/improve_save_btn")) + ) + save_upload_btn.click() + self.logger.info("已点击保存上传按钮") + + # 处理警告弹窗 + time.sleep(1) + if not self.handle_warning_dialog(): + self.logger.error("处理警告弹窗失败") + return False + + return True + + except TimeoutException: + self.logger.error("点击保存上传按钮超时") + return False + except Exception as e: + self.logger.error(f"点击保存上传并处理弹窗时出错: {str(e)}") + return False + + def handle_warning_dialog(self): + """处理警告弹窗""" + try: + self.logger.info("检查并处理警告弹窗") + + # 等待弹窗出现 + warning_dialog = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/parentPanel")) + ) + + # 获取弹窗文本确认是目标弹窗 + alert_title = warning_dialog.find_element(AppiumBy.ID, "android:id/alertTitle") + alert_message = warning_dialog.find_element(AppiumBy.ID, "android:id/message") + + self.logger.info(f"检测到弹窗 - 标题: {alert_title.text}, 消息: {alert_message.text}") + + # 根据业务逻辑选择"是"或"否" + # 这里选择"是"来上传本次数据 + yes_button = warning_dialog.find_element(AppiumBy.ID, "android:id/button1") + yes_button.click() + self.logger.info("已点击'是'按钮确认上传") + # no_button = warning_dialog.find_element(AppiumBy.ID, "android:id/button3") + # no_button.click() + # self.driver.back() + # self.logger.info("已点击'否'按钮取消上传和返回按钮") + + return True + + except TimeoutException: + self.logger.info("未检测到警告弹窗,继续流程") + return True # 没有弹窗也是正常情况 + except Exception as e: + self.logger.error(f"处理警告弹窗时出错: {str(e)}") + return False + + def wait_for_upload_completion(self): + """等待上传完成""" + try: + # time.sleep(2) + self.logger.info("开始等待上传完成") + + # 等待弹窗显示 + upload_list = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/customPanel")) + ) + #等待弹窗消失 + WebDriverWait(self.driver, 20).until( + EC.invisibility_of_element_located((AppiumBy.ID, "android:id/customPanel")) + ) + + self.logger.info("已返回到上传列表页面,上传已完成") + return True + + except TimeoutException: + self.logger.error("等待上传完成超时") + return False + except Exception as e: + self.logger.error(f"等待上传完成时出错: {str(e)}") + return False + + + def parse_work_conditions(self, work_conditions): + """ + 解析工况信息,区分主要工况和次要工况 + 返回: (主要工况字典, 次要工况列表) + """ + if not work_conditions: + logging.warning("工况数据为空") + return {}, [] + + try: + # 统计每个work_type下各个workinfoname的出现次数 + work_type_stats = {} + point_work_conditions = {} # 记录每个测点的工况信息 + + for point_id, condition_data in work_conditions.items(): + work_type = str(condition_data.get('work_type', '')) + workinfoname = condition_data.get('workinfoname', '') + sjname = condition_data.get('sjName', '') + + # 记录测点工况信息 + point_work_conditions[point_id] = { + 'work_type': work_type, + 'workinfoname': workinfoname, + 'sjname': sjname + } + + # 统计出现次数 + if work_type and workinfoname: + if work_type not in work_type_stats: + work_type_stats[work_type] = {} + + if workinfoname in work_type_stats[work_type]: + work_type_stats[work_type][workinfoname] += 1 + else: + work_type_stats[work_type][workinfoname] = 1 + + # 分离主要工况和次要工况 + main_condition_dict = {} # 主要工况字典 {work_type: [主要workinfoname]} + minor_conditions_list = [] # 次要工况列表 [{point_id, work_type, workinfoname}] + + # 定义阈值:出现次数少于这个值的认为是次要工况 + minor_threshold = 3 # 可以根据实际情况调整 + + for work_type, workinfoname_counts in work_type_stats.items(): + if workinfoname_counts: + # 按出现次数从多到少排序 + sorted_workinfonames = sorted(workinfoname_counts.items(), key=lambda x: x[1], reverse=True) + + # 主要工况:取出现次数最多的 + if sorted_workinfonames: + main_workinfoname = sorted_workinfonames[0][0] + main_condition_dict[work_type] = [main_workinfoname] + + logging.info(f"work_type {work_type} 的主要工况: '{main_workinfoname}' (出现{sorted_workinfonames[0][1]}次)") + + # 次要工况:收集所有出现次数较少的工况及其对应测点 + for workinfoname, count in sorted_workinfonames: + if count <= minor_threshold: + # 找到使用这个次要工况的所有测点 + for point_id, point_info in point_work_conditions.items(): + if (point_info['work_type'] == work_type and + point_info['workinfoname'] == workinfoname): + minor_conditions_list.append({ + 'point_id': point_id, + 'work_type': work_type, + 'workinfoname': workinfoname, + 'sjname': point_info['sjname'], + 'count': count + }) + + logging.info(f"解析结果: 主要工况 {len(main_condition_dict)} 种,次要工况 {len(minor_conditions_list)} 个测点") + + # 打印次要工况详情用于调试 + for minor_condition in minor_conditions_list: + logging.info(f"次要工况 - 测点 {minor_condition['point_id']}: work_type={minor_condition['work_type']}, workinfoname='{minor_condition['workinfoname']}'") + + return main_condition_dict, minor_conditions_list + + except Exception as e: + logging.error(f"解析工况信息时发生错误: {str(e)}") + return {}, [] + + def get_work_type_name(work_type: str) -> str: + """ + 根据工点类型编码获取类型名称 + + Args: + work_type: 工点类型编码(1-隧道,2-区间路基,3-桥, 4-涵洞) + + Returns: + 工点类型名称 + """ + work_type_mapping = { + "1": "隧道", + "2": "区间路基", + "3": "桥", + "4": "涵洞" + } + return work_type_mapping.get(work_type, f"未知类型({work_type})") + + def upload_config_page_manager(self, results_dir, breakpoint_name=None, line_num=None): + """执行上传配置页面管理操作""" + try: + # 保存参数为实例属性 + self.results_dir = results_dir + self.breakpoint_name = breakpoint_name + self.line_num = line_num + self.logger.info("开始执行上传配置页面操作manager") + + # 跳转到上传配置页面 + if not go_main_click_tabber_button(self.driver, self.device_id, "com.bjjw.cjgc:id/img_2_layout"): + logging.error(f"设备 {self.device_id} 跳转到测量页面失败") + return False + + # 根据断点名称点击上传按钮 + if not self.click_upload_by_breakpoint_name(breakpoint_name): + self.logger.error("点击上传按钮失败") + return False + + if not self.handle_upload_dialog(): + self.logger.error("处理上传对话框失败") + return False + + self.logger.info("开始执行上传配置页面测试") + + # 检查是否在上传配置页面 + if not self.is_on_upload_config_page(): + self.logger.info("不在上传配置页面,尝试导航...") + return False + + + # 检查是否有变化量属性,有就执行下面代码,没有就执行重新下载函数 + # 直接检查页面中是否有"变化量"属性 + if not self.check_change_amount_on_page(): + self.logger.info("页面中缺少变化量属性,执行下载操作") + # 执行下载操作 + if not self.execute_download_operation(): + self.logger.error("下载操作执行失败") + return False + # 下载操作完成后,点击上传导航按钮跳转到上传页面执行更多下载操作并检查状态 + if not self.aging_down_data(breakpoint_name): + self.logger.error("三次下载都失败,属性不存在变量") + return False + else: + self.logger.info("页面中包含变化量属性,继续执行后续操作") + + user_id = global_variable.GLOBAL_USERNAME + if user_id is None: + self.logger.error("获取用户ID失败") + return False + + max_variation = apis.get_user_max_variation(user_id) + if max_variation is None: + self.logger.error("获取用户最大变化量失败") + return False + + # # 循环滑动收集所有测点数据 + # logging.info("准备循环滑动收集所有测点数据") + # all_point_data = self.collect_all_point_data(results_dir) + + # # 保存测试结果 + # result_file = os.path.join(results_dir, f"{self.line_num}_{datetime.now().strftime('%Y%m%d')}.txt") + # with open(result_file, 'w', encoding='utf-8') as f: + # f.write(f"测试时间: {datetime.now().strftime('%Y-%m-%d')}\n") + # f.write(f"获取到的测点数: {len(all_point_data)}\n\n") + # for i, point in enumerate(all_point_data, 1): + # f.write(f"测点 {i}: {point.get('point_name', '未知')}\n") + # parsed = point.get('parsed_data', {}) + # # if parsed: + # # f.write(f" 本期数值: {parsed.get('current_value', 'N/A')}\n") + # # f.write(f" 上期数值: {parsed.get('previous_value', 'N/A')}\n") + # # f.write(f" 变化量: {parsed.get('change_amount', 'N/A')}\n") + # # f.write(f" 测量时间: {parsed.get('measurement_time', 'N/A')}\n") + # f.write(f"完整数据:\n{point.get('point_value', 'N/A')}\n\n") + + # self.logger.info(f"测试结果已保存到: {result_file}") + + # 给ai接口发送文件,等待接口返回是否能上传数据 + # 不能就点击手机返回按钮,能就继续填写表单 + # if not self.check_ai_upload_permission(result_file): + # self.logger.info("AI接口返回不允许上传,点击返回按钮") + # self.driver.back() + # return True # 返回True继续下一个断点的上传配置。 + + if not self.collect_check_all_point_data(max_variation): + self.logger.error(f"断点 '{breakpoint_name}' 上传失败") + self.driver.back() + return False # 返回False继续下一个断点的上传配置。 + + + # 获取线路的所有工况信息 + work_conditions = apis.get_work_conditions_by_linecode(self.line_num) + # work_conditions = {'1962527': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}, + # '0299815Z2': {'sjName': '王顺', 'workinfoname': '冬休', 'work_type': 2}, + # '0299820H1': {'sjName': '王顺', 'workinfoname': '架桥机(运梁车) 首次通过后', 'work_type': 4}, + # '0431248D1': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 4}, + # '0431248D2': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 4}, + # '0299815Z1': {'sjName': '王顺', 'workinfoname': '架桥机(运梁车) 首次通过前', 'work_type': 2}, + # '0431289D2': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 4}, + # '0431330D1': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}, + # '0431330D2': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}, + # '0431370D1': {'sjName': '王顺', 'workinfoname': '轨道板(道床)铺设后,第1个月', 'work_type': 2}} + self.logger.info(f"获取线路工况信息成功: {work_conditions}") + if not work_conditions: + self.logger.error("获取工况信息失败") + return False + + # 提取人员姓名和身份证 + if not self._load_user_data(): + self.logger.error("加载用户数据失败") + return False + + # 获取第一个数据员姓名和身份证号 + user_info = self.get_first_sjname_and_id(self.line_num, work_conditions) + self.logger.info(f"获取到的第一个数据员姓名和身份证号为:{user_info}") + + if not user_info: + self.logger.error(f"无法获取线路 '{self.line_num}' 的数据员信息") + return False + + + # 解析工况信息,现在返回两个值:主要工况字典和次要工况列表 + main_condition_dict, minor_conditions_list = self.parse_work_conditions(work_conditions) + self.logger.info(f"主要工况: {main_condition_dict}") + self.logger.info(f"次要工况数量: {len(minor_conditions_list)}") + # 设置所有测点并填写表单 - 传入两个参数 + if not self.set_all_points_and_fill_form(results_dir, user_info.get("name"), user_info.get("id_card"), main_condition_dict, minor_conditions_list): + self.logger.error("设置所有测点并填写表单失败") + return False + + + # # 表达填写完成,点击"保存上传"并处理弹窗 + # if not self.click_save_upload_and_handle_dialogs(): + # self.logger.error("点击保存上传并处理弹窗失败") + # return False + + # 暂不上传,使用返回按钮替代。 + self.driver.back() + + # 等待上传,查看loading弹窗。没有就下一个 + if not self.wait_for_upload_completion(): + self.logger.error("等待上传完成失败") + return False + + self.logger.info("上传配置页面操作执行完成") + # 把上传成功的断点写入全局变量GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST + global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST.append(breakpoint_name) + + return True + + except Exception as e: + self.logger.error(f"执行上传配置页面操作时出错: {str(e)}") + # # 保存错误截图 + # error_screenshot_file = os.path.join( + # results_dir, + # f"upload_config_error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + # ) + # self.driver.save_screenshot(error_screenshot_file) + # self.logger.info(f"错误截图已保存: {error_screenshot_file}") + return False \ No newline at end of file diff --git a/permissions.py b/permissions.py new file mode 100644 index 0000000..8fc9a94 --- /dev/null +++ b/permissions.py @@ -0,0 +1,251 @@ +# 权限处理 +import subprocess +import logging +import time + +# 配置日志 +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s") + +def check_device_connection(device_id: str) -> bool: + """检查设备连接状态""" + try: + check_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.product.model"] + result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + logging.info(f"设备 {device_id} 连接正常,型号: {result.stdout.strip()}") + return True + else: + logging.error(f"设备 {device_id} 连接失败: {result.stderr.strip()}") + return False + except subprocess.TimeoutExpired: + logging.error(f"设备 {device_id} 连接超时") + return False + except Exception as e: + logging.error(f"检查设备 {device_id} 时发生错误: {str(e)}") + return False + +def is_package_installed(device_id: str, package_name: str) -> bool: + """检查包是否已安装""" + try: + check_cmd = ["adb", "-s", device_id, "shell", "pm", "list", "packages", package_name] + result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10) + return result.returncode == 0 and package_name in result.stdout + except Exception as e: + logging.error(f"检查包 {package_name} 时发生错误: {str(e)}") + return False + +def grant_single_permission(device_id: str, package: str, permission: str) -> bool: + """ + 为单个包授予单个权限 + :return: 是否成功授予 + """ + try: + grant_cmd = [ + "adb", "-s", device_id, + "shell", "pm", "grant", package, + permission + ] + result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15) + + if result.returncode == 0: + logging.info(f"设备 {device_id}:已成功授予 {package} 权限: {permission}") + return True + else: + error_msg = result.stderr.strip() + logging.warning(f"设备 {device_id}:授予 {package} 权限 {permission} 失败: {error_msg}") + + # 尝试使用root权限 + if "security" in error_msg.lower() or "permission" in error_msg.lower(): + logging.info(f"设备 {device_id}:尝试使用root权限授予 {package} 权限") + + # 重启adb为root模式 + root_cmd = ["adb", "-s", device_id, "root"] + subprocess.run(root_cmd, capture_output=True, text=True, timeout=10) + time.sleep(2) # 等待root权限生效 + + # 再次尝试授予权限 + result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15) + if result.returncode == 0: + logging.info(f"设备 {device_id}:使用root权限成功授予 {package} 权限: {permission}") + return True + else: + logging.error(f"设备 {device_id}:即使使用root权限也无法授予 {package} 权限 {permission}: {result.stderr.strip()}") + return False + else: + return False + + except subprocess.CalledProcessError as e: + logging.error(f"设备 {device_id}:ADB 命令执行失败,返回码 {e.returncode}") + logging.error(f"标准输出:{e.stdout.strip()}") + logging.error(f"错误输出:{e.stderr.strip()}") + return False + except Exception as e: + logging.error(f"设备 {device_id}:处理 {package} 时发生未知错误:{str(e)}") + return False + +def grant_appium_permissions(device_id: str, require_all: bool = False) -> bool: + """ + 为 Appium UiAutomator2 服务授予权限 + :param device_id: 设备 ID + :param require_all: 是否要求所有权限都成功授予 + :return: 权限授予是否成功(根据require_all参数判断) + """ + # 首先检查设备连接 + if not check_device_connection(device_id): + return False + + packages_to_grant = [ + "io.appium.settings", + "io.appium.uiautomator2.server", + "io.appium.uiautomator2.server.test" + ] + + # 权限列表(按优先级排序) + permissions_to_grant = [ + "android.permission.WRITE_SECURE_SETTINGS", + "android.permission.CHANGE_CONFIGURATION", + "android.permission.DUMP", + ] + + success_count = 0 + total_attempted = 0 + package_results = {} + + # 检查并授予权限 + for package in packages_to_grant: + package_results[package] = {"installed": False, "permissions": {}} + + if not is_package_installed(device_id, package): + logging.warning(f"设备 {device_id}:包 {package} 未安装,跳过权限授予") + package_results[package]["installed"] = False + continue + + package_results[package]["installed"] = True + package_success = 0 + package_attempted = 0 + + for permission in permissions_to_grant: + total_attempted += 1 + package_attempted += 1 + + result = grant_single_permission(device_id, package, permission) + package_results[package]["permissions"][permission] = result + + if result: + success_count += 1 + package_success += 1 + + # 记录每个包的授权结果 + logging.info(f"设备 {device_id}:包 {package} 权限授予结果: {package_success}/{package_attempted}") + + # 统计和报告 + logging.info(f"设备 {device_id}:权限授予完成") + logging.info(f"总计: 尝试 {total_attempted} 次,成功 {success_count} 次") + + # 检查每个包的关键权限 + critical_permission = "android.permission.WRITE_SECURE_SETTINGS" + critical_failures = [] + + for package, info in package_results.items(): + if info["installed"] and critical_permission in info["permissions"]: + if not info["permissions"][critical_permission]: + critical_failures.append(package) + + if critical_failures: + logging.warning(f"设备 {device_id}:以下包的关键权限 {critical_permission} 授予失败: {', '.join(critical_failures)}") + logging.warning("这可能会影响某些自动化功能,但基础测试通常不受影响") + + # 根据require_all参数返回结果 + if require_all: + # 要求所有权限都成功 + if critical_failures: + logging.error("关键权限授予失败,无法继续(require_all=True)") + return False + return success_count == total_attempted + else: + # 不要求所有权限,只要设备连接正常就返回True + logging.info(f"设备 {device_id}:权限授予过程完成,建议重启设备或Appium服务使更改生效") + return True + +def check_appium_compatibility(device_id: str) -> dict: + """ + 检查Appium兼容性 + :return: 兼容性报告字典 + """ + try: + # 获取Android版本 + version_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.build.version.release"] + result = subprocess.run(version_cmd, capture_output=True, text=True, timeout=10) + android_version = result.stdout.strip() if result.returncode == 0 else "未知" + + report = { + "device_id": device_id, + "android_version": android_version, + "compatibility": "unknown", + "notes": [], + "suggestions": [] + } + + try: + version_num = float(android_version.split('.')[0]) + + if version_num >= 11: + report["compatibility"] = "limited" + report["notes"].append("Android 11+ 对WRITE_SECURE_SETTINGS权限限制非常严格") + report["suggestions"].append("使用--no-reset参数启动Appium") + report["suggestions"].append("设置autoGrantPermissions=false") + + elif version_num >= 10: + report["compatibility"] = "moderate" + report["notes"].append("Android 10 限制了WRITE_SECURE_SETTINGS权限") + report["suggestions"].append("可尝试使用root权限的设备") + + elif version_num >= 9: + report["compatibility"] = "good" + report["notes"].append("Android 9 兼容性较好") + + else: + report["compatibility"] = "excellent" + report["notes"].append("Android 8或以下版本完全兼容") + + except (ValueError, IndexError): + report["notes"].append("无法解析Android版本") + + return report + + except Exception as e: + logging.error(f"检查兼容性时出错: {str(e)}") + return {"device_id": device_id, "error": str(e)} + +# 使用示例 +if __name__ == "__main__": + # 获取设备ID(示例) + devices_cmd = ["adb", "devices"] + result = subprocess.run(devices_cmd, capture_output=True, text=True) + + if result.returncode == 0: + lines = result.stdout.strip().split('\n')[1:] # 跳过第一行标题 + for line in lines: + if line.strip() and "device" in line: + device_id = line.split('\t')[0] + logging.info(f"找到设备: {device_id}") + + # 1. 检查兼容性 + report = check_appium_compatibility(device_id) + logging.info(f"兼容性报告: Android {report.get('android_version', '未知')} - {report.get('compatibility', '未知')}") + + # 2. 授予权限(不要求全部成功) + success = grant_appium_permissions(device_id, require_all=False) + + if success: + logging.info(f"设备 {device_id} 设置完成,可以开始测试") + else: + logging.warning(f"设备 {device_id} 权限授予有失败,但可能仍可进行基础测试") + + # 3. 提供建议 + if "suggestions" in report: + logging.info("建议:") + for suggestion in report["suggestions"]: + logging.info(f" - {suggestion}") + else: + logging.error("无法获取设备列表,请确保ADB已正确安装且设备已连接") \ No newline at end of file diff --git a/test_results/上传失败的断点.txt b/test_results/上传失败的断点.txt new file mode 100644 index 0000000..62e0a65 --- /dev/null +++ b/test_results/上传失败的断点.txt @@ -0,0 +1,5 @@ +CDWZQ-3标-雷庙村大桥-8-号桥台-平原 +CDWZQ-3标-雷庙村特大桥-5-6-号墩-平原 +CDWZQ-3标-金马村特大桥-14-号墩-平原 +CDWZQ-3标-雷庙村特大桥-4#-7-14号墩-平原 +CDWZQ-3标-老屋坡特大桥-14-21-平原 diff --git a/test_results/上传成功的断点.txt b/test_results/上传成功的断点.txt new file mode 100644 index 0000000..e69de29 diff --git a/上传人员信息.xlsx b/上传人员信息.xlsx new file mode 100644 index 0000000..4baf13e Binary files /dev/null and b/上传人员信息.xlsx differ