commit aa4d4c7d7c9d33e693319bf7b1a3adacb6dba7b3 Author: YiLin <482244139@qq.com> Date: Thu Mar 12 17:03:56 2026 +0800 first commit diff --git a/CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx b/CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx new file mode 100644 index 0000000..dc6a8c8 Binary files /dev/null and b/CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx differ diff --git a/__pycache__/check_station.cpython-312.pyc b/__pycache__/check_station.cpython-312.pyc new file mode 100644 index 0000000..8ca335c Binary files /dev/null and b/__pycache__/check_station.cpython-312.pyc differ diff --git a/__pycache__/driver_utils.cpython-312.pyc b/__pycache__/driver_utils.cpython-312.pyc new file mode 100644 index 0000000..4f34253 Binary files /dev/null and b/__pycache__/driver_utils.cpython-312.pyc differ diff --git a/__pycache__/permissions.cpython-312.pyc b/__pycache__/permissions.cpython-312.pyc new file mode 100644 index 0000000..0b2c27b Binary files /dev/null and b/__pycache__/permissions.cpython-312.pyc differ diff --git a/actions.py b/actions.py new file mode 100644 index 0000000..d70f38e --- /dev/null +++ b/actions.py @@ -0,0 +1,302 @@ +import logging +import os +import time +import subprocess +from tkinter import E +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 +from page_objects.download_tabbar_page import DownloadTabbarPage +from page_objects.measure_tabbar_page import MeasureTabbarPage +from page_objects.section_mileage_config_page import SectionMileageConfigPage +from page_objects.upload_config_page import UploadConfigPage +from page_objects.more_download_page import MoreDownloadPage +from page_objects.screenshot_page import ScreenshotPage +import globals.driver_utils as driver_utils # 导入驱动工具模块 +import globals.global_variable as global_variable +from page_objects.login_page import LoginPage +import globals.apis as apis +import globals.create_link as create_link + + +class DeviceAutomation: + 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 driver_utils.grant_appium_permissions(self.device_id): + logging.info(f"设备 {self.device_id} 授予Appium权限成功") + else: + logging.warning(f"设备 {self.device_id} 授予Appium权限失败") + + # 确保Appium服务器正在运行,不在运行则启动 + if not driver_utils.check_server_status(4723): + driver_utils.start_appium_server() + + # 初始化Appium驱动和页面对象 + self.init_driver() + # 创建测试结果目录 + self.results_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_results') + + @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 + + target_port = "4723" + 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] + + # 检查是否为无线设备且端口为4723 + if ':' in device_id: + ip_port = device_id.split(':') + if len(ip_port) == 2 and ip_port[1] == target_port: + logging.info(f"找到目标无线设备(端口{target_port}): {device_id}") + global_variable.GLOBAL_DEVICE_ID = device_id + return device_id + + # 如果没有找到端口4723的设备,找其他无线设备 + 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] + + # 检查是否为无线设备(任何端口) + if ':' in device_id and device_id.split(':')[-1].isdigit(): + logging.info(f"未找到端口{target_port}的设备,使用其他无线设备: {device_id}") + global_variable.GLOBAL_DEVICE_ID = device_id + return device_id + + # 如果没有任何无线设备,找有线设备 + 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 + + logging.error("未找到任何可用设备") + return None + + 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_driver(self): + """初始化Appium驱动""" + try: + # 使用全局函数初始化驱动 + self.driver, self.wait = driver_utils.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.measure_tabbar_page = MeasureTabbarPage(self.driver, self.wait,self.device_id) + self.section_mileage_config_page = SectionMileageConfigPage(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) + self.screenshot_page = ScreenshotPage(self.driver, self.wait, self.device_id) + logging.info(f"设备 {self.device_id} 所有页面对象初始化完成") + + # 检查应用是否成功启动 + if driver_utils.is_app_launched(self.driver): + logging.info(f"设备 {self.device_id} 沉降观测App已成功启动") + else: + logging.warning(f"设备 {self.device_id} 应用可能未正确启动(init_driver)") + driver_utils.launch_app_manually(self.driver) + + except Exception as e: + logging.error(f"设备 {self.device_id} 初始化驱动失败: {str(e)}") + raise + + def run_automation(self): + """根据当前应用状态处理相应的操作""" + try: + max_retry = 3 # 限制最大重试次数 + retry_count = 0 + while retry_count < max_retry: + login_btn_exists = self.login_page.is_login_page() + if not login_btn_exists: + logging.error(f"设备 {self.device_id} 未知应用状态,无法确定当前页面,跳转到登录页面") + if self.login_page.navigate_to_login_page(self.driver, self.device_id): + logging.info(f"设备 {self.device_id} 成功跳转到登录页面") + else: + logging.error(f"设备 {self.device_id} 跳转到登录页面失败") + retry_count += 1 + continue + # 处理登录页面状态 + logging.info(f"设备 {self.device_id} 检测到登录页面,执行登录操作") + max_retries_login = 3 + login_success = False + + for attempt in range(max_retries_login + 1): + if self.login_page.login("wangshun"): + login_success = True + break + else: + if attempt < max_retries_login: + logging.warning(f"设备 {self.device_id} 登录失败,准备重试 ({attempt + 1}/{max_retries_login})") + time.sleep(2) # 等待2秒后重试 + else: + logging.error(f"设备 {self.device_id} 登录失败,已达到最大重试次数") + + if login_success: + break + elif retry_count == max_retry-1: + logging.error(f"设备 {self.device_id} 处理登录页面失败,已达到最大重试次数") + return False + else: + retry_count += 1 + + # login_btn_exists = self.login_page.is_login_page() + # if not login_btn_exists: + # logging.error(f"设备 {self.device_id} 未知应用状态,无法确定当前页面,跳转到登录页面") + # if self.login_page.navigate_to_login_page(self.driver, self.device_id): + # logging.info(f"设备 {self.device_id} 成功跳转到登录页面") + # return self.run_automation() # 递归调用处理登录后的状态 + # 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 + + task_count = 0 + max_tasks = 1 # 最大任务数量,防止无限循环 + + while task_count < max_tasks: + # 获取测量任务 + logging.info(f"设备 {self.device_id} 获取测量任务 (第{task_count + 1}次)") + # task_data = apis.get_measurement_task() + # logging.info(f"设备 {self.device_id} 获取到的测量任务: {task_data}") + task_data = { + "id": 39, + "user_name": "czsczq115ykl", + "name": "czsczq115ykl", + "line_num": "L179451", + "line_name": "CDWZQ-2标-资阳沱江特大桥-23-35-山区", + "remaining": "0", + "status": 1 + } + if not task_data: + logging.info(f"设备 {self.device_id} 未获取到状态为1的测量任务,等待后重试") + time.sleep(1) # 等待1秒后重试 + break + # continue + + # 设置全局变量 + global_variable.GLOBAL_CURRENT_PROJECT_NAME = task_data.get('line_name', '') + global_variable.GLOBAL_LINE_NUM = task_data.get('line_num', '') + logging.info(f"设备 {self.device_id} 当前要处理的项目名称:{global_variable.GLOBAL_CURRENT_PROJECT_NAME}") + + # 执行测量操作 + # logging.info(f"设备 {self.device_id} 开始执行测量操作") + if not self.measure_tabbar_page.measure_tabbar_page_manager(): + logging.error(f"设备 {self.device_id} 测量操作执行失败") + + # # 返回到测量页面 + # self.driver.back() + # self.check_and_click_confirm_popup_appium() + + continue # 继续下一个任务 + + logging.info(f"设备 {self.device_id} 测量页面操作执行成功") + + # 在测量操作完成后执行断面里程配置 + logging.info(f"设备 {self.device_id} 开始执行断面里程配置") + if not self.section_mileage_config_page.section_mileage_config_page_manager(): + logging.error(f"设备 {self.device_id} 断面里程配置执行失败") + continue # 继续下一个任务 + + # 任务完成后短暂等待 + logging.info(f"设备 {self.device_id} 第{task_count}个任务完成") + task_count += 1 + + logging.info(f"设备 {self.device_id} 已完成{task_count}个任务,结束打数据流程") + if task_count == 0: + logging.error(f"没有完成打数据的线路,结束任务") + return False + + + # GLOBAL_TESTED_BREAKPOINT_LIST 把已打完的写入日志文件 + with open(os.path.join(self.results_dir, "打数据完成线路.txt"), "w", encoding='utf-8') as f: + for bp in global_variable.GLOBAL_TESTED_BREAKPOINT_LIST: + f.write(f"{bp}\n") + + return task_count > 0 + + except Exception as e: + logging.error(f"设备 {self.device_id} 处理应用状态时出错: {str(e)}") + return False + +# 主执行逻辑 +if __name__ == "__main__": + create_link.setup_adb_wireless() + automation = None + try: + automation = DeviceAutomation() + success = automation.run_automation() + + if success: + logging.info(f"设备 {automation.device_id} 自动化流程执行成功") + else: + logging.error(f"设备 {automation.device_id} 自动化流程执行失败") + except Exception as e: + logging.error(f"设备执行出错: {str(e)}") + finally: + if automation: + driver_utils.safe_quit_driver(automation.driver, automation.device_id) diff --git a/check_station.png b/check_station.png new file mode 100644 index 0000000..e054821 Binary files /dev/null and b/check_station.png differ diff --git a/check_station.py b/check_station.py new file mode 100644 index 0000000..13ff4a0 --- /dev/null +++ b/check_station.py @@ -0,0 +1,211 @@ +import logging +import time +import requests +import pandas as pd +from io import BytesIO +import subprocess +import globals.global_variable as global_variable +import globals.driver_utils as driver_utils # 导入驱动工具模块 + + + +class CheckStation: + def __init__(self, driver=None, wait=None,device_id=None): + """初始化CheckStation对象""" + if device_id is None: + self.device_id = self.get_device_id() + else: + self.device_id = device_id + if driver is None or wait is None: + self.driver, self.wait = driver_utils.init_appium_driver(self.device_id) + else: + self.driver = driver + self.wait = wait + if driver_utils.grant_appium_permissions(self.device_id): + logging.info(f"设备 {self.device_id} 授予Appium权限成功") + else: + logging.warning(f"设备 {self.device_id} 授予Appium权限失败") + + # 确保Appium服务器正在运行,不在运行则启动 + if not driver_utils.check_server_status(4723): + driver_utils.start_appium_server() + + # 检查应用是否成功启动 + if driver_utils.is_app_launched(self.driver): + logging.info(f"设备 {self.device_id} 沉降观测App已成功启动") + else: + logging.warning(f"设备 {self.device_id} 应用可能未正确启动") + driver_utils.check_app_status(self.driver) + + @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}") + + # 使用全局配置 + device_id = global_variable.GLOBAL_DEVICE_ID + logging.info(f"使用全局配置设备: {device_id}") + return device_id + + def get_measure_data(self): + # 模拟获取测量数据 + pass + + def add_transition_point(self): + # 添加转点逻辑 + print("添加转点") + return True + + def get_excel_from_url(self, url): + """ + 从URL获取Excel文件并解析为字典 + Excel只有一列数据(A列),每行是站点值 + + Args: + url: Excel文件的URL地址 + + Returns: + dict: 解析后的站点数据字典 {行号: 值},失败返回None + """ + try: + print(f"正在从URL获取数据: {url}") + response = requests.get(url, timeout=30) + response.raise_for_status() # 检查请求是否成功 + + # 使用pandas读取Excel数据,指定没有表头,只读第一个sheet + excel_data = pd.read_excel( + BytesIO(response.content), + header=None, # 没有表头 + sheet_name=0, # 只读取第一个sheet + dtype=str # 全部作为字符串读取 + ) + + station_dict = {} + + # 解析Excel数据:使用行号+1作为站点编号,A列的值作为站点值 + print("解析Excel数据(使用行号作为站点编号)...") + for index, row in excel_data.iterrows(): + station_num = index + 1 # 行号从1开始作为站点编号 + station_value = str(row[0]).strip() if pd.notna(row[0]) else "" + + if station_value: # 只保存非空值 + station_dict[station_num] = station_value + + print(f"成功解析Excel,共{len(station_dict)}条数据") + return station_dict + + except requests.exceptions.RequestException as e: + print(f"请求URL失败: {e}") + return None + except Exception as e: + print(f"解析Excel失败: {e}") + return None + + def check_station_exists(self, station_data: dict, station_num: int) -> str: + """ + 根据站点编号检查该站点的值是否以Z开头 + + Args: + station_data: 站点数据字典 {编号: 值} + station_num: 要检查的站点编号 + + Returns: + str: 如果站点存在且以Z开头返回"add",否则返回"pass" + """ + if station_num not in station_data: + print(f"站点{station_num}不存在") + return "error" + + value = station_data[station_num] + str_value = str(value).strip() + is_z = str_value.upper().startswith('Z') + + result = "add" if is_z else "pass" + print(f"站点{station_num}: {value} -> {result}") + return result + + + def run(self): + last_station_num = 0 + + url = f"https://database.yuxindazhineng.com/team-bucket/69378c5b4f42d83d9504560d/前测点表/20260309/CDWZQ-2标-龙家沟左线大桥-0-11号墩-平原.xlsx" + station_data = self.get_excel_from_url(url) + print(station_data) + station_quantity = len(station_data) + over_station_num = 0 + over_station_list = [] + while over_station_num < station_quantity: + try: + # 键盘输出线路编号 + station_num_input = input("请输入线路编号:") + if not station_num_input.isdigit(): # 检查输入是否为数字 + print("输入错误:请输入一个整数") + continue + station_num = int(station_num_input) # 转为整数 + + if station_num in over_station_list: + print("已处理该站点,跳过") + continue + + if last_station_num == station_num: + print("输入与上次相同,跳过处理") + continue + last_station_num = station_num + + result = self.check_station_exists(station_data, station_num) + if result == "error": + print("处理错误:站点不存在") + # 错误处理逻辑,比如记录日志、发送警报等 + elif result == "add": + print("执行添加操作") + # 添加转点 + if not self.add_transition_point(): + print("添加转点失败") + # 可以决定是否继续循环 + continue + over_station_num += 1 + else: # result == "pass" + print("跳过处理") + over_station_num += 1 + + over_station_list.append(station_num) + + # 可以添加适当的延时,避免CPU占用过高 + # time.sleep(1) + + except KeyboardInterrupt: + print("程序被用户中断") + break + except Exception as e: + print(f"发生错误: {e}") + time.sleep(20) + # 错误处理,可以继续循环或退出 + print(f"已处理{over_station_num}个站点") + + # 截图 + self.driver.save_screenshot("check_station.png") + return True + +if __name__ == "__main__": + check_station = CheckStation() + check_station.run() \ No newline at end of file diff --git a/ck/.gitignore b/ck/.gitignore new file mode 100644 index 0000000..a8775af --- /dev/null +++ b/ck/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Virtual Environment +venv/ +env/ +ENV/ + +# Test +.pytest_cache/ +.coverage +htmlcov/ diff --git a/ck/README.md b/ck/README.md new file mode 100644 index 0000000..ade52a4 --- /dev/null +++ b/ck/README.md @@ -0,0 +1,292 @@ +# 串口通信协议 + +一个完整的Python串口通信协议实现,支持两台设备之间可靠的数据传输,可传输结构化数据(用户名、线路名称、站点编号等)。 + +## 特性 + +- ✅ **完整的协议帧结构** - 包含帧头、长度、命令、数据、CRC校验、帧尾 +- ✅ **CRC16校验** - 使用CRC-16/MODBUS算法确保数据完整性 +- ✅ **异步接收** - 多线程接收,不阻塞主程序 +- ✅ **多种命令类型** - 心跳、数据传输、控制命令、应答等 +- ✅ **结构化数据** - 支持JSON格式的站点数据传输(用户名、线路名称、站点编号) +- ✅ **易于使用** - 简洁的API,开箱即用 + +## 协议帧格式 + +``` ++------+------+--------+------+--------+------+ +| HEAD | LEN | CMD | DATA | CRC | TAIL | ++------+------+--------+------+--------+------+ +| 0xAA | 2B | 1B | N字节 | 2B | 0x55 | ++------+------+--------+------+--------+------+ +``` + +### 字段说明 + +- **HEAD**: 帧头标识 (1字节, 0xAA) +- **LEN**: 数据长度 (2字节, 大端序, 包含CMD+DATA) +- **CMD**: 命令字 (1字节) +- **DATA**: 数据内容 (N字节) +- **CRC**: CRC16校验 (2字节, 大端序) +- **TAIL**: 帧尾标识 (1字节, 0x55) + +## 安装 + +```bash +pip install -r requirements.txt +``` + +## 命令类型 + +| 命令 | 值 | 说明 | +| ----------- | ---- | -------------- | +| HEARTBEAT | 0x01 | 心跳包 | +| DATA_QUERY | 0x02 | 数据查询 | +| DATA_RESPONSE | 0x03 | 数据响应 | +| CONTROL | 0x04 | 控制命令 | +| ACK | 0x05 | 应答 | +| NACK | 0x06 | 否定应答 | + +## 站点数据模型 + +### StationData 类 + +用于传输站点信息的数据模型: + +```python +from data_models import StationData + +# 创建站点数据 +station = StationData( + username="张三", # 用户名 + line_name="1号线", # 线路名称 + station_no=5 # 第几站 +) + +# 序列化为字节(用于发送) +data_bytes = station.to_bytes() + +# 从字节反序列化(接收后解析) +restored = StationData.from_bytes(data_bytes) +``` + +## 使用方法 + +### 基本示例 + +```python +from serial_protocol import SerialProtocol, Command + +# 创建协议实例 +device = SerialProtocol(port='COM1', baudrate=115200) + +# 打开串口 +if device.open(): + # 发送数据 + device.send_data(b"Hello, World!") + + # 发送心跳 + device.send_heartbeat() + + # 发送控制命令 + device.send_control(0x10, b"\x01\x02") + + # 关闭串口 + device.close() +``` + +### 异步接收数据 + +```python +def on_receive(cmd, data): + print(f"收到命令: 0x{cmd:02X}, 数据: {data.hex()}") + +device = SerialProtocol(port='COM1', baudrate=115200) +device.open() + +# 启动接收线程 +device.start_receive(on_receive) + +# ... 主程序运行 ... + +device.close() +``` + +## 完整示例 + +### 设备A (发送端) + +运行 `device_a.py`,设备A会定期发送: +- 心跳包 +- **站点数据**(包含用户名、线路名称、站点编号) +- **接收设备B的字典响应** + +输出示例: +``` +============================================================ +循环 1 +============================================================ +✓ 发送心跳包 + +准备发送站点数据: + 用户: 李四, 线路: 2号线, 站点: 第1站 +✓ 站点数据发送成功 + +[设备A] 收到数据: + 命令: 0x03 (DATA_RESPONSE) + + 📥 收到设备B的响应字典: + 1: "李四" + 2: "2号线" + 3: "1" + 4: "已接收" + 5: "设备B确认" +``` + +运行命令: +```bash +python device_a.py +``` + +### 设备B (接收端) + +运行 `device_b.py`,设备B会: +- 接收站点数据并解析显示 +- 自动应答心跳包 +- **返回统一格式的响应字典** `{1:"xxx", 2:"xxx", 3:"xxx", ...}` + +输出示例: +``` +============================================================ +[设备B] 收到数据 +============================================================ +命令类型: 0x03 (DATA_RESPONSE) + +📍 站点数据详情: + 用户名称: 李四 + 线路名称: 2号线 + 站点编号: 第1站 + +📤 设备B统一响应格式: + 1: "李四" + 2: "2号线" + 3: "1" + 4: "已接收" + 5: "设备B确认" + +<<< 已发送响应字典 +``` + +运行命令: +```bash +python device_b.py +``` + +## 设备B统一响应格式 + +设备B接收到站点数据后,会返回统一格式的字典响应: + +```python +{ + 1: "用户名", + 2: "线路名称", + 3: "站点编号", + 4: "已接收", + 5: "设备B确认" +} +``` + +**字段说明:** +- `1`: 接收到的用户名 +- `2`: 接收到的线路名称 +- `3`: 接收到的站点编号(字符串) +- `4`: 固定值 "已接收" +- `5`: 固定值 "设备B确认" + +## 硬件连接 + +### 方案1: 使用两个USB转串口模块 + +``` +设备A (COM1) <---RX/TX交叉---> 设备B (COM2) + TX ----------------------> RX + RX <---------------------- TX + GND <---------------------> GND +``` + +### 方案2: 使用虚拟串口 (测试用) + +**Windows**: 使用 com0com 或 Virtual Serial Port Driver +**Linux**: 使用 socat + +```bash +# Linux创建虚拟串口对 +socat -d -d pty,raw,echo=0 pty,raw,echo=0 +# 会创建 /dev/pts/X 和 /dev/pts/Y +``` + +## API文档 + +### SerialProtocol 类 + +#### 初始化 + +```python +SerialProtocol(port: str, baudrate: int = 115200, timeout: float = 1.0) +``` + +#### 方法 + +- `open() -> bool` - 打开串口 +- `close()` - 关闭串口 +- `send_frame(cmd: int, data: bytes) -> bool` - 发送数据帧 +- `receive_frame() -> Optional[dict]` - 接收数据帧(阻塞) +- `start_receive(callback)` - 启动异步接收 +- `stop_receive()` - 停止异步接收 +- `send_heartbeat() -> bool` - 发送心跳包 +- `send_data(data: bytes) -> bool` - 发送数据 +- `send_control(code: int, params: bytes) -> bool` - 发送控制命令 +- `send_ack() -> bool` - 发送应答 +- `send_nack() -> bool` - 发送否定应答 + +#### 静态方法 + +- `calc_crc16(data: bytes) -> int` - 计算CRC16校验值 +- `build_frame(cmd: int, data: bytes) -> bytes` - 构建数据帧 +- `parse_frame(frame: bytes) -> Optional[dict]` - 解析数据帧 + +## 常见问题 + +### 1. 如何修改串口号? + +编辑 `device_a.py` 和 `device_b.py`,修改 `port` 参数: +- Windows: `'COM1'`, `'COM2'`, etc. +- Linux: `'/dev/ttyUSB0'`, `'/dev/ttyS0'`, etc. + +### 2. 如何修改波特率? + +修改 `baudrate` 参数,常用值:9600, 19200, 38400, 57600, 115200 + +### 3. 数据长度限制? + +理论最大65535字节,但建议单帧数据不超过1024字节以提高可靠性。 + +### 4. 如何处理超时? + +设置 `timeout` 参数控制读取超时时间。 + +## 应用场景 + +- 🤖 机器人通信 +- 📡 传感器数据采集 +- 🎮 设备控制 +- 📊 工业自动化 +- 🔌 嵌入式系统互联 + +## 许可证 + +MIT License + +## 作者 + +Created with ❤️ for reliable serial communication diff --git a/ck/data_models.py b/ck/data_models.py new file mode 100644 index 0000000..8dd72fd --- /dev/null +++ b/ck/data_models.py @@ -0,0 +1,82 @@ +""" +数据模型定义 +定义设备间传输的数据结构 +""" + +import json +from typing import Optional, Dict, Any +from dataclasses import dataclass, asdict + + +@dataclass +class StationData: + """站点数据模型""" + username: str # 用户名 + line_name: str # 线路名称 + station_no: int # 第几站 + + def to_bytes(self) -> bytes: + """转换为字节数据""" + data_dict = asdict(self) + json_str = json.dumps(data_dict, ensure_ascii=False) + return json_str.encode('utf-8') + + @staticmethod + def from_bytes(data: bytes) -> Optional['StationData']: + """从字节数据解析""" + try: + json_str = data.decode('utf-8') + data_dict = json.loads(json_str) + return StationData( + username=data_dict['username'], + line_name=data_dict['line_name'], + station_no=data_dict['station_no'] + ) + except Exception as e: + print(f"解析数据失败: {e}") + return None + + def __str__(self): + """字符串表示""" + return (f"用户: {self.username}, " + f"线路: {self.line_name}, " + f"站点: 第{self.station_no}站") + + +class ResponseData: + """设备B统一响应数据格式 {1:"xxxx", 2:"xxxx", 3:"xxxx", ...}""" + + @staticmethod + def create_response(station_data: StationData) -> Dict[int, str]: + """ + 根据站点数据创建响应字典 + + Args: + station_data: 接收到的站点数据 + + Returns: + 格式化的响应字典 {1: "用户名", 2: "线路名", 3: "站点号", ...} + """ + return { + 1: station_data.username, + 2: station_data.line_name, + 3: str(station_data.station_no), + 4: "已接收", + 5: "设备B确认" + } + + @staticmethod + def to_bytes(response_dict: Dict[int, str]) -> bytes: + """将响应字典转换为字节数据""" + json_str = json.dumps(response_dict, ensure_ascii=False) + return json_str.encode('utf-8') + + @staticmethod + def from_bytes(data: bytes) -> Optional[Dict[int, Any]]: + """从字节数据解析响应字典""" + try: + json_str = data.decode('utf-8') + return json.loads(json_str) + except Exception as e: + print(f"解析响应数据失败: {e}") + return None diff --git a/ck/device_a.py b/ck/device_a.py new file mode 100644 index 0000000..6e25f06 --- /dev/null +++ b/ck/device_a.py @@ -0,0 +1,103 @@ +""" +设备A示例 - 发送端 +模拟第一台设备,定期发送站点数据(用户名、线路名称、站点编号) +""" + +from serial_protocol import SerialProtocol, Command +from data_models import StationData, ResponseData +import time + + +def on_receive(cmd: int, data: bytes): + """接收数据回调函数""" + print("\n[设备A] 收到数据:") + cmd_name = (Command(cmd).name if cmd in Command._value2member_map_ + else 'UNKNOWN') + print(f" 命令: 0x{cmd:02X} ({cmd_name})") + + if cmd == Command.ACK: + print(" >>> 对方已确认接收") + elif cmd == Command.DATA_RESPONSE: + # 优先尝试解析字典响应 + response_dict = ResponseData.from_bytes(data) + if response_dict and isinstance(response_dict, dict): + print("\n 📥 收到设备B的响应字典:") + for key, value in response_dict.items(): + print(f" {key}: \"{value}\"") + else: + # 尝试解析站点数据 + station_data = StationData.from_bytes(data) + if station_data: + print(f" >>> 对方发来站点数据: {station_data}") + else: + print(f" >>> 对方发来数据: " + f"{data.decode('utf-8', errors='ignore')}") + + +def main(): + # 创建串口协议实例 + # Windows: 'COM1', 'COM2', etc. + # Linux: '/dev/ttyUSB0', '/dev/ttyS0', etc. + device = SerialProtocol(port='COM1', baudrate=115200) + + print("=" * 60) + print("设备A - 发送端") + print("=" * 60) + + # 打开串口 + if not device.open(): + print("❌ 无法打开串口") + return + + print(f"✓ 串口已打开: {device.port} @ {device.baudrate}") + + # 启动接收线程 + device.start_receive(on_receive) + print("✓ 接收线程已启动") + + try: + # 模拟线路数据 + lines = ["1号线", "2号线", "3号线", "环线"] + users = ["张三", "李四", "王五", "赵六"] + + # 模拟设备运行 + counter = 0 + while True: + counter += 1 + print(f"\n{'='*60}") + print(f"循环 {counter}") + print('='*60) + + # 1. 发送心跳包 + if device.send_heartbeat(): + print("✓ 发送心跳包") + else: + print("✗ 发送心跳包失败") + + time.sleep(2) + + # 2. 发送站点数据 + station_data = StationData( + username=users[counter % len(users)], + line_name=lines[counter % len(lines)], + station_no=counter + ) + print("\n准备发送站点数据:") + print(f" {station_data}") + + if device.send_data(station_data.to_bytes()): + print("✓ 站点数据发送成功") + else: + print("✗ 站点数据发送失败") + + time.sleep(5) + + except KeyboardInterrupt: + print("\n\n用户中断") + finally: + device.close() + print("串口已关闭") + + +if __name__ == '__main__': + main() diff --git a/ck/device_b.py b/ck/device_b.py new file mode 100644 index 0000000..a761684 --- /dev/null +++ b/ck/device_b.py @@ -0,0 +1,117 @@ +""" +设备B示例 - 接收端 +模拟第二台设备,接收站点数据(用户名、线路名称、站点编号)并应答 +""" + +from serial_protocol import SerialProtocol, Command +from data_models import StationData, ResponseData +import time +import subprocess + + +def on_receive(cmd: int, data: bytes): + """接收数据回调函数""" + print("\n" + "="*60) + print("[设备B] 收到数据") + print("="*60) + cmd_name = (Command(cmd).name if cmd in Command._value2member_map_ + else 'UNKNOWN') + print(f"命令类型: 0x{cmd:02X} ({cmd_name})") + + # 根据不同命令进行处理 + if cmd == Command.HEARTBEAT: + print(">>> 收到心跳包") + # 可以回复ACK + device.send_ack() + print("<<< 已发送ACK应答\n") + + elif cmd == Command.DATA_RESPONSE: + # 尝试解析站点数据 + station_data = StationData.from_bytes(data) + if station_data: + print("\n📍 站点数据详情:") + print(f" 用户名称: {station_data.username}") + print(f" 线路名称: {station_data.line_name}") + print(f" 站点编号: 第{station_data.station_no}站") + + # 根据站点编号执行不同逻辑 + if station_data.station_no == 0: + print("\n🚀 站点编号为0,启动 actions.py") + # 启动 actions.py + + subprocess.Popen(["python", "actions.py"], cwd="d:\\Projects\\cjgc_data") + + if station_data.station_no > 0: + print(f"\n🚀 站点编号为{station_data.station_no},启动 check_station.py") + # 启动 check_station.py + + subprocess.Popen(["python", "check_station.py"], cwd="d:\\Projects\\cjgc_data") + + # 创建统一格式的响应字典 {1:"xxx", 2:"xxx", 3:"xxx", ...} + response_dict = ResponseData.create_response(station_data) + print("\n📤 设备B统一响应格式:") + for key, value in response_dict.items(): + print(f" {key}: \"{value}\"") + + # 发送响应 + device.send_data(ResponseData.to_bytes(response_dict)) + print("\n<<< 已发送响应字典\n") + else: + # 如果不是站点数据,按普通消息处理 + message = data.decode('utf-8', errors='ignore') + print(f">>> 收到普通消息: {message}") + device.send_ack() + print("<<< 已发送ACK\n") + + elif cmd == Command.CONTROL: + if len(data) >= 1: + control_code = data[0] + params = data[1:] + print(f">>> 收到控制命令: 0x{control_code:02X}, " + f"参数: {params.hex()}") + # 执行控制逻辑... + device.send_ack() + print("<<< 已发送ACK\n") + + +# 全局变量,用于在回调中访问 +device = None + + +def main(): + global device + + # 创建串口协议实例 + # 注意: 设备B应该使用另一个串口,或者通过虚拟串口对连接 + # Windows: 'COM2', Linux: '/dev/ttyUSB1' + device = SerialProtocol(port='COM2', baudrate=115200) + + print("=" * 60) + print("设备B - 接收端") + print("=" * 60) + + # 打开串口 + if not device.open(): + print("❌ 无法打开串口") + return + + print(f"✓ 串口已打开: {device.port} @ {device.baudrate}") + + # 启动接收线程 + device.start_receive(on_receive) + print("✓ 接收线程已启动,等待接收数据...") + + try: + # 保持运行 + while True: + time.sleep(1) + + except KeyboardInterrupt: + print("\n\n用户中断") + finally: + device.close() + print("串口已关闭") + + +if __name__ == '__main__': + main() diff --git a/ck/requirements.txt b/ck/requirements.txt new file mode 100644 index 0000000..7ad05ef --- /dev/null +++ b/ck/requirements.txt @@ -0,0 +1 @@ +pyserial>=3.5 diff --git a/ck/serial_protocol.py b/ck/serial_protocol.py new file mode 100644 index 0000000..4af333c --- /dev/null +++ b/ck/serial_protocol.py @@ -0,0 +1,298 @@ +""" +串口通信协议实现 +支持两台设备之间可靠的数据传输 + +协议帧格式: ++------+------+--------+------+--------+------+ +| HEAD | LEN | CMD | DATA | CRC | TAIL | ++------+------+--------+------+--------+------+ +| 0xAA | 2B | 1B | N字节 | 2B | 0x55 | ++------+------+--------+------+--------+------+ + +HEAD: 帧头标识 (1字节, 0xAA) +LEN: 数据长度 (2字节, 大端序, 包含CMD+DATA) +CMD: 命令字 (1字节) +DATA: 数据内容 (N字节) +CRC: CRC16校验 (2字节, 大端序) +TAIL: 帧尾标识 (1字节, 0x55) +""" + +import serial +import struct +from enum import IntEnum +from typing import Optional, Callable +import threading + + +class Command(IntEnum): + """命令类型定义""" + HEARTBEAT = 0x01 # 心跳包 + DATA_QUERY = 0x02 # 数据查询 + DATA_RESPONSE = 0x03 # 数据响应 + CONTROL = 0x04 # 控制命令 + ACK = 0x05 # 应答 + NACK = 0x06 # 否定应答 + + +class SerialProtocol: + """串口协议类""" + + # 协议常量 + FRAME_HEAD = 0xAA + FRAME_TAIL = 0x55 + # 最小帧长度: HEAD(1) + LEN(2) + CMD(1) + CRC(2) + TAIL(1) + MIN_FRAME_LEN = 7 + + def __init__(self, port: str, baudrate: int = 115200, + timeout: float = 1.0): + """ + 初始化串口协议 + + Args: + port: 串口名称 (如 'COM1', '/dev/ttyUSB0') + baudrate: 波特率 + timeout: 超时时间(秒) + """ + self.port = port + self.baudrate = baudrate + self.timeout = timeout + self.serial = None + self.running = False + self.receive_thread = None + self.receive_callback: Optional[Callable] = None + + def open(self) -> bool: + """打开串口""" + try: + self.serial = serial.Serial( + port=self.port, + baudrate=self.baudrate, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + timeout=self.timeout + ) + return True + except Exception as e: + print(f"打开串口失败: {e}") + return False + + def close(self): + """关闭串口""" + self.stop_receive() + if self.serial and self.serial.is_open: + self.serial.close() + + @staticmethod + def calc_crc16(data: bytes) -> int: + """ + 计算CRC16校验值 (CRC-16/MODBUS) + + Args: + data: 需要计算校验的数据 + + Returns: + CRC16校验值 + """ + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + return crc + + def build_frame(self, cmd: int, data: bytes = b'') -> bytes: + """ + 构建数据帧 + + Args: + cmd: 命令字 + data: 数据内容 + + Returns: + 完整的数据帧 + """ + # 计算长度 (CMD + DATA) + length = 1 + len(data) + + # 构建帧体 (不含CRC和TAIL) + frame_body = (struct.pack('>BHB', self.FRAME_HEAD, length, cmd) + + data) + + # 计算CRC (对HEAD+LEN+CMD+DATA进行校验) + crc = self.calc_crc16(frame_body) + + # 添加CRC和TAIL + frame = frame_body + struct.pack('>HB', crc, self.FRAME_TAIL) + + return frame + + def parse_frame(self, frame: bytes) -> Optional[dict]: + """ + 解析数据帧 + + Args: + frame: 接收到的数据帧 + + Returns: + 解析结果字典 {'cmd': 命令字, 'data': 数据}, 失败返回None + """ + if len(frame) < self.MIN_FRAME_LEN: + return None + + # 检查帧头和帧尾 + if frame[0] != self.FRAME_HEAD or frame[-1] != self.FRAME_TAIL: + return None + + try: + # 解析长度 + length = struct.unpack('>H', frame[1:3])[0] + + # 检查帧长度是否匹配 + # HEAD + LEN + (CMD+DATA) + CRC + TAIL + expected_len = 1 + 2 + length + 2 + 1 + if len(frame) != expected_len: + return None + + # 解析命令字 + cmd = frame[3] + + # 提取数据 + data = frame[4:4+length-1] + + # 提取CRC + received_crc = struct.unpack('>H', frame[-3:-1])[0] + + # 计算CRC并校验 + calc_crc = self.calc_crc16(frame[:-3]) + if received_crc != calc_crc: + print(f"CRC校验失败: 接收={received_crc:04X}, " + f"计算={calc_crc:04X}") + return None + + return {'cmd': cmd, 'data': data} + + except Exception as e: + print(f"解析帧失败: {e}") + return None + + def send_frame(self, cmd: int, data: bytes = b'') -> bool: + """ + 发送数据帧 + + Args: + cmd: 命令字 + data: 数据内容 + + Returns: + 发送是否成功 + """ + if not self.serial or not self.serial.is_open: + print("串口未打开") + return False + + try: + frame = self.build_frame(cmd, data) + self.serial.write(frame) + return True + except Exception as e: + print(f"发送失败: {e}") + return False + + def receive_frame(self) -> Optional[dict]: + """ + 接收一帧数据 (阻塞式) + + Returns: + 解析结果字典或None + """ + if not self.serial or not self.serial.is_open: + return None + + try: + # 等待帧头 + while True: + byte = self.serial.read(1) + if not byte: + return None + if byte[0] == self.FRAME_HEAD: + break + + # 读取长度字段 + len_bytes = self.serial.read(2) + if len(len_bytes) != 2: + return None + + length = struct.unpack('>H', len_bytes)[0] + + # 读取剩余数据: CMD + DATA + CRC + TAIL + remaining = self.serial.read(length + 3) + if len(remaining) != length + 3: + return None + + # 重组完整帧 + frame = bytes([self.FRAME_HEAD]) + len_bytes + remaining + + # 解析帧 + return self.parse_frame(frame) + + except Exception as e: + print(f"接收失败: {e}") + return None + + def start_receive(self, callback: Callable[[int, bytes], None]): + """ + 启动接收线程 + + Args: + callback: 回调函数 callback(cmd, data) + """ + if self.running: + return + + self.receive_callback = callback + self.running = True + self.receive_thread = threading.Thread( + target=self._receive_loop, daemon=True) + self.receive_thread.start() + + def stop_receive(self): + """停止接收线程""" + self.running = False + if self.receive_thread: + self.receive_thread.join(timeout=2.0) + + def _receive_loop(self): + """接收循环""" + while self.running: + result = self.receive_frame() + if result and self.receive_callback: + try: + self.receive_callback(result['cmd'], result['data']) + except Exception as e: + print(f"回调函数执行失败: {e}") + + def send_heartbeat(self) -> bool: + """发送心跳包""" + return self.send_frame(Command.HEARTBEAT) + + def send_data(self, data: bytes) -> bool: + """发送数据""" + return self.send_frame(Command.DATA_RESPONSE, data) + + def send_control(self, control_code: int, + params: bytes = b'') -> bool: + """发送控制命令""" + data = bytes([control_code]) + params + return self.send_frame(Command.CONTROL, data) + + def send_ack(self) -> bool: + """发送应答""" + return self.send_frame(Command.ACK) + + def send_nack(self) -> bool: + """发送否定应答""" + return self.send_frame(Command.NACK) diff --git a/globals/__pycache__/apis.cpython-312.pyc b/globals/__pycache__/apis.cpython-312.pyc new file mode 100644 index 0000000..6b2fb5b Binary files /dev/null and b/globals/__pycache__/apis.cpython-312.pyc differ diff --git a/globals/__pycache__/create_link.cpython-312.pyc b/globals/__pycache__/create_link.cpython-312.pyc new file mode 100644 index 0000000..e173432 Binary files /dev/null and b/globals/__pycache__/create_link.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..0bbd255 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..d8b6e3b 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..c02f3b2 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..a8c901c Binary files /dev/null and b/globals/__pycache__/ids.cpython-312.pyc differ diff --git a/globals/apis.py b/globals/apis.py new file mode 100644 index 0000000..6864abc --- /dev/null +++ b/globals/apis.py @@ -0,0 +1,520 @@ +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 filter_breakpoint_list_by_status(status_codes): + """ + 根据状态码过滤断点列表,只保留line_name + + Args: + status_codes: 状态码列表,如 [0, 1] 或 [0, 1, 2, 3] + + Returns: + list: 包含line_name的列表 + """ + data = get_breakpoint_list() + + if not data: + logging.info("获取断点列表失败或列表为空") + return [] + + # 根据状态码过滤数据 + if status_codes: + filtered_data = [item for item in data if item.get('status') in status_codes] + logging.info(f"过滤后的断点数量 (状态{status_codes}): {len(filtered_data)}") + + # 按状态分组显示 + for status in status_codes: + status_count = len([item for item in filtered_data if item.get('status') == status]) + logging.info(f"状态{status}的断点数量: {status_count}") + else: + # 如果没有指定状态码,返回所有数据 + filtered_data = data + logging.info("未指定状态码,返回所有数据") + return filtered_data + +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 change_breakpoint_status(user_name, line_num, status): + """ + 修改断点状态 + + Args: + user_name: 登录账号名 + line_num: 线路编码 + status: 当前工作状态,0未开始 1操作中 2操作完成 + + Returns: + bool: 操作是否成功 + """ + try: + url = "https://engineering.yuxindazhineng.com/index/index/change" + data = { + "user_name": user_name, + "line_num": line_num, + "status": status + } + + response = requests.post(url, data=data, timeout=10) + result = response.json() + + if result.get("code") == 0: + logging.info(f"修改断点状态成功: 线路{line_num} 状态{status} - {result.get('msg')}") + return True + else: + logging.error(f"修改断点状态失败: 线路{line_num} 状态{status} - {result.get('msg')}") + return False + + except Exception as e: + logging.error(f"修改断点状态请求异常: {str(e)}") + return False + + +def get_one_addr(user_name): + """ + 根据用户名获取一个地址信息 + + Args: + user_name (str): 登录用户名 + + Returns: + dict: API的原始响应数据 + """ + # 请求地址 + url = "https://engineering.yuxindazhineng.com/index/index/getOneAddr" + + # 请求参数 + data = { + "user_name": user_name + } + + # 请求头 + headers = { + 'Content-Type': 'application/json' + } + + try: + # 发送POST请求 + response = requests.post(url,data=data,timeout=10) + + # 检查请求是否成功 + response.raise_for_status() + + # 解析返回数据的地址 + addr = response.json().get('data').get('addr') + if addr: + logging.info(f"获取到地址: {addr}") + else: + logging.warning("返回数据中未包含地址信息") + + # 直接返回API的响应 + return addr + + except requests.exceptions.RequestException as e: + # 返回错误信息 + return False + except json.JSONDecodeError as e: + return False + +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('workinfoname', ''), + "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请求 + logging.info(f"向接口 {api_url} 发送请求,查询用户名:{username}") + 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 + +def get_accounts_from_server(yh_id): + """从服务器获取账户信息""" + url = "http://www.yuxindazhineng.com:3002/api/accounts/get_uplaod_data" + headers = { + "Content-Type": "application/json" + } + data = { + "yh_id": yh_id + } + + try: + print(f"🔍 查询服务器账户信息,用户ID: {yh_id}") + response = requests.post(url, headers=headers, json=data, timeout=10) + + if response.status_code == 200: + result = response.json() + if result.get("code") == 0: + print(f"✅ 查询成功,找到 {result.get('total', 0)} 个账户") + return result.get("data", []) + else: + print(f"❌ 查询失败: {result.get('message', '未知错误')}") + return [] + else: + print(f"❌ 服务器响应错误: {response.status_code}") + return [] + except requests.exceptions.RequestException as e: + print(f"❌ 网络请求失败: {e}") + return [] + except json.JSONDecodeError as e: + print(f"❌ JSON解析失败: {e}") + return [] \ No newline at end of file diff --git a/globals/create_link.py b/globals/create_link.py new file mode 100644 index 0000000..f9d915b --- /dev/null +++ b/globals/create_link.py @@ -0,0 +1,273 @@ +import subprocess +import re +import time +import requests +import json +from appium import webdriver +from appium.webdriver.common.appiumby import AppiumBy +from appium.options.android import UiAutomator2Options +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from urllib3.connection import port_by_scheme + + +# ======================= +# 基础工具函数 +# ======================= + +def run_command(command): + """执行系统命令并返回输出""" + result = subprocess.run(command, shell=True, capture_output=True, text=True) + return result.stdout.strip() + + +# ======================= +# 无线ADB连接管理 +# ======================= + +def check_wireless_connections(target_port=4723): + """ + 检查当前无线ADB连接状态 + 返回: (list) 当前无线连接的设备列表,每个元素为(device_id, ip, port) + """ + devices_output = run_command("adb devices") + lines = devices_output.splitlines()[1:] + + wireless_connections = [] + + for line in lines: + if not line.strip(): + continue + + parts = line.split() + if len(parts) < 2: + continue + + device_id = parts[0] + status = parts[1] + + # 检查是否为无线连接(包含:端口) + if ":" in device_id and status == "device": + # 解析IP和端口 + ip_port = device_id.split(":") + if len(ip_port) == 2: + ip, port = ip_port[0], ip_port[1] + wireless_connections.append((device_id, ip, int(port))) + + return wireless_connections + +def disconnect_wireless_connection(connection_id): + """断开指定的无线ADB连接""" + print(f" 断开连接: {connection_id}") + result = run_command(f"adb disconnect {connection_id}") + return result + +def cleanup_wireless_connections(target_device_ip=None, target_port=4723): + """ + 清理无线ADB连接 + - 如果target_device_ip为None:断开所有端口为4723的连接 + - 如果target_device_ip有值:断开所有端口为4723且IP不是目标设备的连接 + 返回: (bool) 是否需要建立新连接 + """ + print("\n🔍 检查无线ADB连接状态...") + + # 获取当前所有无线连接 + wireless_connections = check_wireless_connections(target_port) + + if not wireless_connections: + print("📡 当前没有无线ADB连接") + return True # 需要建立新连接 + + print(f"📡 发现 {len(wireless_connections)} 个无线连接:") + for conn_id, ip, port in wireless_connections: + print(f" - {conn_id} (IP: {ip}, 端口: {port})") + + need_new_connection = True + connections_to_disconnect = [] + + for conn_id, ip, port in wireless_connections: + # 检查端口是否为4723 + if port != target_port: + print(f" ⚠️ 连接 {conn_id} 端口不是 {target_port},保持不动") + continue + + # 如果没有指定目标IP,断开所有4723端口的连接 + if target_device_ip is None: + connections_to_disconnect.append(conn_id) + continue + + # 如果指定了目标IP,检查IP是否匹配 + if ip == target_device_ip: + print(f" ✅ 发现目标设备的连接: {conn_id}") + need_new_connection = False # 已有正确连接,不需要新建 + else: + print(f" ⚠️ 发现其他设备的4723端口连接: {conn_id}") + connections_to_disconnect.append(conn_id) + + # 断开需要清理的连接 + for conn_id in connections_to_disconnect: + disconnect_wireless_connection(conn_id) + time.sleep(1) # 等待断开完成 + + # 如果断开了一些连接,重新检查状态 + if connections_to_disconnect: + print("🔄 重新检查连接状态...") + time.sleep(2) + remaining = check_wireless_connections(target_port) + if remaining: + for conn_id, ip, port in remaining: + if ip == target_device_ip and port == target_port: + print(f" ✅ 目标设备连接仍然存在: {conn_id}") + need_new_connection = False + break + + return need_new_connection + + +# ======================= +# Appium 启动 +# ======================= + +def start_appium(): + appium_port = 4723 + print(f"🚀 启动 Appium Server(端口 {appium_port})...") + subprocess.Popen( + ["appium.cmd", "-a", "127.0.0.1", "-p", str(appium_port)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + # 检查端口是否就绪(替代固定sleep) + max_wait = 30 # 最大等待30秒 + start_time = time.time() + while time.time() - start_time < max_wait: + try: + # 尝试连接Appium端口,验证是否就绪 + import socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + result = sock.connect_ex(("127.0.0.1", appium_port)) + sock.close() + if result == 0: # 端口就绪 + print(f"✅ Appium Server 启动成功(端口 {appium_port})") + return True + except Exception: + pass + time.sleep(1) + + print(f"❌ Appium Server 启动超时({max_wait}秒)") + return False + + +# ======================= +# 无线 ADB 建链主流程 +# ======================= + +def setup_adb_wireless(): + target_port = 4723 + print(f"🚀 开始无线 ADB 建链(端口 {target_port})") + + # 获取USB连接的设备 + devices_output = run_command("adb devices") + lines = devices_output.splitlines()[1:] + + usb_devices = [] + + for line in lines: + if not line.strip(): + continue + + parts = line.split() + if len(parts) < 2 or parts[1] != "device": + continue + + device_id = parts[0] + + # 跳过已经是无线的 + if ":" in device_id: + continue + + usb_devices.append(device_id) + + if not usb_devices: + print("❌ 未检测到 USB 设备") + return + + for serial in usb_devices: + print(f"\n🔎 处理设备: {serial}") + + # 获取WLAN IP + ip_info = run_command(f"adb -s {serial} shell ip addr show wlan0") + ip_match = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', ip_info) + + if not ip_match: + print("⚠️ 获取 IP 失败,请确认已连接 WiFi") + continue + + device_ip = ip_match.group(1) + print(f"📍 设备 IP: {device_ip}") + + # ===== 清理现有无线连接 ===== + need_new_connection = cleanup_wireless_connections( + target_device_ip=device_ip, + target_port=target_port + ) + + # ===== 建立新连接(如果需要) ===== + if need_new_connection: + print(f"\n🔌 建立新的无线连接: {device_ip}:{target_port}") + + # 切 TCP 模式 + print(f" 设置设备 {serial} 为 TCP 模式,端口 {target_port}...") + run_command(f"adb -s {serial} tcpip {target_port}") + time.sleep(3) # 等待模式切换 + + # 无线连接 + connect_result = run_command(f"adb connect {device_ip}:{target_port}") + time.sleep(2) # 等待连接稳定 + + if "connected" not in connect_result.lower(): + print(f"❌ 无线连接失败: {connect_result}") + continue + else: + print(f"✅ 无线连接成功: {device_ip}:{target_port}") + else: + print(f"✅ 已存在目标设备的有效连接,跳过新建") + + # 验证连接 + wireless_id = f"{device_ip}:{target_port}" + verify_result = run_command("adb devices") + if wireless_id in verify_result and "device" in verify_result.split(wireless_id)[1][:10]: + print(f"✅ 连接验证通过: {wireless_id}") + else: + print(f"❌ 连接验证失败,请检查") + continue + + # ===== 后续自动化 ===== + if not start_appium(): + print("❌ Appium启动失败,跳过后续操作") + continue + + # driver, app_started = start_settlement_app(wireless_id, device_ip, target_port) + + # if not app_started: + # print("⚠️ App启动失败,跳过后续操作") + # continue + + + print(f"🎉 所有操作完成! 设备 {serial} 已就绪") + + # # 关闭Appium连接 + # if driver: + # print("🔄 关闭Appium连接...") + # driver.quit() + + break # 处理完第一个设备后退出,如需处理多个设备可移除此行 + + +# ======================= +# 程序入口 +# ======================= + +if __name__ == "__main__": + # 配置参数 + setup_adb_wireless() \ No newline at end of file diff --git a/globals/driver_utils.py b/globals/driver_utils.py new file mode 100644 index 0000000..89b1a12 --- /dev/null +++ b/globals/driver_utils.py @@ -0,0 +1,1195 @@ +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 + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s") + +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秒 + + # # 添加额外的能力,避免初始化错误 + # options.set_capability('skipDeviceInitialization', True) # 跳过设备初始化,避免修改系统设置 + # options.set_capability('disableHiddenApiPolicy', True) # 禁用隐藏API策略,避免权限问题 + # options.set_capability('skipServerInstallation', True) # 跳过服务器安装,使用已有的UiAutomator2服务器 + # options.set_capability('skipUnlock', True) # 跳过解锁屏幕,避免干扰 + + try: + # driver_url = "http://127.0.0.1:4723/wd/hub" + # # 连接Appium服务器 + # logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}") + # driver = webdriver.Remote(driver_url, options=options) + # logging.info(f"设备 {device_id} Appium服务器连接成功") + driver_urls = [ + "http://127.0.0.1:4723/wd/hub", # 标准路径 + "http://127.0.0.1:4723", # 简化路径 + "http://localhost:4723/wd/hub", # localhost + ] + + driver = None + last_exception = None + + # 尝试多个URL + for driver_url in driver_urls: + try: + logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}") + driver = webdriver.Remote(driver_url, options=options) + logging.info(f"设备 {device_id} Appium服务器连接成功: {driver_url}") + break # 连接成功,跳出循环 + except Exception as e: + last_exception = e + logging.warning(f"设备 {device_id} 连接失败 {driver_url}: {str(e)[:100]}") + continue + + # 检查是否连接成功 + if not driver: + logging.error(f"设备 {device_id} 所有Appium服务器地址尝试失败") + logging.error(f"最后错误: {str(last_exception)}") + raise Exception(f"设备 {device_id} 无法连接到Appium服务器: {str(last_exception)}") + + # 初始化等待对象 + wait = WebDriverWait(driver, 20) + logging.info(f"设备 {device_id} WebDriverWait初始化成功") + + # 等待应用稳定 + time.sleep(2) + + # 设置屏幕永不休眠 + try: + # 使用ADB命令设置屏幕永不休眠 + screen_timeout_cmd = [ + "adb", "-s", device_id, + "shell", "settings", "put", "system", "screen_off_timeout", "86400000" + ] + timeout_result = subprocess.run(screen_timeout_cmd, capture_output=True, text=True, timeout=15) + if timeout_result.returncode == 0: + logging.info(f"设备 {device_id} 已成功设置屏幕永不休眠") + else: + logging.warning(f"设备 {device_id} 设置屏幕永不休眠失败: {timeout_result.stderr}") + except Exception as timeout_error: + logging.warning(f"设备 {device_id} 设置屏幕永不休眠时出错: {str(timeout_error)}") + + + 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 + """ + # 从全局变量获取设备ID + 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.warning(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.error(f"{device_str}会话已失效 (InvalidSessionIdException)") + 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服务器 + driver_urls = [ + "http://127.0.0.1:4723/wd/hub", # 标准路径 + "http://127.0.0.1:4723", # 简化路径 + "http://localhost:4723/wd/hub", # localhost + ] + + driver = None + last_exception = None + + # 尝试多个URL + for driver_url in driver_urls: + try: + logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}") + driver = webdriver.Remote(driver_url, options=options) + logging.info(f"设备 {device_id} Appium服务器连接成功: {driver_url}") + break # 连接成功,跳出循环 + except Exception as e: + last_exception = e + logging.warning(f"设备 {device_id} 连接失败 {driver_url}: {str(e)[:100]}") + continue + + # 检查是否连接成功 + if not driver: + logging.error(f"设备 {device_id} 所有Appium服务器地址尝试失败") + logging.error(f"最后错误: {str(last_exception)}") + raise Exception(f"设备 {device_id} 无法连接到Appium服务器: {str(last_exception)}") + + + # 初始化等待对象 + 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() + +# # 添加常见的Node.js路径(macOS常见问题) +# additional_paths = [ +# "/usr/local/bin", +# "/opt/homebrew/bin", # Apple Silicon Mac +# "/usr/bin", +# "/bin", +# os.path.expanduser("~/.npm-global/bin"), +# os.path.expanduser("~/node_modules/.bin"), +# # Android基础路径 +# "/Users/{}/Library/Android/sdk/platform-tools".format(os.getenv('USER')), +# ] + +# # 更新PATH环境变量 +# 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" + +# # 使用完整环境启动 +# process = subprocess.Popen( +# appium_cmd, +# shell=True, +# env=env, +# stdout=subprocess.PIPE, +# stderr=subprocess.PIPE, +# text=True +# ) + +# logging.info(f"Appium启动进程已创建,PID: {process.pid}") +# return wait_for_appium_start(port) + +# except Exception as e: +# logging.error(f"使用完整环境启动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 + +def grant_appium_permissions(device_id: str, require_all: bool = False) -> bool: + """ + 修复版:为 Appium 授予权限(使用正确的方法) + """ + logging.info(f"设备 {device_id}:开始设置Appium权限") + + # 1. 使用系统设置命令(替代原来的pm grant尝试) + logging.info("使用系统设置命令...") + system_commands = [ + ["adb", "-s", device_id, "shell", "settings", "put", "global", "window_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "transition_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "animator_duration_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "system", "screen_off_timeout", "86400000"], + ] + + success_count = 0 + for cmd in system_commands: + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功: {' '.join(cmd[3:])}") + else: + logging.warning(f" 失败: {' '.join(cmd[3:])}") + except: + logging.warning(f" 异常: {' '.join(cmd[3:])}") + + # 2. 授予可自动授予的权限 + logging.info("授予基础权限...") + grantable = [ + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.ACCESS_WIFI_STATE", + ] + + for perm in grantable: + cmd = ["adb", "-s", device_id, "shell", "pm", "grant", "io.appium.settings", perm] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功授予: {perm.split('.')[-1]}") + else: + logging.debug(f" 跳过: {perm.split('.')[-1]}") + + # 3. 返回结果 + logging.info(f"设置完成,成功项数: {success_count}") + + if require_all: + return success_count == (len(system_commands) + len(grantable)) + else: + return success_count > 0 # 只要有成功项就返回True + +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 + + +# 禁用requests的警告(访问本地接口无需SSL验证,避免控制台刷屏) +requests.packages.urllib3.disable_warnings() + +def check_appium_server_status(port=4723, timeout=30): + """ + 检测指定端口的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 InvalidSessionIdException: + # 会话已经失效,不需要重试 + 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 check_app_status(driver, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"): + """ + 检查应用状态(不跳转页面) + + 参数: + driver: WebDriver实例 + package_name: 应用包名,默认为"com.bjjw.cjgc" + activity: 应用启动Activity,默认为".activity.LoginActivity" + + 返回: + bool: 应用是否正在运行 + """ + try: + device_id = None + 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}") + + # 检查应用是否在运行 + if device_id: + # 使用ADB命令检查应用进程 + cmd = [ + "adb", "-s", device_id, + "shell", "ps", "|", "grep", package_name + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + + if package_name in result.stdout: + logging.info(f"{device_str}应用正在运行中") + return True + else: + logging.info(f"{device_str}应用未运行") + return False + else: + # 尝试使用Appium API检查应用状态 + try: + if driver: + # 获取当前包名 + current_package = driver.current_package + if current_package == package_name: + logging.info(f"{device_str}应用正在运行中(当前包名: {current_package})") + return True + else: + logging.info(f"{device_str}应用未运行(当前包名: {current_package})") + return False + except: + logging.warning(f"{device_str}无法获取当前应用状态") + return False + + except Exception as e: + logging.error(f"检查应用状态时出错: {str(e)}") + logging.error(f"错误堆栈: {traceback.format_exc()}") + return False +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, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"): + """ + 手动启动应用 + + 参数: + driver: WebDriver实例 + package_name: 应用包名,默认为"com.bjjw.cjgc" + activity: 应用启动Activity,默认为".activity.LoginActivity" + """ + try: + 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}") + + # 首先尝试使用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命令启动应用") + + # 等待应用启动 + 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): +# """ +# 跳转到主页面并点击对应的导航菜单按钮 + +# 参数: +# driver: WebDriver实例 +# device_id: 设备ID +# tabber_button_text: 导航菜单按钮的文本 + +# 返回: +# bool: 成功返回True,失败返回False +# """ +# try: +# # 检查当前是否已经在主页面 +# 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: +# # 点击返回按钮 +# # driver.press_keycode(4) # 4 是返回按钮的 keycode +# 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.error(f"设备 {device_id} 无法回到主页面,当前Activity: {current_activity}") +# return False + + +# 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) + +# return True + +# except TimeoutException: +# logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时") +# return False +# except Exception as e: +# logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}") +# return False + +# except Exception as e: +# logging.error(f"设备 {device_id} 跳转到主页面并点击菜单按钮时出错: {str(e)}") +# return False + +# 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} 次尝试执行导航操作") + +# # 确保Appium服务器正在运行 +# if not ensure_appium_server_running(): +# logging.error(f"设备 {device_id} Appium服务器未运行,启动失败") +# retry_count += 1 +# if retry_count < max_retries: +# logging.info(f"设备 {device_id} 等待2秒后重试...") +# time.sleep(2) +# continue +# else: +# logging.error(f"设备 {device_id} 达到最大重试次数,Appium服务器启动失败") +# return False + +# # 检查会话有效性 +# 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} 驱动重连失败") +# 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 + +# # 检查当前是否已经在主页面 +# 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 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}' + 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 + +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 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/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..11c1026 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..de856eb 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..48ccda6 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..10752fb 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..8a3d617 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..69e759b 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..676fc3b 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..ad5b059 --- /dev/null +++ b/page_objects/download_tabbar_page.py @@ -0,0 +1,405 @@ +# 更新基站页面操作 +# 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 + # 处理时间选择器,滚动选择年份 + if not self.handle_year_selector(): + 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 _swipe_year_wheel(self): + """滑动年份选择器的滚轮""" + try: + # 获取年份选择器滚轮元素 + year_wheel = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/wheelView1") + + # 获取滚轮的位置和尺寸 + 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 + + # 执行滑动操作 - 从中心向上滑动1/5高度 + self.driver.swipe(center_x, center_y - swipe_distance, center_x, center_y, 500) + + self.logger.info("已滑动年份选择器") + return True + + 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_year_selector(self): + """处理时间选择器,滚动选择年份""" + try: + # 等待时间选择器出现 + self.wait.until( + EC.visibility_of_element_located((AppiumBy.ID, ids.ALERT_DIALOG)) + ) + self.logger.info("时间选择对话框已出现") + + # 滚动选择年份 + if not self._swipe_year_wheel(): + 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对话框已消失,更新完成") + + 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 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..7c29a71 --- /dev/null +++ b/page_objects/login_page.py @@ -0,0 +1,179 @@ +# 登录页面操作 +# 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 subprocess + +import globals.ids as ids +import globals.global_variable as global_variable # 导入全局变量模块 +import globals.apis as apis + + + +class LoginPage: + def __init__(self, driver, wait): + self.driver = driver + self.wait = wait + + 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 is_login_page(self): + """检查当前是否为登录页面""" + try: + return self.driver.find_element(AppiumBy.ID, ids.LOGIN_BTN).is_displayed() + except NoSuchElementException: + return False + + def login(self, username=None): + """执行登录操作""" + try: + logging.info("正在执行登录操作...") + + # # 获取文本框中已有的用户名 + # username_field = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_USERNAME)) + # ) + # 获取用户名输入框 + username_field = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_USERNAME)) + ) + # 填写用户名 + if username: + # 清空用户名输入框 + try: + username_field.clear() + except: + pass + # 填写传入的用户名 + username_field.send_keys(username) + existing_username = username + logging.info(f"已填写用户名: {username}") + else: + # 获取文本框中已有的用户名 + existing_username = username_field.text + # 日志记录获取到的已有用户名(若为空,也需明确记录,避免后续误解) + if existing_username.strip(): # 去除空格后判断是否有有效内容 + logging.info(f"已获取文本框中的已有用户名: {existing_username}") + else: + logging.info("文本框中未检测到已有用户名(内容为空)") + + # 将用户名写入全局变量中 + global_variable.GLOBAL_USERNAME = existing_username # 关键:给全局变量赋值 + # global_variable.set_username(existing_username) + + + # # 读取文本框内已有的用户名(.text属性获取元素显示的文本内容) + # existing_username = username_field.text + # # 3. 将获取到的用户名写入全局变量中 + # # global_variable.GLOBAL_USERNAME = existing_username # 关键:给全局变量赋值 + # global_variable.set_username(existing_username) + + # # 日志记录获取到的已有用户名(若为空,也需明确记录,避免后续误解) + # if existing_username.strip(): # 去除空格后判断是否有有效内容 + # logging.info(f"已获取文本框中的已有用户名: {existing_username}") + # else: + # logging.info("文本框中未检测到已有用户名(内容为空)") + + # 1. 定位密码输入框 + password_field = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_PASSWORD)) + ) + + # 2. 清空密码框(如果需要) + try: + password_field.clear() + # time.sleep(0.5) # 等待清除完成 + except: + # 如果clear方法不可用,尝试其他方式 + pass + + accounts = apis.get_accounts_from_server("68ef0e02b0138d25e2ac9918") + matches = [acc for acc in accounts if acc.get("username") == existing_username] + if matches: + password = matches[0].get("password") + + password_field.send_keys(password) + + # 4. 可选:隐藏键盘 + try: + self.driver.hide_keyboard() + except: + pass + + # 点击登录按钮 + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + login_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_BTN)) + ) + login_btn.click() + logging.info(f"已点击登录按钮 (尝试 {retry_count + 1}/{max_retries})") + + # 等待登录完成 + time.sleep(3) + + # 检查是否登录成功 + if self.is_login_successful(): + logging.info("登录成功") + return True + else: + logging.warning("登录后未检测到主页面元素,准备重试") + retry_count += 1 + if retry_count < max_retries: + logging.info(f"等待2秒后重新尝试登录...") + time.sleep(2) + + logging.error(f"登录失败,已尝试 {max_retries} 次") + return False + + except Exception as e: + logging.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/measure_tabbar_page.py b/page_objects/measure_tabbar_page.py new file mode 100644 index 0000000..fffeb51 --- /dev/null +++ b/page_objects/measure_tabbar_page.py @@ -0,0 +1,403 @@ +# 测量标签栏页面操作 +# page_objects/measure_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 + +import globals.ids as ids +import globals.global_variable as global_variable # 导入全局变量模块 +from globals.driver_utils import check_session_valid, reconnect_driver, go_main_click_tabber_button # 导入会话检查和重连函数 +# import globals.driver_utils as driver_utils # 导入全局变量模块 +class MeasureTabbarPage: + def __init__(self, driver, wait,device_id): + self.driver = driver + self.wait = wait + self.logger = logging.getLogger(__name__) + self.seen_items = set() # 记录已经看到的项目,用于检测是否滚动到底部 + self.all_items = set() # 记录所有看到的项目,用于检测是否已经查看过所有项目 + # 获取设备ID用于重连操作 + self.device_id = device_id + + def is_measure_tabbar_visible(self): + """文件列表是否可见""" + try: + return self.driver.find_element(AppiumBy.ID, ids.MEASURE_LIST_ID).is_displayed() + except NoSuchElementException: + self.logger.warning("文件列表未找到") + return False + except Exception as e: + self.logger.error(f"文件列表可见性时发生意外错误: {str(e)}") + return False + + def click_measure_tabbar(self): + """点击测量标签栏""" + try: + measure_tab = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_TABBAR_ID)) + ) + measure_tab.click() + self.logger.info("已点击测量标签栏") + + time.sleep(1) + + # 等待测量页面加载完成 + self.wait.until( + lambda driver: self.is_measure_tabbar_visible() + ) + return True + except TimeoutException: + self.logger.error("等待测量标签栏可点击超时") + return False + except Exception as e: + self.logger.error(f"点击测量标签栏时出错: {str(e)}") + return False + + # def scroll_list(self): + # """滑动列表以加载更多项目""" + # try: + # # 获取列表容器 + # list_container = self.driver.find_element(AppiumBy.ID, ids.MEASURE_LIST_ID) + + # # 计算滑动坐标 + # start_x = list_container.location['x'] + list_container.size['width'] // 2 + # start_y = list_container.location['y'] + list_container.size['height'] * 0.8 + # end_y = list_container.location['y'] + list_container.size['height'] * 0.2 + + # # 执行滑动 + # self.driver.swipe(start_x, start_y, start_x, end_y, 1000) + # self.logger.info("已滑动列表") + + # # 等待新内容加载 + # time.sleep(2) + # return True + # except Exception as e: + # self.logger.error(f"滑动列表失败: {str(e)}") + # return False + def scroll_list(self, direction="down"): + """滑动列表以加载更多项目 + + Args: + direction: 滑动方向,"down"表示向下滑动,"up"表示向上滑动 + + Returns: + bool: 滑动是否成功执行,对于向上滑动,如果滑动到顶则返回False + """ + max_retry = 2 + retry_count = 0 + + while retry_count <= max_retry: + # 检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"会话无效,尝试重新连接... (尝试 {retry_count + 1}/{max_retry})") + try: + # 尝试重新连接驱动 + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("驱动重新连接成功") + except Exception as reconnect_error: + self.logger.error(f"驱动重新连接失败: {str(reconnect_error)}") + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,滑动列表失败") + return False + time.sleep(1) + continue + + 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 + 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.driver.swipe(start_x, start_y, start_x, end_y, 1000) + + # 等待新内容加载 + time.sleep(1) + + # 向上滑动时,检查是否滑动到顶 + 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: + error_msg = str(e) + self.logger.error(f"滑动列表失败: {error_msg}") + + # 如果是连接相关的错误,尝试重连 + if any(keyword in error_msg.lower() for keyword in ['socket hang up', 'could not proxy command', 'session']): + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,滑动列表失败") + return False + self.logger.warning(f"尝试重新连接后重试... (尝试 {retry_count}/{max_retry})") + time.sleep(1) + else: + # 非连接错误,直接返回False + return False + + + def get_current_items(self): + """获取当前页面中的所有项目文本""" + max_retry = 2 + retry_count = 0 + + while retry_count <= max_retry: + # 检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"会话无效,尝试重新连接... (尝试 {retry_count + 1}/{max_retry})") + try: + # 尝试重新连接驱动 + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("驱动重新连接成功") + except Exception as reconnect_error: + self.logger.error(f"驱动重新连接失败: {str(reconnect_error)}") + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,获取当前项目失败") + return [] + time.sleep(0.1) # 等待1秒后重试 + continue + + 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 + except Exception as item_error: + self.logger.warning(f"处理项目时出错: {str(item_error)}") + continue + + return item_texts + except Exception as e: + error_msg = str(e) + self.logger.error(f"获取当前项目失败: {error_msg}") + + # 如果是连接相关的错误,尝试重连 + if any(keyword in error_msg.lower() for keyword in ['socket hang up', 'could not proxy command', 'session']): + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,获取当前项目失败") + return [] + self.logger.warning(f"尝试重新连接后重试... (尝试 {retry_count}/{max_retry})") + time.sleep(1) + else: + # 非连接错误,直接返回空列表 + return [] + + def click_item_by_text(self, text): + """点击指定文本的项目""" + max_retry = 2 + retry_count = 0 + + while retry_count <= max_retry: + # 检查会话是否有效 + if not check_session_valid(self.driver, self.device_id): + self.logger.warning(f"会话无效,尝试重新连接... (尝试 {retry_count + 1}/{max_retry})") + try: + # 尝试重新连接驱动 + self.driver, self.wait = reconnect_driver(self.device_id, self.driver) + self.logger.info("驱动重新连接成功") + except Exception as reconnect_error: + self.logger.error(f"驱动重新连接失败: {str(reconnect_error)}") + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,点击项目失败") + return False + time.sleep(1) + continue + + 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 + except Exception as item_error: + self.logger.warning(f"处理项目时出错: {str(item_error)}") + continue + + self.logger.warning(f"未找到可点击的项目: {text}") + return False + except Exception as e: + error_msg = str(e) + self.logger.error(f"点击项目失败: {error_msg}") + + # 如果是连接相关的错误,尝试重连 + if any(keyword in error_msg.lower() for keyword in ['socket hang up', 'could not proxy command', 'session']): + retry_count += 1 + if retry_count > max_retry: + self.logger.error("达到最大重试次数,点击项目失败") + return False + self.logger.warning(f"尝试重新连接后重试... (尝试 {retry_count}/{max_retry})") + time.sleep(1) + else: + # 非连接错误,直接返回False + return False + + def find_keyword(self, fixed_filename): + """查找指定关键词并点击,支持向下和向上滑动查找""" + try: + # 等待线路列表容器出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_LIST_ID)) + ) + # self.logger.info("线路列表容器已找到") + + max_scroll_attempts = 50 # 最大滚动尝试次数 + scroll_count = 0 + found_items_count = 0 # 记录已找到的项目数量 + last_items_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() + + # # 记录所有看到的项目 + # self.all_items.update(current_items) + + # # 检查是否已经查看过所有项目 + # if len(current_items) > 0 and found_items_count == len(self.all_items): + # self.logger.info("已向下查看所有项目,未找到目标文件") + # break + # # return False + + # found_items_count = len(self.all_items) + + # 向下滑动列表以加载更多项目 + 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 measure_tabbar_page_manager(self): + """执行测量操作""" + try: + # 跳转到测量页面 + if not go_main_click_tabber_button(self.driver, self.device_id, "com.bjjw.cjgc:id/img_3_layout"): + logging.error(f"设备 {self.device_id} 跳转到测量页面失败") + return False + + # # 点击测量标签栏 + # if not self.click_measure_tabbar(): + # self.logger.error("点击测量标签栏失败") + # return False + + # 固定文件名 + fixed_filename = global_variable.GLOBAL_CURRENT_PROJECT_NAME + self.logger.info(f"开始查找测量数据: {fixed_filename}") + + # 重置已看到的项目集合 + self.seen_items = set() + self.all_items = set() + + # 查找并点击测量数据 + if self.find_keyword(fixed_filename): + self.logger.info("成功找到并点击测量数据") + return True + else: + self.logger.warning("未找到测量数据") + return False + + except Exception as e: + self.logger.error(f"执行测量操作时出错: {str(e)}") + 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..43e3939 --- /dev/null +++ b/page_objects/more_download_page.py @@ -0,0 +1,343 @@ +# 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 + +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/wheelView1") + + # 获取滚轮的位置和尺寸 + 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 + + # 执行滑动操作 - 从中心向上滑动1/5高度 + self.driver.swipe(center_x, center_y - swipe_distance, center_x, center_y, 500) + + self.logger.info("已滑动年份选择器") + return True + + except Exception as e: + self.logger.error(f"滑动年份选择器时出错: {str(e)}") + return False + + def wait_for_loading_dialog(self, timeout=900): + """ + 检查特定结构的加载弹窗的出现和消失 + + 参数: + timeout: 最大等待时间,默认10分钟(600秒) + + 返回: + bool: 如果加载弹窗出现并消失返回True,否则返回False + """ + try: + self.logger.info("开始检查加载弹窗...") + + # 首先检查加载弹窗是否出现 + 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 + + except Exception as e: + self.logger.error(f"检查加载弹窗时出错: {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(): + 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(): + 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..b75cf4d --- /dev/null +++ b/page_objects/screenshot_page.py @@ -0,0 +1,1907 @@ +# 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 load_line_time_mapping_dict(self, filename="20251022.1.CZSCZQ-3fhg0410.txt", log_directory="D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + # """ + # 加载指定文件中的线路编码和时间到全局字典 + # 参数: + # filename: 文件名 (例如: "20251022.1.CZSCZQ-3fhg0410.txt") + # log_directory: 文件所在目录 + # """ + + # try: + # # 获取当前年月日(例如:20251023) + # current_date = datetime.now().strftime("%Y%m%d") + # # 拼接格式:年月日.1.用户名.txt + # filename = f"{current_date}.1.{global_variable.GLOBAL_USERNAME}.txt" + # file_path = os.path.join(log_directory, filename) + # if not os.path.exists(file_path): + # self.logger.error(f"文件不存在: {file_path}") + # return + + # self.logger.info(f"正在加载文件: {file_path}") + + # # 临时字典,用于存储当前文件中的线路编码和时间 + # temp_mapping = {} + + # with open(file_path, 'r', encoding='utf-8') as f: + # for line_num, line in enumerate(f, 1): + # line = line.strip() + # # 解析日志行格式: "2025-10-22 16:22:50.171, INFO, 已修改线路时间:L205413, 结束时间:2025-10-22 09:18:17.460" + # if "已修改线路时间" in line and "结束时间" in line: + # # 提取线路编码 + # line_code_match = re.search(r'已修改线路时间[::]\s*([^,]+)', line) + # # 提取结束时间 + # end_time_match = re.search(r'结束时间[::]\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', line) + + # if line_code_match and end_time_match: + # line_code = line_code_match.group(1).strip() + # end_time_str = end_time_match.group(1) + + # # 解析日期时间 + # try: + # end_time = datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S") + # # 存入临时字典(同一个线路编码,后面的会覆盖前面的,实现取最后一条) + # temp_mapping[line_code] = end_time + # print(f"第{line_num}行: 找到线路编码 {line_code} 时间: {end_time}") + # except ValueError as e: + # print(f"第{line_num}行: 解析时间格式错误: {end_time_str}, 错误: {e}") + # continue + # else: + # print(f"第{line_num}行: 无法解析线路编码或结束时间") + + # # 将临时字典的内容更新到全局字典 + # for line_code, end_time in temp_mapping.items(): + # global_variable.LINE_TIME_MAPPING_DICT[line_code] = end_time + # print(f"更新全局字典: {line_code} -> {end_time}") + + # print(f"文件加载完成,共处理 {len(temp_mapping)} 条记录") + # print(f"当前全局字典总数: {len(global_variable.LINE_TIME_MAPPING_DICT)} 条记录") + # return True + + # except Exception as e: + # print(f"加载文件 {filename} 时出错: {str(e)}") + + def load_line_time_mapping_dict(self, filename="20251022.1.CZSCZQ-3fhg0410.txt", log_directory="D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510", poll_interval=120, max_wait_time=18000): + """ + 加载指定文件中的线路编码和时间到全局字典 + 参数: + filename: 文件名 (例如: "20251022.1.CZSCZQ-3fhg0410.txt") + log_directory: 文件所在目录 + poll_interval: 轮询间隔时间(秒),默认2分钟 + max_wait_time: 最大等待时间(秒),默认5小时 + """ + + try: + current_year_month = datetime.now().strftime("%Y%m") + # 将Logs\\后的内容替换为实际年月 + log_directory = log_directory.split("Logs\\")[0] + "Logs\\" + current_year_month + + # 获取当前年月日(例如:20251023) + current_date = datetime.now().strftime("%Y%m%d") + # 拼接格式:年月日.1.用户名.txt + filename = f"{current_date}.1.{global_variable.GLOBAL_USERNAME}.txt" + file_path = os.path.join(log_directory, filename) + + # 轮询等待文件出现 + start_time = time.time() + wait_count = 0 + + while not os.path.exists(file_path): + wait_count += 1 + elapsed_time = time.time() - start_time + + if elapsed_time >= max_wait_time: + self.logger.error(f"等待文件超时 ({max_wait_time}秒),文件仍未出现: {file_path}") + return False + + self.logger.info(f"第{wait_count}次检查 - 文件不存在,等待 {poll_interval} 秒后重试... (已等待 {elapsed_time:.0f} 秒)") + time.sleep(poll_interval) + + # 文件存在,继续处理 + self.logger.info(f"文件已找到,正在加载: {file_path}") + + # 临时字典,用于存储当前文件中的线路编码和时间 + temp_mapping = {} + + with open(file_path, 'r', encoding='utf-8') as f: + for line_num, line in enumerate(f, 1): + line = line.strip() + # 解析日志行格式: "2025-10-22 16:22:50.171, INFO, 已修改线路时间:L205413, 结束时间:2025-10-22 09:18:17.460" + if "已修改线路时间" in line and "结束时间" in line: + # 提取线路编码 + line_code_match = re.search(r'已修改线路时间[::]\s*([^,]+)', line) + # 提取结束时间 + end_time_match = re.search(r'结束时间[::]\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', line) + + if line_code_match and end_time_match: + line_code = line_code_match.group(1).strip() + end_time_str = end_time_match.group(1) + + # 解析日期时间 + try: + end_time = datetime.strptime(end_time_str, "%Y-%m-%d %H:%M:%S") + # 存入临时字典(同一个线路编码,后面的会覆盖前面的,实现取最后一条) + temp_mapping[line_code] = end_time + self.logger.info(f"第{line_num}行: 找到线路编码 {line_code} 时间: {end_time}") + except ValueError as e: + self.logger.warning(f"第{line_num}行: 解析时间格式错误: {end_time_str}, 错误: {e}") + continue + else: + self.logger.warning(f"第{line_num}行: 无法解析线路编码或结束时间") + + # 将临时字典的内容更新到全局字典 + for line_code, end_time in temp_mapping.items(): + global_variable.LINE_TIME_MAPPING_DICT[line_code] = end_time + self.logger.info(f"更新全局字典: {line_code} -> {end_time}") + + total_wait_time = time.time() - start_time + self.logger.info(f"文件加载完成,等待时间: {total_wait_time:.0f}秒,共处理 {len(temp_mapping)} 条记录") + self.logger.info(f"当前全局字典总数: {len(global_variable.LINE_TIME_MAPPING_DICT)} 条记录") + return True + + except Exception as e: + self.logger.error(f"加载文件 {filename} 时出错: {str(e)}") + return False + + 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) + + # 等待新内容加载 + time.sleep(1) + + # 向上滑动时,检查是否滑动到顶 + 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 + found_items_count = 0 # 记录已找到的项目数量 + last_items_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() + + # # 记录所有看到的项目 + # self.all_items.update(current_items) + + # # 检查是否已经查看过所有项目 + # if len(current_items) > 0 and found_items_count == len(self.all_items): + # self.logger.info("已向下查看所有项目,未找到目标文件") + # break + # # return False + + # found_items_count = len(self.all_items) + + # 向下滑动列表以加载更多项目 + 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, 5).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 False # 没有弹窗也认为是成功的 + 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, 5) + + 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 + + # 滑动列表到底部 + 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 + + 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(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 screenshot_page_manager(self, device_id, results_dir): + # """执行截图页面管理操作""" + # try: + # # 加载指定文件中的线路编码和时间到全局字典 + # if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + # self.logger.error(f"设备 {device_id} 加载线路时间映射字典失败") + # return False + + # time.sleep(5) + + # # 循环检查数据数量是否一致,直到获取到完整数据 + # retry_count = 0 + # while True: + # # 获取断点列表和线路时间字典的数量 + # breakpoint_count = len(global_variable.GLOBAL_BREAKPOINT_DICT) + # line_time_count = len(global_variable.LINE_TIME_MAPPING_DICT) + # self.logger.info(f"设备 {device_id} 断点列表数量: {breakpoint_count}, 文件中获取的线路时间数量: {line_time_count}") + + # # 如果断点列表为空,无法比较,直接跳出循环 + # if breakpoint_count == 0: + # self.logger.warning(f"设备 {device_id} 断点列表为空,无法进行数量比较") + # break + + # # 如果数量一致,获取到完整数据,跳出循环 + # if line_time_count == breakpoint_count: + # self.logger.info(f"设备 {device_id} 数据数量一致,已获取完整数据") + # break + + # # 数量不一致,等待三分钟后再次获取文件 + # retry_count += 1 + # self.logger.warning(f"设备 {device_id} 数据数量不一致: 断点列表({breakpoint_count}) != 线路时间({line_time_count}),第{retry_count}次重试,等待1分钟后重新加载文件") + # time.sleep(60) # 等待3分钟 + + # # 重新加载文件 + # if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + # self.logger.error(f"设备 {device_id} 重新加载线路时间映射字典失败") + # else: + # self.logger.info(f"设备 {device_id} 重新加载完成,新的线路时间数量: {len(global_variable.LINE_TIME_MAPPING_DICT)}") + + # # # 禁用WiFi + # # if not self.disable_wifi(device_id): + # # self.logger.error(f"设备 {device_id} 禁用WiFi失败") + # # return False + + # # 获取GLOBAL_BREAKPOINT_DICT中的断点名称和对应的线路编码 + # # 检查GLOBAL_BREAKPOINT_DICT是否为空,如果为空则初始化一些测试数据 + # if not global_variable.GLOBAL_BREAKPOINT_DICT: + # self.logger.warning("global_variable.GLOBAL_BREAKPOINT_DICT为空,正在初始化测试数据") + # # 添加一些测试断点数据,实际使用时应该从其他地方加载 + # # 注意:这里的值应该是字典,与section_mileage_config_page.py中的数据结构保持一致 + # global_variable.GLOBAL_BREAKPOINT_DICT = { + # "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区": { + # 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区", + # 'line_num': "L205413" + # }, + # "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区": { + # 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区", + # 'line_num': "L205414" + # } + # } + + # # 开始循环 + # for breakpoint_name in global_variable.GLOBAL_BREAKPOINT_DICT.keys(): + # self.logger.info(f"开始处理要平差截图的断点 {breakpoint_name}") + # # 把断点名称给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 + + # # 根据断点名称在GLOBAL_BREAKPOINT_DICT中获取对应的字典 + # breakpoint_info = global_variable.GLOBAL_BREAKPOINT_DICT.get(breakpoint_name) + # if not breakpoint_info: + # self.logger.error(f"设备 {device_id} 未找到断点 {breakpoint_name} 对应的信息") + # return False + + # # 从字典中获取线路编码 + # line_code = breakpoint_info.get('line_num') + # if not line_code: + # self.logger.error(f"设备 {device_id} 断点 {breakpoint_name} 没有线路编码信息") + # return False + + # # 根据线路编码查找对应的时间 + # date_str, time_str = self.get_line_end_time(line_code) + # if not time_str or not date_str: + # self.logger.error(f"设备 {device_id} 未找到线路 {line_code} 对应的时间") + # return False + + # # 修改时间 + # if not self.set_device_time(device_id, time_str, date_str): + # self.logger.error(f"设备 {device_id} 设置设备时间失败") + # return False + + # # 滑动列表到底部 + # 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 + + # # 检查是否在测量页面,在就重新执行选择断点,滑动列表到底部,点击最后一个spinner, 再下滑一次,点击平差处理按钮平差 + # if not self.handle_back_navigation(breakpoint_name, device_id): + # self.logger.error(f"{breakpoint_name}平差失败,未截图") + # return False + + + # # 检测并处理"是 保留成果"弹窗 + # if not self.handle_adjustment_result_dialog(): + # self.logger.error("处理平差结果弹窗失败") + # return False + + # # 平差完成,将断点数据保存到上传列表中 + # if not self.add_breakpoint_to_upload_list(breakpoint_name, line_code): + # self.logger.error(f"设备 {device_id} 保存断点 {breakpoint_name} 到上传列表失败") + # return False + + # # 平差处理完成后截图 + # time.sleep(3) # 等待平差处理按钮点击后的界面变化 + # logging.info("断点保存到上传列表成功,开始截图") + # if not self.take_screenshot(breakpoint_name): + # self.logger.error(f"设备 {device_id} 截图失败") + # return False + + # # 点击返回按钮并处理弹窗 + # if not self.execute_back_navigation_steps(device_id): + # self.logger.error(f"设备 {device_id} 处理返回按钮确认失败") + # return False + # # 启用WiFi + # if not self.enable_wifi(device_id): + # self.logger.error(f"设备 {device_id} 启用WiFi失败") + # return False + + # self.logger.info(f"设备 {device_id} 截图页面操作执行完成") + # return True + # except Exception as e: + # self.logger.error(f"设备 {device_id} 执行截图页面操作时出错: {str(e)}") + # # 保存错误截图 + # # error_screenshot_file = os.path.join( + # # results_dir, + # # f"screenshot_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 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, results_dir): + """执行截图页面管理操作""" + 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 + + # 加载指定文件中的线路编码和时间到全局字典 + if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + self.logger.error(f"设备 {device_id} 加载线路时间映射字典失败") + return False + + time.sleep(5) + + # 循环检查数据数量是否一致,直到获取到完整数据 + data_retry_count = 0 + while True: + # 获取断点列表和线路时间字典的数量 + breakpoint_count = len(global_variable.GLOBAL_BREAKPOINT_DICT) + line_time_count = len(global_variable.LINE_TIME_MAPPING_DICT) + self.logger.info(f"设备 {device_id} 断点列表数量: {breakpoint_count}, 文件中获取的线路时间数量: {line_time_count}") + + # 如果断点列表为空,无法比较,直接跳出循环 + if breakpoint_count == 0: + self.logger.warning(f"设备 {device_id} 断点列表为空,无法进行数量比较") + break + + # 如果数量一致,获取到完整数据,跳出循环 + # if line_time_count >= 2: + if line_time_count >= breakpoint_count: + self.logger.info(f"设备 {device_id} 数据数量一致,已获取完整数据") + break + + # 数量不一致,等待三分钟后再次获取文件 + data_retry_count += 1 + self.logger.warning(f"设备 {device_id} 数据数量不一致: 断点列表({breakpoint_count}) != 线路时间({line_time_count}),第{data_retry_count}次重试,等待1分钟后重新加载文件") + time.sleep(60) # 等待1分钟 + + # 重新加载文件 + if not self.load_line_time_mapping_dict("20251022.1.CZSCZQ-3fhg0410.txt", "D:\\soft\\安卓时间修改-v0.7.13-1\\Logs\\202510"): + self.logger.error(f"设备 {device_id} 重新加载线路时间映射字典失败") + else: + self.logger.info(f"设备 {device_id} 重新加载完成,新的线路时间数量: {len(global_variable.LINE_TIME_MAPPING_DICT)}") + + # 检查GLOBAL_BREAKPOINT_DICT是否为空,如果为空则初始化一些测试数据 + if not global_variable.GLOBAL_BREAKPOINT_DICT: + self.logger.warning("global_variable.GLOBAL_BREAKPOINT_DICT为空,正在初始化测试数据") + global_variable.GLOBAL_BREAKPOINT_DICT = { + "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区": { + 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK297+201-DK297+199-山区", + 'line_num': "L205413" + }, + "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区": { + 'breakpoint_name': "CZSCZQ-3-康定2号隧道-DK296+701-DK296+699-山区", + 'line_num': "L205414" + } + } + + # 创建断点列表的副本,用于重试时重新处理 + breakpoint_names = list(global_variable.GLOBAL_BREAKPOINT_DICT.keys()) + processed_breakpoints = [] + + # 开始循环处理断点 + for breakpoint_name in breakpoint_names: + if breakpoint_name in processed_breakpoints: + continue + + 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} 检查平差处理按钮失败") + continue + + # 根据断点名称在GLOBAL_BREAKPOINT_DICT中获取对应的字典 + breakpoint_info = global_variable.GLOBAL_BREAKPOINT_DICT.get(breakpoint_name) + if not breakpoint_info: + self.logger.error(f"设备 {device_id} 未找到断点 {breakpoint_name} 对应的信息") + continue + + # 从字典中获取线路编码 + line_code = breakpoint_info.get('line_num') + if not line_code: + self.logger.error(f"设备 {device_id} 断点 {breakpoint_name} 没有线路编码信息") + continue + + # 根据线路编码查找对应的时间 + date_str, time_str = self.get_line_end_time(line_code) + if not time_str or not date_str: + self.logger.error(f"设备 {device_id} 未找到线路 {line_code} 对应的时间") + continue + + # 修改时间 + if not self.set_device_time(device_id, time_str, date_str): + self.logger.error(f"设备 {device_id} 设置设备时间失败") + continue + + # 滑动列表到底部 + if not self.scroll_list_to_bottom(device_id): + self.logger.error(f"设备 {device_id} 下滑列表到底部失败") + continue + + # 2. 点击最后一个spinner + if not self.click_last_spinner_with_retry(device_id): + self.logger.error(f"设备 {device_id} 点击最后一个spinner失败") + continue + + # 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} 点击平差处理按钮失败") + 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 + + # 平差处理完成后截图 + time.sleep(3) # 等待平差处理按钮点击后的界面变化 + logging.info("断点保存到上传列表成功,开始截图") + if not self.take_screenshot(breakpoint_name): + self.logger.error(f"设备 {device_id} 截图失败") + 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): + # 启用WiFi + if not self.enable_wifi(device_id): + self.logger.error(f"设备 {device_id} 启用WiFi失败") + return False + + 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 + + # # 保存错误截图 + # error_screenshot_file = os.path.join( + # results_dir, + # f"screenshot_error_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + # ) + # try: + # self.driver.save_screenshot(error_screenshot_file) + # self.logger.info(f"错误截图已保存: {error_screenshot_file}") + # except: + # self.logger.error("保存错误截图失败") + + 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/section_mileage_config_page.py b/page_objects/section_mileage_config_page.py new file mode 100644 index 0000000..c1e8dac --- /dev/null +++ b/page_objects/section_mileage_config_page.py @@ -0,0 +1,1112 @@ +# page_objects/section_mileage_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, StaleElementReferenceException +from globals import apis, global_variable +from globals.ex_apis import get_weather_simple +import time +import logging + +import globals.ids as ids # 导入元素ID +from globals.driver_utils import check_session_valid, reconnect_driver +from check_station import CheckStation + +class SectionMileageConfigPage: + def __init__(self, driver, wait, device_id): + self.driver = driver + self.wait = wait + self.device_id = device_id + self.logger = logging.getLogger(__name__) + self.check_station_page = CheckStation(self.driver, self.wait,self.device_id) + + + def is_on_config_page(self): + """检查是否在断面里程配置页面""" + try: + title_bar = self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_TITLE_ID)) + ) + return True + except TimeoutException: + self.logger.warning("未找到断面里程配置页面的标题栏") + return False + + def scroll_to_find_element(self, element_id, max_scroll_attempts=5): + """ + 下滑页面直到找到指定元素 + + 参数: + element_id: 要查找的元素ID + max_scroll_attempts: 最大下滑次数 + + 返回: + bool: 是否找到元素 + """ + try: + for attempt in range(max_scroll_attempts): + # 检查元素是否存在 + try: + element = self.driver.find_element(AppiumBy.ID, element_id) + if element.is_displayed(): + # self.logger.info(f"找到元素: {element_id}") + return True + except NoSuchElementException: + pass + + # 如果没找到,下滑页面 + self.logger.info(f"第 {attempt + 1} 次下滑查找元素: {element_id}") + window_size = self.driver.get_window_size() + start_x = window_size['width'] // 2 + self.driver.swipe(start_x, 1600, start_x, 500, 500) + time.sleep(1) # 等待滑动完成 + + self.logger.warning(f"下滑 {max_scroll_attempts} 次后仍未找到元素: {element_id}") + return False + + except Exception as e: + self.logger.error(f"下滑查找元素时出错: {str(e)}") + return False + + def scroll_to_find_all_elements(self, element_ids, max_scroll_attempts=10): + """ + 下滑页面直到找到所有指定元素 + + 参数: + element_ids: 要查找的元素ID列表 + max_scroll_attempts: 最大下滑次数 + + 返回: + bool: 是否找到所有元素 + """ + try: + found_count = 0 + target_count = len(element_ids) + + for attempt in range(max_scroll_attempts): + # 检查每个元素是否存在 + current_found = 0 + for element_id in element_ids: + try: + element = self.driver.find_element(AppiumBy.ID, element_id) + if element.is_displayed(): + current_found += 1 + except NoSuchElementException: + pass + + if current_found > found_count: + found_count = current_found + self.logger.info(f"找到 {found_count}/{target_count} 个元素") + + if found_count == target_count: + self.logger.info(f"成功找到所有 {target_count} 个元素") + return True + + # 如果没找到全部,下滑页面 + self.logger.info(f"第 {attempt + 1} 次下滑,已找到 {found_count}/{target_count} 个元素") + window_size = self.driver.get_window_size() + start_x = window_size['width'] // 2 + self.driver.swipe(start_x, 1600, start_x, 500, 500) + time.sleep(1) # 等待滑动完成 + + self.logger.warning(f"下滑 {max_scroll_attempts} 次后仍未找到所有元素,只找到 {found_count}/{target_count} 个") + return found_count > 0 # 至少找到一个元素也算部分成功 + + except Exception as e: + self.logger.error(f"下滑查找所有元素时出错: {str(e)}") + return False + + def check_work_base_consistency(self): + """ + 判断第一个工作基点和最后一个工作基点是否一致 + + 返回: + bool: 是否一致,True表示一致,False表示不一致 + """ + try: + # 获取工作基点路径文本 + point_order_element = self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, "com.bjjw.cjgc:id/select_point_order_name")) + ) + point_order_text = point_order_element.text + # self.logger.info(f"工作基点路径: {point_order_text}") + + # 解析第一个工作基点和最后一个工作基点 + if "--->" in point_order_text: + parts = point_order_text.split("--->") + + # 提取第一个工作基点 + first_base = parts[0].strip() + # 提取最后一个工作基点 + last_base = parts[-1].strip() + + # self.logger.info(f"第一个工作基点: {first_base}") + # self.logger.info(f"最后一个工作基点: {last_base}") + + # 判断是否一致(去除可能的工作基点标识) + first_base_clean = first_base.replace("(工作基点)", "").strip() + last_base_clean = last_base.replace("(工作基点)", "").strip() + + is_consistent = first_base_clean == last_base_clean + # self.logger.info(f"工作基点一致性: {is_consistent}") + + return is_consistent + else: + self.logger.warning("无法解析工作基点路径格式") + return False + + except Exception as e: + self.logger.error(f"检查工作基点一致性时出错: {str(e)}") + return False + + def get_observation_type_based_on_base(self): + """ + 根据工作基点一致性返回观测类型 + + 返回: + str: 观测类型 + """ + try: + is_consistent = self.check_work_base_consistency() + + if is_consistent: + obs_type = "往:aBFFB 返:aBFFB" + # self.logger.info("工作基点一致,选择观测类型: 往:aBFFB 返:aBFFB") + else: + obs_type = "aBFFB" + # self.logger.info("工作基点不一致,选择观测类型: aBFFB") + + return obs_type + + except Exception as e: + self.logger.error(f"获取观测类型时出错: {str(e)}") + return "aBFFB" # 默认值 + + def select_weather(self, weather_option="阴"): + """选择天气""" + try: + # 先下滑查找天气元素 + if not self.scroll_to_find_element(ids.MEASURE_WEATHER_ID): + self.logger.error("未找到天气下拉框") + return False + + # 点击天气下拉框 + weather_spinner = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_WEATHER_ID)) + ) + weather_spinner.click() + time.sleep(1) # 等待选项弹出 + + # 等待选择对话框出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_SELECT_ID)) + ) + + # 选择指定天气选项 + weather_xpath = f"//android.widget.TextView[@resource-id='{ids.SELECT_DIALOG_TEXT1_ID}' and @text='{weather_option}']" + weather_option_element = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, weather_xpath)) + ) + weather_option_element.click() + self.logger.info(f"已选择天气: {weather_option}") + return True + except Exception as e: + self.logger.error(f"选择天气失败: {str(e)}") + return False + + def select_observation_type(self, obs_type=None): + """选择观测类型""" + try: + # 如果未指定观测类型,根据工作基点自动选择 + if obs_type is None: + obs_type = self.get_observation_type_based_on_base() + + # 先下滑查找观测类型元素 + if not self.scroll_to_find_element(ids.MEASURE_TYPE_ID): + self.logger.error("未找到观测类型下拉框") + return False + + # 点击观测类型下拉框 + obs_type_spinner = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_TYPE_ID)) + ) + obs_type_spinner.click() + time.sleep(2) # 等待选项弹出 + + # 等待选择对话框出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, ids.MEASURE_SELECT_ID)) + ) + + # 选择指定观测类型 + obs_type_xpath = f"//android.widget.TextView[@resource-id='{ids.SELECT_DIALOG_TEXT1_ID}' and @text='{obs_type}']" + obs_type_option_element = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, obs_type_xpath)) + ) + obs_type_option_element.click() + self.logger.info(f"已选择观测类型: {obs_type}") + return True + except Exception as e: + self.logger.error(f"选择观测类型失败: {str(e)}") + return False + + def enter_temperature(self, temperature="25"): + """填写温度""" + try: + # 先下滑查找温度输入框 + if not self.scroll_to_find_element(ids.MEASURE_TEMPERATURE_ID): + self.logger.error("未找到温度输入框") + return False + + # 找到温度输入框并输入温度值 + temp_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_TEMPERATURE_ID)) + ) + temp_input.clear() # 清空原有内容 + temp_input.send_keys(temperature) + # self.logger.info(f"已输入温度: {temperature}") + return True + except Exception as e: + self.logger.error(f"输入温度失败: {str(e)}") + return False + + def enter_barometric_pressure(self, pressure="800"): + """填写气压""" + try: + # 先下滑查找气压输入框 + if not self.scroll_to_find_element("com.bjjw.cjgc:id/point_list_barometric_et"): + self.logger.error("未找到气压输入框") + return False + + # 找到气压输入框并输入气压值 + pressure_input = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/point_list_barometric_et")) + ) + pressure_input.clear() # 清空原有内容 + pressure_input.send_keys(pressure) + # self.logger.info(f"已输入气压: {pressure}") + return True + except Exception as e: + self.logger.error(f"输入气压失败: {str(e)}") + return False + + def click_save_button(self): + """点击保存按钮""" + try: + # 先下滑查找保存按钮 + if not self.scroll_to_find_element(ids.MEASURE_SAVE_ID): + self.logger.error("未找到保存按钮") + return False + + save_button = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.MEASURE_SAVE_ID)) + ) + save_button.click() + self.logger.info("已点击保存按钮") + return True + except Exception as e: + self.logger.error(f"点击保存按钮失败: {str(e)}") + return False + + def click_conn_level_btn(self): + """点击连接水准仪按钮""" + try: + conn_level_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.CONNECT_LEVEL_METER)) + ) + conn_level_btn.click() + self.logger.info("已点击连接水准仪按钮1") + + # 等待设备选择对话框出现 + self.wait.until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/content")) + ) + return True + except Exception as e: + self.logger.error(f"点击连接水准仪按钮失败1: {str(e)}") + return False + + def connect_to_device(self): + """连接设备,处理可能出现的弹窗""" + try: + # 检查已配对设备列表中是否有设备 + paired_devices_list_xpath = "//android.widget.ListView[@resource-id='com.bjjw.cjgc:id/paired_devices']" + try: + # 等待配对设备列表出现 + paired_list = self.wait.until( + EC.visibility_of_element_located((AppiumBy.XPATH, paired_devices_list_xpath)) + ) + + # 获取列表中的所有子元素(设备项) + device_items = paired_list.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + + if device_items: + # 获取第一个设备的文本 + first_device_text = device_items[0].text + # self.logger.info(f"找到设备: {first_device_text}") + + + # 检查第一个设备是否不是"没有已配对的设备" + if "没有已配对的设备" not in first_device_text: + # 存在元素,点击第一个设备 + first_device = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, f"{paired_devices_list_xpath}/android.widget.TextView[1]")) + ) + first_device.click() + self.logger.info(f"已点击第一个设备: {first_device_text}") + # 处理可能出现的弹窗 + # if self._handle_alert_dialog(): + # # 弹窗已处理,重新尝试连接 + # self.logger.info("弹窗已处理,重新尝试连接设备") + # return self._retry_connection(paired_devices_list_xpath) + # else: + # # 没有弹窗,正常检查连接状态 + # return self._check_connection_status() + # 新增:最多尝试3次连接 + max_retry_times = 3 # 最大重试次数 + current_retry = 0 # 当前重试计数器(初始为0,代表第1次尝试) + + while current_retry < max_retry_times: + current_retry += 1 # 每次循环先+1,记录当前是第几次尝试 + self.logger.info(f"第 {current_retry} 次尝试连接设备(最多{max_retry_times}次)") + + if self._handle_alert_dialog(): + # 弹窗已处理,执行本次重试连接 + self.logger.info("弹窗已处理,查看按钮状态") + connect_success = self._retry_connection(paired_devices_list_xpath) + + # 若本次连接成功,立即返回True,终止重试 + if connect_success: + self.logger.info(f"第 {current_retry} 次尝试连接成功") + return True + else: + # 本次连接失败,判断是否还有剩余重试次数 + remaining_times = max_retry_times - current_retry + if remaining_times > 0: + self.logger.warning(f"第 {current_retry} 次尝试连接失败,剩余 {remaining_times} 次重试机会") + else: + self.logger.error(f"第 {current_retry} 次尝试连接失败,已达到最大重试次数({max_retry_times}次)") + else: + # 未检测到弹窗,直接尝试连接(逻辑与原代码一致,仅增加重试计数) + # self.logger.info("未检测到弹窗,尝试连接设备") + # connect_success = self._retry_connection(paired_devices_list_xpath) + connect_success = self._check_connection_status() + + if connect_success: + self.logger.info(f"第 {current_retry} 次尝试连接成功") + return True + else: + remaining_times = max_retry_times - current_retry + if remaining_times > 0: + self.logger.warning(f"第 {current_retry} 次尝试连接失败,剩余 {remaining_times} 次重试机会") + else: + self.logger.error(f"第 {current_retry} 次尝试连接失败,已达到最大重试次数({max_retry_times}次)") + # 循环结束:3次均失败,返回False + return False + else: + self.logger.info("第一个设备是'没有已配对的设备',不点击,等待用户手动连接") + return self._wait_for_manual_connection() + + else: + self.logger.info("没有找到已配对设备") + return False + + except TimeoutException: + self.logger.info("没有已配对设备;配对设备失败") + return False + + except Exception as e: + self.logger.error(f"连接设备过程中出错: {str(e)}") + return False + + def _handle_alert_dialog(self): + """处理连接蓝牙失败警告弹窗""" + try: + # 等待弹窗出现(短暂等待) + alert_dialog = WebDriverWait(self.driver, 5).until( + EC.visibility_of_element_located((AppiumBy.ID, "android:id/content")) + ) + + # 查找关闭报警按钮 + close_alert_btn = WebDriverWait(self.driver, 3).until( + EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[@text='关闭报警']")) + ) + + close_alert_btn.click() + self.logger.info("已点击'关闭报警'按钮") + + # # 等待弹窗消失 + # WebDriverWait(self.driver, 5).until( + # EC.invisibility_of_element_located((AppiumBy.ID, "android:id/content")) + # ) + # 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 _retry_connection(self, paired_devices_list_xpath): + """重新尝试连接设备""" + try: + # 再次点击连接蓝牙设备按钮 + connect_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, ids.CONNECT_LEVEL_METER)) + ) + connect_btn.click() + self.logger.info("已重新点击连接蓝牙设备按钮") + + # 等待设备列表重新出现 + paired_list = self.wait.until( + EC.visibility_of_element_located((AppiumBy.XPATH, paired_devices_list_xpath)) + ) + + # 获取设备列表并点击第一个设备 + device_items = paired_list.find_elements(AppiumBy.CLASS_NAME, "android.widget.TextView") + if device_items and "没有已配对的设备" not in device_items[0].text: + first_device = self.wait.until( + EC.element_to_be_clickable((AppiumBy.XPATH, f"{paired_devices_list_xpath}/android.widget.TextView[1]")) + ) + first_device.click() + self.logger.info("已重新点击第一个设备") + + # 检查连接状态 + return self._check_connection_status() + else: + self.logger.warning("重新尝试时未找到可用设备") + return False + + except Exception as e: + self.logger.error(f"重新尝试连接时出错: {str(e)}") + return False + + def _check_connection_status(self): + """检查连接状态""" + try: + # 等待连接状态更新 + time.sleep(3) + + conn_level_btn = self.driver.find_element(AppiumBy.ID, ids.CONNECT_LEVEL_METER) + if "已连接上" in conn_level_btn.text: + self.logger.info(f"蓝牙设备连接成功: {conn_level_btn.text}") + return True + else: + self.logger.warning(f"蓝牙设备连接失败: {conn_level_btn.text}") + return False + except NoSuchElementException: + self.logger.warning("未找到连接按钮") + return False + + def _wait_for_manual_connection(self): + """等待用户手动连接""" + max_wait_time = 60 # 总最大等待时间:600秒 + poll_interval = 5 # 每次检查后的休眠间隔:30秒 + btn_wait_time = 15 # 单个循环内,等待"连接按钮"出现的最大时间:15秒 + start_time = time.time() # 记录总等待的开始时间戳 + + while time.time() - start_time < max_wait_time: + conn_level_btn = None # 初始化按钮对象,避免上一轮残留值影响 + try: + # 第一步:先等待15秒,直到按钮出现或超时(解决按钮延迟加载问题) + conn_level_btn = WebDriverWait(self.driver, btn_wait_time).until( + EC.presence_of_element_located((AppiumBy.ID, ids.CONNECT_LEVEL_METER)) + ) + self.logger.debug("已检测到连接按钮") + + if "已连接上" in conn_level_btn.text: + self.logger.info(f"用户手动连接成功: {conn_level_btn.text}") + return True + else: + # 未连接,计算总剩余等待时间并记录 + elapsed_total = time.time() - start_time + remaining_total = max_wait_time - elapsed_total + self.logger.debug(f"等待手动连接中...总剩余时间: {remaining_total:.0f}秒") + # 情况1:10秒内未找到按钮(btn_wait_time超时) + except TimeoutException: + elapsed_total = time.time() - start_time + remaining_total = max_wait_time - elapsed_total + self.logger.warning(f"未检测到连接按钮,{remaining_total:.0f}秒内将再次检查") + + # 情况2:其他意外异常(如驱动异常) + except Exception as e: + self.logger.error(f"检查连接状态时出现意外错误: {str(e)}") + + # 无论是否找到按钮,都休眠poll_interval秒再进入下一轮循环 + time.sleep(poll_interval) + + self.logger.warning(f"等待{max_wait_time}秒后仍未连接成功,终止等待") + return False + + def wait_for_measurement_data(self, timeout=900): + """ + 等待并轮询测量数据接口,每10秒访问一次,直到有数据返回 + + 参数: + timeout: 最大等待时间(秒) + + 返回: + bool: 是否成功获取到数据 + """ + try: + start_time = time.time() + + while time.time() - start_time < timeout: + try: + task_data = apis.get_end_with_num() + if not task_data: + # self.logger.info("接口返回但没有数据,继续等待...") + time.sleep(10) + # # 1. 获取屏幕尺寸,计算中心坐标(通用适配所有设备) + # screen_size = self.driver.get_window_size() + # center_x = screen_size['width'] / 2 + # center_y = screen_size['height'] / 2 + # # 2. 点击屏幕中心(点击时长500ms,和常规操作一致) + # self.driver.tap([(center_x, center_y)], 500) + # self.logger.info(f"已点击屏幕中心(坐标:{center_x}, {center_y}),间隔30秒触发") + + continue + self.logger.info(f"接口返回数据:{task_data}") + if task_data.get('status') == 3: + self.logger.info("测量任务状态为3,测量结束") + return True + else: + self.logger.info("测量任务状态不为3,继续等待...") + continue + except Exception as e: + self.logger.error(f"处理接口响应时出错: {str(e)}") + + self.logger.error(f"在 {timeout} 秒内未获取到有效数据") + return False + + 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: + # 使用ID查找测量控制按钮 + measure_btn = self.driver.find_element( + AppiumBy.ID, + "com.bjjw.cjgc:id/btn_control_begin_or_end" + ) + + if "测量结束" in measure_btn.text: + self.logger.info("检测到测量结束按钮,暂停两秒等待电脑执行点击任务") + time.sleep(5) + 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("驱动重新初始化成功") + else: + self.logger.error("驱动重新初始化失败") + # 继续尝试,而不是立即失败 + + # 等待一段时间后再次检查 + time.sleep(5) + + # 每30秒输出一次等待状态 + if int(time.time() - start_time) % 30 == 0: + elapsed = int(time.time() - start_time) + self.logger.info(f"已等待 {elapsed} 秒,仍在等待测量结束...") + + self.logger.error("等待测量结束按钮超时") + return False + + except Exception as e: + self.logger.error(f"等待测量结束时发生严重错误: {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, 20) + + 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 click_start_measure_btn(self): + """点击开始测量按钮并处理确认弹窗""" + try: + # 查找并点击开始测量按钮 + start_measure_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/btn_control_begin_or_end")) + ) + + # 检查按钮文本 + btn_text = start_measure_btn.text + self.logger.info(f"测量按钮文本: {btn_text}") + + if "点击开始测量" in btn_text or "开始测量" in btn_text: + start_measure_btn.click() + self.logger.info("已点击开始测量按钮") + + # 处理确认弹窗 + dialog_result = self._handle_start_measure_confirm_dialog() + + # 如果确认弹窗处理成功,检查线路前测表,查看是否要添加转点 + if not dialog_result: + logging.error(f"设备 {self.device_id} 处理开始测量弹窗失败") + return False + + else: + self.logger.warning(f"测量按钮状态不是开始测量,当前状态: {btn_text}") + return False + + return True + + + except TimeoutException: + self.logger.error("等待开始测量按钮超时") + return False + except Exception as e: + self.logger.error(f"点击开始测量按钮时出错: {str(e)}") + return False + + + def add_transition_point(self): + """添加转点""" + try: + # 查找并点击添加转点按钮 + add_transition_btn = self.wait.until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/btn_add_ZPoint")) + ) + add_transition_btn.click() + 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 chang_status(self): + # """修改断点状态1->2""" + # try: + # # 修改断点状态1->2 + # user_name = global_variable.GLOBAL_USERNAME + # line_num = global_variable.GLOBAL_LINE_NUM + + # if line_num: + # success = apis.change_breakpoint_status(user_name, line_num, 2) + # if success: + # self.logger.info(f"成功修改断点状态: 线路{line_num} 状态1->2") + # return True + # else: + # self.logger.error(f"修改断点状态失败: 线路{line_num} 状态1->2") + # return False + # else: + # self.logger.warning("未找到线路编码,跳过修改断点状态") + # return False + # except Exception as e: + # self.logger.error(f"修改状态时出错: {str(e)}") + # return False + + + def _handle_start_measure_confirm_dialog(self): + """处理开始测量确认弹窗""" + try: + # 等待确认弹窗出现 + confirm_dialog = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((AppiumBy.ID, "android:id/content")) + ) + self.logger.info("检测到开始测量确认弹窗") + + # 检查弹窗标题和消息 + try: + title_element = self.driver.find_element(AppiumBy.ID, "android:id/alertTitle") + message_element = self.driver.find_element(AppiumBy.ID, "android:id/message") + self.logger.info(f"弹窗标题: {title_element.text}, 消息: {message_element.text}") + except NoSuchElementException: + self.logger.info("无法获取弹窗详细信息") + + # 点击"是"按钮 + yes_button = WebDriverWait(self.driver, 5).until( + EC.element_to_be_clickable((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/content")) + # ) + self.logger.info("确认弹窗已关闭") + return True + else: + self.logger.error(f"确认按钮文本不是'是',实际文本: {yes_button.text}") + return False + + except TimeoutException: + self.logger.warning("未检测到开始测量确认弹窗,可能不需要确认") + return True # 没有弹窗也认为是成功的 + except Exception as e: + self.logger.error(f"处理开始测量确认弹窗时出错: {str(e)}") + return False + + def click_system_back_button(self): + """点击手机系统返回按钮""" + try: + self.driver.back() + self.logger.info("已点击手机系统返回按钮") + return True + except Exception as e: + self.logger.error(f"点击手机系统返回按钮失败: {str(e)}") + return False + + def add_breakpoint_to_tested_list(self): + """添加测量结束的断点到列表和字典""" + breakpoint_name = global_variable.GLOBAL_CURRENT_PROJECT_NAME + line_num = global_variable.GLOBAL_LINE_NUM + if breakpoint_name and breakpoint_name not in global_variable.GLOBAL_TESTED_BREAKPOINT_LIST: + global_variable.GLOBAL_TESTED_BREAKPOINT_LIST.append(breakpoint_name) + global_variable.GLOBAL_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 click_back_button(self): + """点击手机系统返回按钮""" + 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} 驱动重连失败") + try: + self.driver.back() + self.logger.info("已点击手机系统返回按钮") + return True + except Exception as e: + self.logger.error(f"点击手机系统返回按钮失败: {str(e)}") + return False + + # def check_listview_stability(self, timeout=20, poll_interval=2, target_count=2): + # """ + # 检查列表视图中级2 LinearLayout数量的稳定性 + + # 参数: + # timeout: 超时时间(秒),默认20秒 + # poll_interval: 轮询间隔(秒),默认2秒 + # target_count: 目标数量,默认2个 + + # 返回: + # str: + # - "flash": 20秒内数量无变化 + # - "error": 变化后20秒内未达到目标数量 + # - "stable": 达到目标数量并保持稳定 + # """ + # listview_id = "com.bjjw.cjgc:id/auto_data_list" + # start_time = time.time() + # last_count = None + # change_detected = False + # change_time = None + + # self.logger.info(f"开始监控列表 {listview_id} 中级2 LinearLayout数量,目标数量: {target_count},超时时间: {timeout}秒") + + # try: + # while time.time() - start_time < timeout: + # try: + # # 获取当前层级2 LinearLayout数量 + # current_count = self._get_level2_linear_layout_count() + + # # 首次获取或数量发生变化 + # if last_count is None: + # self.logger.info(f"初始层级2 LinearLayout数量: {current_count}") + # last_count = current_count + # change_time = time.time() + # elif current_count != last_count: + # self.logger.info(f"层级2 LinearLayout数量发生变化: {last_count} -> {current_count}") + # last_count = current_count + # change_detected = True + # change_time = time.time() + + # # 检查是否达到目标数量 + # if current_count >= target_count: + # self.logger.info(f"已达到目标数量 {target_count},继续监控稳定性") + # # 重置计时器,继续监控是否稳定 + # start_time = time.time() + + # # 检查是否在变化后20秒内未达到目标数量 + # if change_detected and change_time and (time.time() - change_time) > timeout: + # if last_count < target_count: + # self.logger.error(f"变化后{timeout}秒内未达到目标数量{target_count},当前数量: {last_count}") + # return "error" + + # # 检查是否20秒内无变化 + # if change_time and (time.time() - change_time) > timeout: + # self.logger.info(f"层级2 LinearLayout数量在{timeout}秒内无变化,返回flash") + # return "flash" + + # time.sleep(poll_interval) + + # except StaleElementReferenceException: + # self.logger.warning("元素已过时,重新获取") + # continue + # except Exception as e: + # self.logger.error(f"获取层级2 LinearLayout数量时出错: {str(e)}") + # if change_detected and change_time and (time.time() - change_time) > timeout: + # return "error" + # else: + # time.sleep(poll_interval) + # continue + + # # 超时处理 + # if change_detected: + # if last_count >= target_count: + # self.logger.info(f"已达到目标数量{target_count}并保持稳定") + # return "stable" + # else: + # self.logger.error(f"变化后{timeout}秒内未达到目标数量{target_count},当前数量: {last_count}") + # return "error" + # else: + # self.logger.info(f"层级2 LinearLayout数量在{timeout}秒内无变化,返回flash") + # return "flash" + + # except Exception as e: + # self.logger.error(f"监控列表稳定性时出错: {str(e)}") + # return "error" + + # def _get_level2_linear_layout_count(self): + # """ + # 获取层级2 LinearLayout的数量 + + # 返回: + # int: 层级2 LinearLayout的数量 + # """ + # try: + # # 定位到ListView + # listview = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/auto_data_list") + + # # 获取层级2的LinearLayout(ListView的直接子元素) + # # 使用XPath查找直接子元素 + # level2_layouts = listview.find_elements(AppiumBy.XPATH, "./android.widget.LinearLayout") + + # count = len(level2_layouts) + # self.logger.debug(f"当前层级2 LinearLayout数量: {count}") + + # return count + + # except NoSuchElementException: + # self.logger.error(f"未找到列表元素: com.bjjw.cjgc:id/auto_data_list") + # return 0 + # except Exception as e: + # self.logger.error(f"获取层级2 LinearLayout数量时出错: {str(e)}") + # return 0 + + def handle_measurement_dialog(self): + """处理测量弹窗 - 选择继续测量""" + try: + self.logger.info("检查线路弹出测量弹窗...") + + # 直接尝试点击"继续测量"按钮 + remeasure_btn = WebDriverWait(self.driver, 2).until( + EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/measure_remeasure_all_btn")) + ) + remeasure_btn.click() + self.logger.info("已点击'重新测量'按钮") + try: + WebDriverWait(self.driver, 2).until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/button1")) + ) + except TimeoutException: + self.logger.info("未找到'是'按钮,可能弹窗未出现") + return False + + confirm_btn = WebDriverWait(self.driver, 2).until( + EC.element_to_be_clickable((AppiumBy.ID, "android:id/button1")) + ) + confirm_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 section_mileage_config_page_manager(self, address=None, obs_type=None): + """执行完整的断面里程配置""" + try: + + if not self.handle_measurement_dialog(): + self.logger.error("检测到线路测量弹窗,但处理测量弹窗失败") + return False + + self.logger.info("检测到线路测量弹窗,且处理测量弹窗成功") + + if not self.is_on_config_page(): + self.logger.error("不在断面里程配置页面") + return False + + # 下滑查找所有必要元素 + required_elements = [ + ids.MEASURE_WEATHER_ID, + ids.MEASURE_TYPE_ID, + ids.MEASURE_TEMPERATURE_ID, + "com.bjjw.cjgc:id/point_list_barometric_et" + ] + + if not self.scroll_to_find_all_elements(required_elements): + self.logger.warning("未找到所有必要元素,但继续尝试配置") + + # 临时地址 + address = apis.get_one_addr(global_variable.GLOBAL_USERNAME) or "四川省资阳市" + + + # 获取实时天气信息 + if address: + weather, temperature, pressure = get_weather_simple(address) + self.logger.info(f"获取到实时天气: {weather}, 温度: {temperature}°C, 气压: {pressure}hPa") + else: + # 使用默认值 + weather, temperature, pressure = "阴", 25.0, 720.0 + + # 选择天气 + if not self.select_weather(weather): + return False + + time.sleep(1) # 短暂等待 + + # 选择观测类型(如果未指定,根据工作基点自动选择) + if not self.select_observation_type(obs_type): + return False + + time.sleep(1) # 短暂等待 + + # 填写温度 + if not self.enter_temperature(temperature): + return False + + time.sleep(1) # 短暂等待 + + # 填写气压 + if not self.enter_barometric_pressure(pressure): + return False + + time.sleep(1) # 短暂等待 + + + # 点击保存 + if not self.click_save_button(): + return False + + # 连接水准仪 + if not self.click_conn_level_btn(): + return False + + # 连接蓝牙设备 + if not self.connect_to_device(): + return False + + # 连接设备成功,要点击“点击开始测量” + if not self.click_start_measure_btn(): + return False + + # if not self.check_station_page.run(): + # self.logger.error("检查站页面运行失败") + # return False + + + # # 添加断点到列表 + # if not self.add_breakpoint_to_tested_list(): + # return False + + # # 点击返回按钮 + # if not self.click_back_button(): + # return False + + # # 测量结束。点击手机物理返回按钮,返回测量页面 + # # 点击了手机独步导航栏返回键 + # if not self.click_system_back_button(): + # return False + + 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/upload_config_page.py b/page_objects/upload_config_page.py new file mode 100644 index 0000000..faf178e --- /dev/null +++ b/page_objects/upload_config_page.py @@ -0,0 +1,2227 @@ +#上传配置页面 +# \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: + # 直接使用固定的X坐标(屏幕中间) + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + x = 540 # 假设屏幕宽度为 1080,取中间值 + + # 执行滑动操作 + 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: + # 直接使用固定的X坐标(屏幕中间) + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + x = 540 # 假设屏幕宽度为 1080,取中间值 + + # 执行滑动操作(向下滑动) + 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 _select_work_condition(self, condition_dict: Dict[str, List[str]]): + # logging.info(f"开始选择工况信息: {condition_dict}") + # """选择工况信息""" + # # 所有可能的工况选择按钮ID + # condition_button_ids = [ + # "com.bjjw.cjgc:id/all_point_workinfo_sp_qiaoliang", # 梁桥 + # "com.bjjw.cjgc:id/all_point_workinfo_sp_luji", # 路基 + # "com.bjjw.cjgc:id/all_point_workinfo_sp_handong", # 涵洞 + # "com.bjjw.cjgc:id/all_point_workinfo_sp_suidao" # 隧道 + # ] + # try: + # # 遍历所有可能的工况选择按钮 + # for button_id in condition_button_ids: + # try: + # # 点击"选择工况信息"按钮 + # condition_button = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, button_id)) + # ) + # condition_button.click() + # logging.info(f"成功点击工况选择按钮: {button_id}") + + # # 选择指定的工况选项 + # if self._select_condition_option(condition): + # logging.info(f"成功选择工况: {condition} 对于按钮: {button_id}") + # else: + # logging.warning(f"未能选择工况: {condition} 对于按钮: {button_id}") + # # 继续尝试其他按钮,不立即返回失败 + + # except TimeoutException: + # logging.debug(f"未找到或无法点击工况选择按钮: {button_id},继续尝试下一个") + # continue + # except Exception as e: + # logging.warning(f"点击按钮 {button_id} 时出错: {str(e)},继续尝试下一个") + # continue + + # return True + # except TimeoutException: + # self.logger.error("等待工况信息选择超时") + # return False + # except Exception as e: + # self.logger.error(f"选择工况信息时出错: {str(e)}") + # return False + + # def _select_condition_option(self, condition, max_scroll_attempts=5): + # """选择具体的工况选项""" + # try: + # # 滑动查找指定的工况选项 + # for attempt in range(max_scroll_attempts): + # try: + # # 尝试查找并点击指定的工况选项 + # condition_option = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.XPATH, + # f"//android.widget.TextView[@resource-id='android:id/text1' and @text='{condition}']")) + # ) + # condition_option.click() + # logging.info(f"成功选择工况选项: {condition}") + # return True + + # except TimeoutException: + # logging.debug(f"第 {attempt + 1} 次查找未找到工况选项: {condition},尝试滑动") + # # 执行滑动操作 + # self._scroll_to_find_condition() + + # logging.error(f"经过 {max_scroll_attempts} 次滑动仍未找到工况选项: {condition}") + # return False + + # except Exception as e: + # logging.error(f"选择工况选项时出错: {str(e)}") + # return False + + # 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 + + # # 选择工况信息 - 传入condition_dict + # 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)}") + # 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, condition_dict: Dict[str, List[str]]): + # """根据工况字典选择工况信息 + + # Args: + # condition_dict: 工况字典,格式为 {work_type: [workinfoname1, workinfoname2, ...]} + # work_type: 工点类型编码(1-隧道,2-区间路基,3-桥, 4-涵洞) + # """ + # self.logger.info(f"开始选择工况信息: {condition_dict}") + + # # 工点类型编码与界面控件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 + # total_count = len(condition_dict) + + # # 遍历condition_dict中的每个工点类型 + # for work_type, workinfo_names in condition_dict.items(): + # # 确保work_type是字符串类型,与映射表匹配 + # 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 len(workinfo_names) > 1: + # self.logger.info(f"{work_type_name}有{len(workinfo_names)-1}个次要工况需要处理: {workinfo_names[1:]}") + # self._handle_minor_work_conditions(work_type_str, workinfo_names[1:]) + # else: + # self.logger.warning(f"工点类型{work_type_name}({work_type_str})没有可用的工况名称") + + # self.logger.info(f"工况选择完成: 成功{success_count}/{total_count}个工点类型") + # return success_count > 0 # 只要有一个成功就返回True + + # 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 _select_minor_conditions_option(self, option_name: str, work_type_name: str) -> bool: + # """根据测点名字选择对应的下拉列表中的选项""" + # try: + # # 找到选项元素 + # option = self.wait.until( + # EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/point_pstate_sp")) + # ) + # option.click() + # # 选择次要的工况选项 + # if self._select_condition_option(option_name): + # self.logger.info(f"成功为{option_name}选择次要工况: {work_type_name}") + # success_count += 1 + # return True + # except TimeoutException: + # self.logger.error(f"未找到选项: {option_name}") + # return False + # 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: + # 直接使用固定的坐标值 + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + start_x = 540 # 假设屏幕宽度为 1080,取中间值 + start_y = 1400 # 假设屏幕高度为 2000,取 70% 位置 + end_y = 600 # 假设屏幕高度为 2000,取 30% 位置 + + # 执行滑动 + 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 _handle_minor_work_conditions(self, minor_conditions_list: List[Dict]) -> int: + # """处理次要工况:滑动页面,找到对应测点并设置工况""" + # success_count = 0 + # processed_points = set() # 记录已处理的测点,避免重复处理 + # logging.info(f"要处理的次要工况:{minor_conditions_list}") + + # try: + + # while True: + # points_on_page_before = [] + # # 获取当前页面的测点 + # points_on_page_after = self.collect_points_on_page() + # self.logger.info(f"当前页面共找到 {len(points_on_page_after)} 个测点") + # if points_on_page_after == points_on_page_before: + # self.logger.info("当前页面没有新的测点,停止滑动") + # break + # else: + # points_on_page_before = points_on_page_after + # # 如果当前页面测点在次要工况字典中,则进行点击"com.bjjw.cjgc:id/point_workinfo_sp",选择对应的工况 + # _select_minor_conditions_option + + + + + # self.swipe_down() + + # # # 滑动收集页面中的所有测点 + # # all_points_on_page = self.collect_all_points_on_page() + # # self.logger.info(f"页面中共找到 {len(all_points_on_page)} 个测点") + + # # # 为每个次要工况找到对应的测点并设置工况 + # # for minor_condition in minor_conditions_list: + # # point_id = minor_condition['point_id'] + # # target_workinfoname = minor_condition['workinfoname'] + # # work_type = minor_condition['work_type'] + + # # if point_id in processed_points: + # # continue + + # # # 在页面中查找对应的测点 + # # target_point = None + # # for point_data in all_points_on_page: + # # if point_data.get('point_id') == point_id: + # # target_point = point_data + # # break + + # # if target_point: + # # # 找到测点,设置工况 + # # if self._set_single_point_work_condition(target_point, target_workinfoname, work_type): + # # success_count += 1 + # # processed_points.add(point_id) + # # self.logger.info(f"成功为测点 {point_id} 设置次要工况: {target_workinfoname}") + # # else: + # # self.logger.warning(f"为测点 {point_id} 设置次要工况失败") + # # else: + # # self.logger.warning(f"未在页面中找到测点 {point_id},可能不在当前视图内") + # # return success_count + + # except Exception as e: + # self.logger.error(f"处理次要工况时出错: {str(e)}") + # return success_count + + 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: + # 直接使用固定的屏幕中央坐标 + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + x = 540 # 假设屏幕宽度为 1080,取中间值 + y = 1000 # 假设屏幕高度为 2000,取中间值 + + # 点击屏幕中央(假设选项在中间) + 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: + # 直接使用固定的坐标值 + # 避免调用 get_window_size() 方法,防止 Appium 服务器崩溃 + start_x = 540 # 假设屏幕宽度为 1080,取中间值 + start_y = 1400 # 假设屏幕高度为 2000,取 70% 位置 + end_y = 600 # 假设屏幕高度为 2000,取 30% 位置 + + # 执行滑动 + 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: Optional[Dict[str, Dict]]) -> List[Dict[str, str]]: + # """ + # 解析工况信息,获取所有work_type和对应的workinfoname + + # Args: + # work_conditions: 从get_work_conditions_by_linecode获取的工况数据 + + # Returns: + # 返回工况列表,格式为 [{"work_type": "1", "workinfoname": "墩台混凝土施工"}, ...] + # """ + # condition_list = [] + + # if not work_conditions: + # logging.warning("工况数据为空") + # return condition_list + + # try: + # # 使用集合来去重,避免重复的工况组合 + # unique_conditions = set() + + # for point_id, condition_data in work_conditions.items(): + # work_type = condition_data.get('work_type', '') + # workinfoname = condition_data.get('workinfoname', '') + + # # 过滤空值 + # if work_type and workinfoname: + # # 创建唯一标识符 + # condition_key = f"{work_type}_{workinfoname}" + + # if condition_key not in unique_conditions: + # unique_conditions.add(condition_key) + # condition_list.append({ + # "work_type": work_type, + # "workinfoname": workinfoname + # }) + + # logging.info(f"解析出 {len(condition_list)} 种不同的工况组合") + # return condition_list + + # except Exception as e: + # logging.error(f"解析工况信息时发生错误: {str(e)}") + # return [] + + 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 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 + # 下载操作完成后,点击上传导航按钮跳转到上传页面执行更多下载操作并检查状态 + 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 + + # # 解析为需要的列表格式 + # condition_list = self.parse_work_conditions(work_conditions) + # self.logger.info(f"设备获取到的工作条件列表为:{condition_list}") + + # # 转换为condition_dict格式(如果需要) + # condition_dict = {} + # for condition in condition_list: + # work_type = condition['work_type'] + # workinfoname = condition['workinfoname'] + # if work_type not in condition_dict: + # condition_dict[work_type] = [] + # if workinfoname not in condition_dict[work_type]: + # condition_dict[work_type].append(workinfoname) + + # self.logger.info(f"设备获取到的工作条件字典为:{condition_dict}") + + # # 设置所有测点并填写表单 + # if not self.set_all_points_and_fill_form(results_dir, user_info.get("name"), user_info.get("id_card"), condition_dict): + # self.logger.error("设置所有测点并填写表单失败") + # 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 + + # 等待上传,查看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..a225c3b --- /dev/null +++ b/permissions.py @@ -0,0 +1,279 @@ +# 权限处理 +import subprocess +import logging +import time +import os + +# 配置日志 +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) -> bool: +# """ +# 为 Appium UiAutomator2 服务授予 WRITE_SECURE_SETTINGS 权限 +# :param device_id: 设备 ID(可通过 `adb devices` 查看) +# :return: 权限授予是否成功 +# """ +# # 首先检查设备连接 +# 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 + +# # 检查并授予权限 +# for package in packages_to_grant: +# if not is_package_installed(device_id, package): +# logging.warning(f"设备 {device_id}:包 {package} 未安装,跳过权限授予") +# continue + +# for permission in permissions_to_grant: +# total_attempted += 1 +# result = grant_single_permission(device_id, package, permission) +# if result: +# success_count += 1 + +# try: +# grant_cmd = [ +# "adb", "-s", device_id, +# "shell", "pm", "grant", package, +# "android.permission.WRITE_SECURE_SETTINGS" +# ] +# result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15) + +# if result.returncode == 0: +# logging.info(f"设备 {device_id}:已成功授予 {package} 权限") +# else: +# logging.warning(f"设备 {device_id}:授予 {package} 权限失败: {result.stderr.strip()}") +# # 尝试使用root权限 +# if "security" in result.stderr.lower() or "permission" in result.stderr.lower(): +# logging.info(f"设备 {device_id}:尝试使用root权限授予 {package} 权限") +# 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} 权限") +# else: +# logging.error(f"设备 {device_id}:即使使用root权限也无法授予 {package} 权限: {result.stderr.strip()}") + +# 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()}") +# except Exception as e: +# logging.error(f"设备 {device_id}:处理 {package} 时发生未知错误:{str(e)}") + +# # 最终验证 +# logging.info(f"设备 {device_id}:权限授予过程完成,建议重启设备或Appium服务使更改生效") +# return True + +def grant_appium_permissions(device_id: str, require_all: bool = False) -> bool: + """ + 修复版:为 Appium 授予权限(使用正确的方法) + """ + logging.info(f"设备 {device_id}:开始设置Appium权限") + + # 1. 使用系统设置命令(替代原来的pm grant尝试) + logging.info("使用系统设置命令...") + system_commands = [ + ["adb", "-s", device_id, "shell", "settings", "put", "global", "window_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "transition_animation_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "global", "animator_duration_scale", "0"], + ["adb", "-s", device_id, "shell", "settings", "put", "system", "screen_off_timeout", "86400000"], + ] + + success_count = 0 + for cmd in system_commands: + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功: {' '.join(cmd[3:])}") + else: + logging.warning(f" 失败: {' '.join(cmd[3:])}") + except: + logging.warning(f" 异常: {' '.join(cmd[3:])}") + + # 2. 授予可自动授予的权限 + logging.info("授予基础权限...") + grantable = [ + "android.permission.INTERNET", + "android.permission.ACCESS_NETWORK_STATE", + "android.permission.ACCESS_WIFI_STATE", + ] + + for perm in grantable: + cmd = ["adb", "-s", device_id, "shell", "pm", "grant", "io.appium.settings", perm] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + success_count += 1 + logging.info(f" 成功授予: {perm.split('.')[-1]}") + else: + logging.debug(f" 跳过: {perm.split('.')[-1]}") + + # 3. 返回结果 + logging.info(f"设置完成,成功项数: {success_count}") + + if require_all: + return success_count == (len(system_commands) + len(grantable)) + else: + return success_count > 0 # 只要有成功项就返回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}") + grant_appium_permissions(device_id) + else: + logging.error("无法获取设备列表,请确保ADB已正确安装且设备已连接") \ No newline at end of file diff --git a/test/__pycache__/protocol.cpython-312.pyc b/test/__pycache__/protocol.cpython-312.pyc new file mode 100644 index 0000000..be18691 Binary files /dev/null and b/test/__pycache__/protocol.cpython-312.pyc differ diff --git a/test/control.py b/test/control.py new file mode 100644 index 0000000..e4333a4 --- /dev/null +++ b/test/control.py @@ -0,0 +1,23 @@ +import socket +from protocol import build_frame + +SERVER_IP = '127.0.0.1' +SERVER_PORT = 9000 + +TARGET_DEVICE = 10000052 + +# 控制 2、3 开;5 关 +payload = bytes([ + 0x02, 0x01, + 0x03, 0x01, + 0x05, 0x00 +]) + +frame = build_frame(0xA0, TARGET_DEVICE, payload) + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((SERVER_IP, SERVER_PORT)) + # CTRL + device_id + frame + msg = b'CTRL' + TARGET_DEVICE.to_bytes(4, 'little') + frame + s.sendall(msg) + print("[CONTROL] send command") diff --git a/test/device.py b/test/device.py new file mode 100644 index 0000000..cdb06de --- /dev/null +++ b/test/device.py @@ -0,0 +1,29 @@ +import socket +import time +from protocol import build_frame + +SERVER_IP = '127.0.0.1' +SERVER_PORT = 9000 + +DEVICE_ID = 10000052 # 每个设备改这个 + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((SERVER_IP, SERVER_PORT)) + print(f"[DEVICE {DEVICE_ID}] connected") + + while True: + # 发送心跳 + hb = build_frame(0x00, DEVICE_ID) + s.sendall(hb) + + # 接收控制指令 + s.settimeout(1) + try: + data = s.recv(1024) + if data and data[8] == 0xA0: + print(f"[DEVICE {DEVICE_ID}] recv A0:", data.hex(' ')) + # 这里执行你的 IO / GPIO + except socket.timeout: + pass + + time.sleep(3) diff --git a/test/protocol.py b/test/protocol.py new file mode 100644 index 0000000..0dd57bb --- /dev/null +++ b/test/protocol.py @@ -0,0 +1,22 @@ +def sum16(data: bytes) -> int: + return sum(data) & 0xFFFF + + +def build_frame(cmd: int, device_id: int, payload: bytes = b"") -> bytes: + frame = bytearray() + frame += b'\x23\xA9' # 帧头 + frame += b'\x00\x00' # 长度占位 + frame += device_id.to_bytes(4, 'little') + frame += bytes([cmd]) + frame += payload + + length = len(frame) + 2 # 加上校验和 + frame[2:4] = length.to_bytes(2, 'big') + + s = sum16(frame) + frame += s.to_bytes(2, 'big') + return bytes(frame) + + +def parse_device_id(frame: bytes) -> int: + return int.from_bytes(frame[4:8], 'little') diff --git a/test/server.py b/test/server.py new file mode 100644 index 0000000..9ac693e --- /dev/null +++ b/test/server.py @@ -0,0 +1,41 @@ +import socket +import threading + +HOST = "127.0.0.1" +PORT = 9000 + +devices = {} # device_id -> socket + +def handle_client(conn): + while True: + data = conn.recv(1024) + if not data: + break + + # 前 4 字节当 device_id(小端) + device_id = int.from_bytes(data[4:8], "little") + print("[SERVER] recv for device:", device_id, data.hex(" ")) + + if device_id in devices: + devices[device_id].send(data) + print("[SERVER] forwarded to device", device_id) + +def accept_loop(): + s = socket.socket() + s.bind((HOST, PORT)) + s.listen() + print("[SERVER] listening") + + while True: + conn, _ = s.accept() + + # 第一次 recv 认为是设备注册 + first = conn.recv(1024) + if first.startswith(b"DEVICE"): + device_id = int(first.split(b":")[1]) + devices[device_id] = conn + print(f"[SERVER] device {device_id} registered") + else: + threading.Thread(target=handle_client, args=(conn,), daemon=True).start() + +accept_loop() diff --git a/test/test_play_data.py b/test/test_play_data.py new file mode 100644 index 0000000..7c60010 --- /dev/null +++ b/test/test_play_data.py @@ -0,0 +1,149 @@ +import time +import requests +import pandas as pd +from io import BytesIO + + +class CheckStation: + def __init__(self): + self.station_num = 0 + self.last_data = None + + def get_measure_data(self): + # 模拟获取测量数据 + pass + + def add_transition_point(self): + # 添加转点逻辑 + print("添加转点") + return True + + def get_excel_from_url(self, url): + """ + 从URL获取Excel文件并解析为字典 + Excel只有一列数据(A列),每行是站点值 + + Args: + url: Excel文件的URL地址 + + Returns: + dict: 解析后的站点数据字典 {行号: 值},失败返回None + """ + try: + print(f"正在从URL获取数据: {url}") + response = requests.get(url, timeout=30) + response.raise_for_status() # 检查请求是否成功 + + # 使用pandas读取Excel数据,指定没有表头,只读第一个sheet + excel_data = pd.read_excel( + BytesIO(response.content), + header=None, # 没有表头 + sheet_name=0, # 只读取第一个sheet + dtype=str # 全部作为字符串读取 + ) + + station_dict = {} + + # 解析Excel数据:使用行号+1作为站点编号,A列的值作为站点值 + print("解析Excel数据(使用行号作为站点编号)...") + for index, row in excel_data.iterrows(): + station_num = index + 1 # 行号从1开始作为站点编号 + station_value = str(row[0]).strip() if pd.notna(row[0]) else "" + + if station_value: # 只保存非空值 + station_dict[station_num] = station_value + + print(f"成功解析Excel,共{len(station_dict)}条数据") + return station_dict + + except requests.exceptions.RequestException as e: + print(f"请求URL失败: {e}") + return None + except Exception as e: + print(f"解析Excel失败: {e}") + return None + + def check_station_exists(self, station_data: dict, station_num: int) -> str: + """ + 根据站点编号检查该站点的值是否以Z开头 + + Args: + station_data: 站点数据字典 {编号: 值} + station_num: 要检查的站点编号 + + Returns: + str: 如果站点存在且以Z开头返回"add",否则返回"pass" + """ + if station_num not in station_data: + print(f"站点{station_num}不存在") + return "error" + + value = station_data[station_num] + str_value = str(value).strip() + is_z = str_value.upper().startswith('Z') + + result = "add" if is_z else "pass" + print(f"站点{station_num}: {value} -> {result}") + return result + + + def run(self): + last_station_num = 0 + + url = f"https://database.yuxindazhineng.com/team-bucket/69378c5b4f42d83d9504560d/前测点表/20260309/CDWZQ-2标-龙家沟左线大桥-0-11号墩-平原.xlsx" + station_data = self.get_excel_from_url(url) + print(station_data) + station_quantity = len(station_data) + over_station_num = 0 + over_station_list = [] + while over_station_num < station_quantity: + try: + # 键盘输出线路编号 + station_num_input = input("请输入线路编号:") + if not station_num_input.isdigit(): # 检查输入是否为数字 + print("输入错误:请输入一个整数") + continue + station_num = int(station_num_input) # 转为整数 + + if station_num in over_station_list: + print("已处理该站点,跳过") + continue + + if last_station_num == station_num: + print("输入与上次相同,跳过处理") + continue + last_station_num = station_num + + result = self.check_station_exists(station_data, station_num) + if result == "error": + print("处理错误:站点不存在") + # 错误处理逻辑,比如记录日志、发送警报等 + elif result == "add": + print("执行添加操作") + # 添加转点 + if not self.add_transition_point(): + print("添加转点失败") + # 可以决定是否继续循环 + continue + over_station_num += 1 + else: # result == "pass" + print("跳过处理") + over_station_num += 1 + + over_station_list.append(station_num) + + # 可以添加适当的延时,避免CPU占用过高 + # time.sleep(1) + + except KeyboardInterrupt: + print("程序被用户中断") + break + except Exception as e: + print(f"发生错误: {e}") + time.sleep(20) + # 错误处理,可以继续循环或退出 + print(f"已处理{over_station_num}个站点") + +if __name__ == "__main__": + monitor = StationMonitor() + monitor.run() \ No newline at end of file