这个是自动录入modbus poll的程序。
import time
import unittest
import os
import sys
from appium import webdriver
# 导入文件工具类
from file_utils import file_utils
# 使用新的文件工具类替代旧的ConfigManager
class ConfigManager(object):
"""INI配置文件管理器(兼容旧接口)"""
def __init__(self, config_file='config.ini'):
"""初始化配置管理器"""
self.config_file = config_file
self.create_default_config_if_needed()
def create_default_config_if_needed(self):
"""如果需要则创建默认配置文件"""
if not os.path.exists(self.config_file):
template_data = {
'appium': {
'server_url': 'http://127.0.0.1:4723',
'implicit_wait': '10'
},
'modbus': {
'exe_path': r'D:\soft\software\Modbus\Modbus Poll\mbpoll.exe',
'timeout': '10'
},
'test': {
'delay_time': '2',
'screenshot_path': 'screenshots'
},
'element': {
'clear_button': '清除',
'one_button': '一',
'seven_button': '七',
'plus_button': '加',
'equals_button': '等于',
'calculator_results': 'CalculatorResults',
'从站地址': '127.0.0.1',
'从站端口': '1001',
'从站id': '1',
'数据类型': '01 Read Coils (0x)',
'数据地址': '600',
'数据数量': '100',
'显示一列': '20'
}
}
file_utils.create_ini_template(self.config_file, template_data)
print("已创建默认配置文件: %s" % self.config_file)
def get(self, section, option, fallback=None):
"""获取配置值"""
return file_utils.read_ini(self.config_file, section, option, fallback)
def getint(self, section, option, fallback=0):
"""获取整数配置值"""
return file_utils.read_ini_int(self.config_file, section, option, fallback)
def getboolean(self, section, option, fallback=False):
"""获取布尔配置值"""
return file_utils.read_ini_boolean(self.config_file, section, option, fallback)
# 创建全局配置管理器实例
config = ConfigManager()
class SimpleCalculatorTests(unittest.TestCase):
@classmethod
def setUpClass(self):
#set up appium
desired_caps = {}
# 从配置文件获取设置
app_path = config.get('modbus', 'exe_path', r"D:\soft\software\Modbus\Modbus Poll\mbpoll.exe")
server_url = config.get('appium', 'server_url', 'http://127.0.0.1:4723')
print("正在启动应用: %s" % app_path)
print("连接到Appium服务器: %s" % server_url)
desired_caps["app"] = app_path
self.driver = webdriver.Remote(
command_executor=server_url,
desired_capabilities=desired_caps)
@classmethod
def tearDownClass(self):
self.driver.quit()
def test_run_sequence(self):
"""按顺序执行test_AddConfigPar和test_ImportTxtConfig"""
print("\n? 开始顺序执行测试...")
try:
# 先执行test_AddConfigPar
print("\n=== 执行 test_AddConfigPar ===")
self.Debug_AddConfigPar()
print("✅ test_AddConfigPar 执行完成")
# 等待2秒确保配置生效
time.sleep(2)
# 再执行test_ImportTxtConfig
print("\n=== 执行 test_ImportTxtConfig ===")
self.Debug_ImportTxtConfig()
print("✅ test_ImportTxtConfig 执行完成")
print("\n? 所有测试顺序执行完成!")
except Exception as e:
print(f"❌ 顺序执行失败: {e}")
raise
def getresults(self):
displaytext = self.driver.find_element_by_accessibility_id("CalculatorResults").text
# displaytext = displaytext.strip("Display is " )
displaytext = displaytext.strip("显示为 ")
displaytext = displaytext.rstrip(' ')
displaytext = displaytext.lstrip(' ')
return displaytext
def switch_to_english_input(self):
"""切换输入法为英文半角模式 - 使用Selenium键盘事件"""
try:
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
actions = ActionChains(self.driver)
# 方法1: 使用Alt+Shift切换输入法(Windows标准快捷键)
print("? 尝试切换输入法...")
actions.key_down(Keys.ALT).send_keys(Keys.SHIFT).key_up(Keys.ALT).perform()
time.sleep(0.3)
# 方法2: 使用Ctrl+Space切换输入法(备用方案)
# actions.key_down(Keys.CONTROL).send_keys(Keys.SPACE).key_up(Keys.CONTROL).perform()
# time.sleep(0.2)
# 确保半角模式:模拟Shift+Space
actions.key_down(Keys.SHIFT).send_keys(Keys.SPACE).key_up(Keys.SHIFT).perform()
time.sleep(0.2)
print("✅ 已尝试切换为英文输入法")
except Exception as e:
print(f"❌ 输入法切换失败: {e}")
print("⚠️ 请手动确保输入法为英文半角模式")
print("? 提示:在Windows中按Alt+Shift可切换输入法")
def Debug_AddConfigPar(self):
#self.setUpClass()
# 从配置文件获取元素名称
delay_time = config.getint('test', 'delay_time', 1)
slave_addr = config.get('element', '从站地址', '127.0.0.1')
slave_port = config.get('element', '从站端口', '1001')
slave_id = config.get('element', '从站id', '1')
data_type = config.get('element', '数据类型', '01 Read Coils (0x)')
data_addr = config.get('element', '数据地址', '600')
data_count = config.get('element', '数据数量', '100')
display_count = config.get('element', '显示一列', '20')
print("执行从站-连接:")
print(f"延时时间为:{delay_time}")
txtStr =self.driver.find_element_by_name("Connection").text
print(txtStr)
self.driver.find_element_by_name("Connection").click()
self.driver.find_element_by_name("Connect...\tF3").click()
time.sleep(1)
# 使用AutomationId操作输入框
# 方法1: 直接使用accessibility_id (推荐)
try:
# 查找"Connection Setup"对话框
Connection_dialog = self.driver.find_element_by_name("Connection Setup")
# 确保输入法为英文半角模式
self.switch_to_english_input()
# 查找AutomationId为"1113"的组合框(从站地址)
slave_addr_combo = Connection_dialog.find_element_by_accessibility_id("1113")
# 清除现有内容
slave_addr_combo.clear()
slave_addr_combo.send_keys(slave_addr)
time.sleep(delay_time)
# 查找AutomationId为"1114"的输入框(从站端口)
slave_port_input = Connection_dialog.find_element_by_accessibility_id("1114")
slave_port_input.clear()
slave_port_input.send_keys(slave_port)
time.sleep(delay_time)
# 点击OK关闭对话框
ok_button = Connection_dialog.find_element_by_name("OK")
ok_button.click()
time.sleep(delay_time)
# 查找AutomationId为"1003"的输入框(从站ID)
# slave_id_input = self.driver.find_element_by_accessibility_id("1003")
# slave_id_input.clear()
# slave_id_input.send_keys(slave_id)
except Exception as e:
print(f"使用AutomationId失败: {e}")
# 等待操作完成
time.sleep(delay_time)
print("执行从站-读取设置")
txtStr =self.driver.find_element_by_name("Setup").text
print(txtStr)
txtStr =self.driver.find_element_by_name("Setup").click()
self.driver.find_element_by_name("Read/Write Definition... F8").click()
time.sleep(delay_time)
# 查找"Read/Write Definition"对话框
try:
# 方法1: 通过窗口标题查找对话框
read_write_dialog = self.driver.find_element_by_name("Read/Write Definition")
print("找到Read/Write Definition对话框")
except:
# 方法2: 通过窗口类型查找
read_write_dialog = self.driver.find_element_by_class_name("#32770")
print("通过类名找到对话框")
# 操作ComboBox控件
try:
# 方法1: 使用AutomationId直接定位ComboBox
combo_box = read_write_dialog.find_element_by_accessibility_id("1005")
# 方法1a: 使用Appium的ComboBox操作
combo_box.click() # 先点击展开
time.sleep(delay_time) # 增加等待时间确保下拉列表完全展开
# 查找并选择"01 Read Coils (0x)"选项 - 仅在Read/Write Definition对话框内
try:
print("? 在Read/Write Definition对话框内查找ComboBox下拉选项...")
# 等待下拉列表展开
time.sleep(0.5+delay_time)
# 优化后的简洁选择逻辑:展开下拉列表后使用键盘导航选择
try:
from selenium.webdriver.common.by import By
# 记录当前值
initial_value = combo_box.get_attribute("Value.Value")
print(f"当前ComboBox值: {initial_value}")
if initial_value and data_type in str(initial_value):
print("✅ 已选中目标选项")
target_found = True
else:
print("? 展开下拉列表并使用键盘导航选择...")
# 点击ComboBox展开下拉列表
combo_box.click()
time.sleep(0.8) # 等待下拉列表完全展开
# 方法1: 使用键盘导航选择(↑↓键遍历)
try:
# 先按Home键回到顶部
combo_box.send_keys("\ue011")
time.sleep(0.2)
# 记录起始值
start_value = combo_box.get_attribute("Value.Value")
current_value = start_value
attempts = 0
max_attempts = 20 # 最多遍历20个选项
print("? 开始遍历下拉选项...")
while attempts < max_attempts:
if current_value and data_type in str(current_value):
print(f"✅ 找到并选中目标选项: {current_value}")
target_found = True
break
# 按向下箭头键选择下一个选项
combo_box.send_keys("\ue015")
time.sleep(0.3) # 等待选项切换
# 获取新的当前值
new_value = combo_box.get_attribute("Value.Value")
# 如果回到起始值,说明已经遍历完整个列表
if new_value == start_value and attempts > 0:
print("⚠️ 已遍历完所有选项,未找到目标")
break
current_value = new_value
attempts += 1
if attempts % 5 == 0: # 每5次打印一次进度
print(f" 已检查 {attempts} 个选项,当前: {current_value}")
# 如果找到了目标选项,按回车确认
if target_found:
combo_box.send_keys("\ue007")
time.sleep(0.3)
# 最终验证
final_value = combo_box.get_attribute("Value.Value")
if final_value and data_type in str(final_value):
print(f"✅ 最终确认选择成功: {final_value}")
else:
print(f"⚠️ 选择确认后值不匹配: {final_value}")
except Exception as keyboard_error:
print(f"键盘导航失败: {keyboard_error}")
print("? 尝试直接输入选择...")
try:
# 清除并输入
combo_box.send_keys("\ue009\ue010") # Ctrl+A全选
time.sleep(0.1)
combo_box.send_keys("\ue008") # 退格清除
time.sleep(0.2)
combo_box.send_keys(data_type)
time.sleep(0.5)
combo_box.send_keys("\ue007") # 回车确认
time.sleep(0.5)
final_value = combo_box.get_attribute("Value.Value")
if final_value and data_type in str(final_value):
print(f"✅ 直接输入选择成功: {final_value}")
target_found = True
except Exception as input_error:
print(f"直接输入失败: {input_error}")
except Exception as e:
print(f"ComboBox选择操作失败: {e}")
except Exception as e:
print(f"❌ ComboBox选择失败: {e}")
# 方法5: 获取对话框内所有可见选项(最终备选)
try:
print("? 获取对话框内所有可见选项...")
# 获取对话框内所有可见的ListItem和ComboBox选项
all_options = read_write_dialog.find_elements_by_xpath(".//*[@ClassName='ListItem' or contains(@Name, 'Read')]")
for option in all_options:
try:
name = option.get_attribute("Name")
print(f" 对话框内选项: {name}")
if name and "01 Read Coils (0x)" in str(name):
option.click()
print("✅ 最终遍历选择成功")
break
except:
continue
except Exception as e5:
print(f"最终备选方法失败: {e5}")
except Exception as e:
print(f"ComboBox操作失败: {e}")
# 继续其他输入操作
# 确保输入法为英文半角模式
self.switch_to_english_input()
# 查找AutomationId为"1115"的组合框(从站ID)
slave_id_combo = read_write_dialog.find_element_by_accessibility_id("1115")
slave_id_combo.clear() # 先清除现有内容
slave_id_combo.send_keys(slave_id)
print(f"从站ID: {slave_id}")
time.sleep(delay_time)
# 查找AutomationId为"1004"的输入框(从站首地址)
slave_port_input = read_write_dialog.find_element_by_accessibility_id("1004")
slave_port_input.clear()
slave_port_input.send_keys(data_addr)
print(f"从站首地址: {data_addr}")
time.sleep(delay_time)
# 查找AutomationId为"1006"的输入框(从站地址数量)
slave_addr_input = read_write_dialog.find_element_by_accessibility_id("1006")
slave_addr_input.clear() # 先清除现有内容
slave_addr_input.send_keys(data_count)
print(f"从站地址数量: {data_count}")
time.sleep(delay_time)
# 查找AutomationId为"1137"的输入框(从站一行显示的数量)
# slave_port_input = read_write_dialog.find_element_by_name("20")
# print(f"从站一行显示的数量: {display_count}")
slave_port_input = read_write_dialog.find_element_by_name("Fit to Quantity")
slave_port_input.click()
time.sleep(delay_time)
# 点击OK关闭对话框
ok_button = read_write_dialog.find_element_by_name("OK")
ok_button.click()
# self.driver.find_element_by_name(one_btn).click()
# self.driver.find_element_by_name(plus_btn).click()
# self.driver.find_element_by_name(seven_btn).click()
# self.driver.find_element_by_name(equals_btn).click()
# result = self.getresults()
# print("测试结果: %s" % result)
# self.assertEqual(result, "8")
# 使用配置文件中的延时
time.sleep(delay_time)
def Debug_ImportTxtConfig(self):
"""自动化将数据写入Modbus Poll表格"""
print("? 开始执行表格数据写入...")
delay_time = config.getint('test', 'delay_time', 1)
print(f"延时时间为:{delay_time}")
try:
time.sleep(delay_time)
# 1. 找到表格控件 "Mbpoll1"
print("? 查找表格控件 Mbpoll1...")
table = self.driver.find_element_by_name("Mbpoll1")
print("✅ 找到表格控件")
time.sleep(delay_time)
# 2. 获取表格的位置和大小信息
table_location = table.location
table_size = table.size
print(f"? 表格位置: x={table_location['x']}, y={table_location['y']}")
print(f"? 表格大小: 宽={table_size['width']}, 高={table_size['height']}")
# 3. 从txt文件读取数据
try:
data_file_path = os.path.join(os.path.dirname(__file__), "modbus_data.txt")
if os.path.exists(data_file_path):
with open(data_file_path, 'r', encoding='utf-8') as f:
data_content = f.read().strip()
print(f"? 从文件读取数据: {len(data_content.split(chr(10)))} 行")
else:
# 使用默认数据
data_content = "气缸 1\n气缸 1\n吸嘴 2\n吸嘴 2\n气缸 3\n气缸 3\n吸嘴 4\n吸嘴 4\n气缸 5\n气缸 5"
print("⚠️ 未找到modbus_data.txt,使用默认数据")
except Exception as read_error:
print(f"⚠️ 读取文件失败: {read_error},使用默认数据")
data_content = "气缸 1\n气缸 1\n吸嘴 2\n吸嘴 2\n气缸 3"
# 4. 复制数据到剪贴板
print("? 复制数据到剪贴板...")
try:
import pyperclip
pyperclip.copy(data_content)
print("✅ 使用pyperclip复制到剪贴板")
except ImportError:
# 备用方案:使用系统剪贴板
print("⚠️ pyperclip未安装,使用备用剪贴板方法...")
import subprocess
# 使用Windows命令行复制到剪贴板
subprocess.run(['clip'], input=data_content.encode('utf-8'), check=True)
print("✅ 使用系统剪贴板复制成功")
# 5. 更智能的表格单元格定位
from selenium.webdriver.common.action_chains import ActionChains
actions = ActionChains(SimpleCalculatorTests.driver)
print(f"\n?️ 测试点击左上角单元格...")
actions.move_to_element_with_offset(table, 50, 100).click().perform()
time.sleep(delay_time)
# 6. 验证并执行粘贴操作
print("? 验证焦点并执行粘贴...")
from selenium.webdriver.common.keys import Keys
try:
# 尝试获取当前焦点元素
active_element = self.driver.switch_to.active_element
# 清除现有内容
active_element.send_keys(Keys.CONTROL + "a")
time.sleep(0.2)
active_element.send_keys(Keys.DELETE)
time.sleep(0.2)
# 粘贴所有数据
active_element.send_keys(Keys.CONTROL + "v")
time.sleep(delay_time)
print("✅ 数据整体粘贴完成")
except Exception as click_error:
print(f"❌ 第一次点击失败: {click_error}")
except Exception as e:
print(f"❌ 表格数据写入失败: {e}")
# 保存错误截图
self.driver.save_screenshot("table_write_error.png")
raise e
time.sleep(1)
print("执行从站-读取设置")
txtStr = self.driver.find_element_by_name("Setup").text
print(txtStr)
txtStr = self.driver.find_element_by_name("Setup").click()
self.driver.find_element_by_name("Read/Write Definition... F8").click()
time.sleep(1)
# 查找"Read/Write Definition"对话框
try:
# 方法1: 通过窗口标题查找对话框
read_write_dialog = self.driver.find_element_by_name("Read/Write Definition")
print("找到Read/Write Definition对话框")
except:
# 方法2: 通过窗口类型查找
read_write_dialog = self.driver.find_element_by_class_name("#32770")
print("通过类名找到对话框")
# 查找AutomationId为"1137"的输入框(从站一行显示的数量)
display_count = config.get('element', '显示一列', '20')
slave_port_input = read_write_dialog.find_element_by_name("20")
print(f"从站一行显示的数量: {display_count}")
# slave_port_input = read_write_dialog.find_element_by_name("Fit to Quantity")
slave_port_input.click()
time.sleep(delay_time)
# 点击OK关闭对话框
ok_button = read_write_dialog.find_element_by_name("OK")
ok_button.click()
if __name__ == '__main__':
suite = unittest.TestLoader().loadTestsFromTestCase(SimpleCalculatorTests)
unittest.TextTestRunner(verbosity=2).run(suite)下面是config.ini的内容:
[appium] server_url = http://127.0.0.1:4723 implicit_wait = 10 [modbus] exe_path = E:\Modbus Tools\Modbus Poll\mbpoll.exe timeout = 10 [test] delay_time = 1 screenshot_path = screenshots [element] clear_button = 清除 one_button = 一 seven_button = 七 plus_button = 加 equals_button = 等于 calculator_results = CalculatorResults 从站地址 = 127.0.0.1 从站端口 = 502 从站id = 1 数据类型 = 01 Read Coils (0x) 数据地址 = 600 数据数量 = 100 显示一列 = 20
modbus_data.txt是随便准备的要填充的数据:
气缸 1 吸嘴 2 气缸 2 吸嘴 3 气缸 3 吸嘴 4 气缸 4 吸嘴 5 气缸 5 吸嘴 6 气缸 6 吸嘴 7 气缸 7 吸嘴 8 气缸 8 吸嘴 9 气缸 9 吸嘴 10 气缸 10 吸嘴 11 气缸 11 吸嘴 12 气缸 12 吸嘴 13 气缸 13 吸嘴 14 气缸 14 吸嘴 15 气缸 15 吸嘴 16 气缸 16 吸嘴 17 气缸 17 吸嘴 18 气缸 18 吸嘴 19 气缸 19 吸嘴 20 气缸 20 吸嘴 21 气缸 21 吸嘴 22 气缸 22 吸嘴 23 气缸 23 吸嘴 24 气缸 24 吸嘴 25 气缸 25 吸嘴 26 气缸 26 吸嘴 27 气缸 27 吸嘴 28 气缸 28 吸嘴 29 气缸 29 吸嘴 30 气缸 30 吸嘴 31 气缸 31 吸嘴 32 气缸 32 吸嘴 33 气缸 33 吸嘴 34 气缸 34 吸嘴 35 气缸 35 吸嘴 36 气缸 36 吸嘴 37 气缸 37 吸嘴 38 气缸 38 吸嘴 39 气缸 39 吸嘴 40 气缸 40 吸嘴 41 气缸 41 吸嘴 42 气缸 42 吸嘴 43 气缸 43 吸嘴 44 气缸 44 吸嘴 45 气缸 45 吸嘴 46 气缸 46 吸嘴 47 气缸 47 吸嘴 48 气缸 48 吸嘴 49 气缸 49 吸嘴 50 吸嘴 51 气缸 51
本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

本帖最后由 勇哥,很想停止 于 2025-08-16 12:09:33 编辑 

少有人走的路

















