first commit

This commit is contained in:
2026-03-12 17:03:56 +08:00
commit aa4d4c7d7c
48 changed files with 10958 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

302
actions.py Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

211
check_station.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
pyserial>=3.5

298
ck/serial_protocol.py Normal file
View 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)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

520
globals/apis.py Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

298
globals/ex_apis.py Normal file
View 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)

View 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
View 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"

Binary file not shown.

46
page_objects/call_xie.py Normal file
View 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")

View 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
View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

279
permissions.py Normal file
View 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已正确安装且设备已连接")

Binary file not shown.

23
test/control.py Normal file
View 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
View 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
View 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
View 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
View 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()