first commit
This commit is contained in:
BIN
CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx
Normal file
BIN
CZSCZQ-11-五工区-德达隧道出口-DK637+524-DK637+403-山区.xlsx
Normal file
Binary file not shown.
BIN
__pycache__/check_station.cpython-312.pyc
Normal file
BIN
__pycache__/check_station.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/driver_utils.cpython-312.pyc
Normal file
BIN
__pycache__/driver_utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/permissions.cpython-312.pyc
Normal file
BIN
__pycache__/permissions.cpython-312.pyc
Normal file
Binary file not shown.
302
actions.py
Normal file
302
actions.py
Normal file
@@ -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)
|
||||
BIN
check_station.png
Normal file
BIN
check_station.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
211
check_station.py
Normal file
211
check_station.py
Normal file
@@ -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()
|
||||
25
ck/.gitignore
vendored
Normal file
25
ck/.gitignore
vendored
Normal file
@@ -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/
|
||||
292
ck/README.md
Normal file
292
ck/README.md
Normal file
@@ -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
|
||||
82
ck/data_models.py
Normal file
82
ck/data_models.py
Normal file
@@ -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
|
||||
103
ck/device_a.py
Normal file
103
ck/device_a.py
Normal file
@@ -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()
|
||||
117
ck/device_b.py
Normal file
117
ck/device_b.py
Normal file
@@ -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()
|
||||
1
ck/requirements.txt
Normal file
1
ck/requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pyserial>=3.5
|
||||
298
ck/serial_protocol.py
Normal file
298
ck/serial_protocol.py
Normal file
@@ -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)
|
||||
BIN
globals/__pycache__/apis.cpython-312.pyc
Normal file
BIN
globals/__pycache__/apis.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/create_link.cpython-312.pyc
Normal file
BIN
globals/__pycache__/create_link.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/driver_utils.cpython-312.pyc
Normal file
BIN
globals/__pycache__/driver_utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/ex_apis.cpython-312.pyc
Normal file
BIN
globals/__pycache__/ex_apis.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/global_variable.cpython-312.pyc
Normal file
BIN
globals/__pycache__/global_variable.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/ids.cpython-312.pyc
Normal file
BIN
globals/__pycache__/ids.cpython-312.pyc
Normal file
Binary file not shown.
520
globals/apis.py
Normal file
520
globals/apis.py
Normal file
@@ -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 []
|
||||
273
globals/create_link.py
Normal file
273
globals/create_link.py
Normal file
@@ -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()
|
||||
1195
globals/driver_utils.py
Normal file
1195
globals/driver_utils.py
Normal file
File diff suppressed because it is too large
Load Diff
298
globals/ex_apis.py
Normal file
298
globals/ex_apis.py
Normal file
@@ -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)
|
||||
17
globals/global_variable.py
Normal file
17
globals/global_variable.py
Normal file
@@ -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 = [] # 上传成功的`断点列表
|
||||
|
||||
|
||||
|
||||
|
||||
59
globals/ids.py
Normal file
59
globals/ids.py
Normal file
@@ -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"
|
||||
|
||||
BIN
page_objects/__pycache__/download_tabbar_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/download_tabbar_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/login_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/login_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/more_download_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/more_download_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/screenshot_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/screenshot_page.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
page_objects/__pycache__/upload_config_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/upload_config_page.cpython-312.pyc
Normal file
Binary file not shown.
46
page_objects/call_xie.py
Normal file
46
page_objects/call_xie.py
Normal file
@@ -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")
|
||||
405
page_objects/download_tabbar_page.py
Normal file
405
page_objects/download_tabbar_page.py
Normal file
@@ -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
|
||||
179
page_objects/login_page.py
Normal file
179
page_objects/login_page.py
Normal file
@@ -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
|
||||
403
page_objects/measure_tabbar_page.py
Normal file
403
page_objects/measure_tabbar_page.py
Normal file
@@ -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
|
||||
343
page_objects/more_download_page.py
Normal file
343
page_objects/more_download_page.py
Normal file
@@ -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
|
||||
1907
page_objects/screenshot_page.py
Normal file
1907
page_objects/screenshot_page.py
Normal file
File diff suppressed because it is too large
Load Diff
1112
page_objects/section_mileage_config_page.py
Normal file
1112
page_objects/section_mileage_config_page.py
Normal file
File diff suppressed because it is too large
Load Diff
2227
page_objects/upload_config_page.py
Normal file
2227
page_objects/upload_config_page.py
Normal file
File diff suppressed because it is too large
Load Diff
279
permissions.py
Normal file
279
permissions.py
Normal file
@@ -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已正确安装且设备已连接")
|
||||
BIN
test/__pycache__/protocol.cpython-312.pyc
Normal file
BIN
test/__pycache__/protocol.cpython-312.pyc
Normal file
Binary file not shown.
23
test/control.py
Normal file
23
test/control.py
Normal file
@@ -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")
|
||||
29
test/device.py
Normal file
29
test/device.py
Normal file
@@ -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)
|
||||
22
test/protocol.py
Normal file
22
test/protocol.py
Normal file
@@ -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')
|
||||
41
test/server.py
Normal file
41
test/server.py
Normal file
@@ -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()
|
||||
149
test/test_play_data.py
Normal file
149
test/test_play_data.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user