基于RF框架与Toomos设备的UDS诊断自动化测试实践(1)——设备初始化与全局变量管理
1. 从零开始:为什么我们需要自动化测试框架?
大家好,我是老张,在汽车电子测试这行摸爬滚打了十几年。这些年,我亲眼看着车载诊断从简单的OBD-II读个故障码,发展到今天基于UDS(统一诊断服务)的复杂功能刷写、安全访问、参数配置。测试工作也从工程师抱着电脑、插着诊断仪手动点点点,变成了今天动辄上千个测试用例的自动化回归。
说实话,手动测试的苦,咱们都吃过。一个完整的诊断测试序列,可能包含上百条请求与响应,每条都要人工去比对、记录。效率低不说,还容易出错,尤其是做回归测试的时候,重复劳动简直让人崩溃。所以,搭建一个自动化测试平台,就成了我们这些“老测试”心心念念的事儿。
但理想很丰满,现实往往有点骨感。公司预算有限,动辄几十上百万的商用诊断工具链咱用不起。这时候,像Toomos这样的国产USB转CAN设备就成了一个非常务实的选择。它价格亲民,接口简单,配套的SDK也还算完善,非常适合我们用来搭建一个轻量级、高定制化的自动化测试环境。
那么,用什么来驱动这个自动化测试呢?我选择了Robot Framework(RF)。你可能听说过它,一个基于Python的通用自动化测试框架。为什么选它?第一,它的关键字驱动模式对测试工程师特别友好,写出来的测试用例像自然语言一样好读好维护。第二,生态丰富,社区活跃,我们能很方便地扩展自己的测试库。第三,报告清晰,集成方便,非常适合持续集成(CI)流程。
所以,这个系列文章,我就想跟你分享一下,如何用RF框架结合Toomos设备,一步步搭建起一个可用的UDS诊断自动化测试环境。今天这第一篇,咱们就从最基础、也最关键的一步讲起:如何正确地让设备“活”起来,以及如何优雅地管理那些到处都要用的“全局变量”。别看这两步简单,里面可有不少我踩过的坑和总结出来的最佳实践。
2. 设备初始化:让Toomos设备“听话”的四步曲
拿到Toomos的USB TO CAN设备,第一件事不是急着发报文,而是要先和它建立好“通信协议”。这就好比你要指挥一个乐队,得先确保每个乐手都在场、乐器都调好音、并且愿意听你指挥。设备初始化,就是这个“热身”过程。
2.1 准备工作:获取“武器库”(SDK与资料)
工欲善其事,必先利其器。Toomos官方提供了比较完整的二次开发资料,这是我们所有工作的基础。
- 访问官网:首先,你得去重庆图莫斯电子科技有限公司的官网,找到“资料下载”或“技术支持”板块。
- 下载SDK:找到对应你设备型号的二次开发包(SDK)。通常里面会包含:
USB2XXX.h/USB2XXX.py等头文件或Python封装文件:这是核心的API接口定义。USB2XXX.dll/libUSB2XXX.so等动态链接库:这是实际的驱动功能库。- 二次开发范例程序:这个非常重要!尤其是
CAN相关的例子,是我们理解API用法的捷径。
- 阅读帮助文档:SDK里一般会附带一个
CHM或PDF格式的帮助文档,里面详细列出了所有函数的功能、参数和返回值。虽然读起来可能有点枯燥,但遇到问题时,它是你的第一本“说明书”。
我建议你把范例程序先跑通一遍,感受一下整个调用流程。你会发现,无论你想用设备做什么(CAN、LIN、FlexRay等),都逃不开一个固定的初始化流程。下面,我就把这个流程拆解给你看。
2.2 核心四步:扫描、打开、查信息、初始化CAN
根据Toomos SDK的规范,使用设备进行CAN通信,必须严格遵循以下四个步骤。我把它封装在一个DevManager类里,这样逻辑清晰,也便于复用。
import ctypes
from ctypes import byref, c_char, c_ubyte
# 假设你已经将SDK中的相关定义导入进来
# 例如:from usb2xxx import *
class DevManager:
def __init__(self, baud_rate=500000):
"""
设备管理器初始化
:param baud_rate: CAN总线波特率,默认500Kbps
"""
self.dev_handle = (c_ubyte * 20)() # 设备句柄容器,非常重要!
self.baud_rate = baud_rate
self._initialize_device()
def _initialize_device(self):
"""执行设备初始化的完整流程"""
print("开始初始化Toomos设备...")
self._usb_scan()
self._usb_open()
self._dev_info() # 这一步可选,但强烈建议保留,用于确认设备状态
self._can_init()
print("设备初始化完成!")
现在,我们来深入每一步的细节。
第一步:扫描设备 (_usb_scan)
这一步的目的是“看看有多少设备插在电脑上”。USB_ScanDevice这个函数会扫描所有连接的Toomos设备,并把找到的设备句柄(可以理解为一个设备的身份证号)填充到我们提供的数组里。
def _usb_scan(self):
"""扫描USB总线上的Toomos设备"""
# USB_ScanDevice 是SDK提供的函数,返回找到的设备数量
ret = USB_ScanDevice(byref(self.dev_handle))
if ret == 0:
print("[错误] 未检测到任何Toomos设备连接!")
print("请检查:1. USB线是否接好;2. 设备驱动是否安装;3. 设备指示灯是否正常。")
exit(1) # 直接退出,因为没有设备后续无法进行
else:
print(f"[信息] 发现 {ret} 台Toomos设备。")
# 通常我们默认使用扫描到的第一台设备,即 self.dev_handle[0]
这里有个坑我踩过:self.dev_handle必须提前初始化为一个足够大的数组(比如c_ubyte * 20),因为USB_ScanDevice函数需要向里面写入数据。如果你传一个普通的整数进去,程序会崩溃。
第二步:打开设备 (_usb_open)
扫描到设备后,我们获得了它的“身份证”(句柄),但还不能直接跟它“说话”。USB_OpenDevice函数就是用来和指定设备建立连接,打开通信通道。
def _usb_open(self):
"""打开扫描到的第一台设备"""
# 我们使用扫描到的第一个设备句柄 self.dev_handle[0]
ret = USB_OpenDevice(self.dev_handle[0])
if bool(ret):
print(f"[成功] 设备连接成功,句柄: {self.dev_handle[0]}")
else:
print("[错误] 设备连接失败!")
print("可能原因:设备被其他程序占用(如官方上位机软件),或设备异常。")
raise ConnectionError("无法打开Toomos设备")
第三步:获取设备信息 (_dev_info,可选但推荐)
这一步不是必须的,但我强烈建议你加上。它就像设备的“自检报告”,能告诉你固件版本、硬件版本、序列号等信息。在自动化测试中,这非常有用:你可以用它来校验测试环境是否正确,或者记录日志时附上设备信息,方便问题追溯。
def _dev_info(self):
"""读取设备的详细信息"""
dev_info = DEVICE_INFO() # SDK中定义的结构体
# 创建一个缓冲区来接收功能字符串
dev_fun_str = (c_char * 256)()
ret = DEV_GetDeviceInfo(self.dev_handle[0], byref(dev_info), dev_fun_str)
if bool(ret):
print("\n--- 设备信息 ---")
# 解码字节串时要注意编码,SDK通常是ASCII
fw_name = bytes(dev_info.FirmwareName).decode('ascii', errors='ignore').strip('\x00')
print(f"固件名称: {fw_name}")
# 版本号通常是一个32位整数,需要按位解析
fw_ver = dev_info.FirmwareVersion
print(f"固件版本: v{(fw_ver >> 24) & 0xFF}.{(fw_ver >> 16) & 0xFF}.{fw_ver & 0xFFFF}")
# 同样解析硬件版本
hw_ver = dev_info.HardwareVersion
print(f"硬件版本: v{(hw_ver >> 24) & 0xFF}.{(hw_ver >> 16) & 0xFF}.{hw_ver & 0xFFFF}")
# 序列号可能是一个数组,需要拼接
sn = ''.join([f"{num:08X}" for num in dev_info.SerialNumber])
print(f"序列号: {sn}")
fun_str = bytes(dev_fun_str.value).decode('ascii', errors='ignore').strip('\x00')
print(f"支持功能: {fun_str}")
print("----------------\n")
else:
print("[警告] 获取设备信息失败,但不影响基本通信。")
第四步:初始化CAN通道 (_can_init)
这是最后,也是最关键的一步。我们要告诉设备,使用哪个CAN通道(比如通道0),以及以多大的速度(波特率)进行通信。
def _can_init(self):
"""初始化指定的CAN通道"""
CANConfig = CAN_INIT_CONFIG() # CAN初始化配置结构体
# 1. 根据波特率数值,获取对应的CAN时序参数
# 这一步很重要!SDK需要将500000这样的数字转换成具体的位时序参数
ret = CAN_GetCANSpeedArg(self.dev_handle[0], byref(CANConfig), self.baud_rate)
if ret != CAN_SUCCESS:
print(f"[错误] 获取CAN波特率参数失败!错误码: {ret}")
raise ValueError(f"不支持的波特率: {self.baud_rate}")
# 2. 使用获取到的参数初始化CAN通道(这里以通道0为例)
ret = CAN_Init(self.dev_handle[0], 0, byref(CANConfig)) # 第二个参数0表示CAN通道0
if ret != CAN_SUCCESS:
print(f"[错误] CAN通道初始化失败!错误码: {ret}")
raise RuntimeError("CAN初始化失败,请检查总线连接或设备状态。")
else:
print(f"[成功] CAN通道0初始化完成,波特率: {self.baud_rate} bps")
完成了这四步,你的Toomos设备就已经准备就绪,可以开始收发CAN报文了。但是,我们获取到的那个至关重要的deviceHandle(设备句柄),在后续的测试中要频繁使用,怎么管理它才最方便呢?这就是下一个要解决的大问题。
3. 全局变量管理:告别“句柄满天飞”的混乱局面
在写测试脚本时,尤其是当项目规模变大,代码被拆分到多个文件(比如一个文件处理CAN通信,一个文件解析DBC,一个文件封装UDS服务)时,你会发现这个deviceHandle几乎无处不在。如果每个文件都自己去初始化设备、获取句柄,不仅会造成资源冲突(一个设备被多次打开),代码也充满了重复和耦合。
3.1 为什么需要全局变量管理?
想象一下这个场景:
can_driver.py需要句柄来发送CAN帧。dbc_parser.py需要句柄来发送特定的控制帧(比如用来触发NRC 22条件)。uds_client.py需要句柄来发送和接收UDS诊断报文。test_case.robot是RF的测试用例文件,它需要调用上面所有这些库。
如果每个模块都自己维护一个句柄,要么你得把句柄作为参数层层传递,非常繁琐;要么就会出现多个模块试图打开同一设备的错误。因此,一个集中式的、线程安全的全局变量管理机制就非常有必要了。
我们的目标很简单:在程序开始时(设备初始化后),把句柄存到一个“保险箱”里。之后在任何地方、任何文件中,都能安全、方便地从这个“保险箱”取用句柄。
3.2 实现一个简单优雅的全局变量管理器
Python的global关键字在跨模块使用时并不方便。我更喜欢用一个单独的模块来实现一个全局字典,并通过函数来访问它。这种方法清晰、安全,避免了循环导入问题。
我们来创建一个名为global_var.py的文件:
# global_var.py
"""
全局变量管理器
使用单例模式的思想,提供一个全局的字典来存储跨模块共享的变量。
"""
# 用一个字典来存储所有全局变量
_global_dict = {}
def init():
"""初始化全局字典(可选,通常在程序启动时调用)"""
global _global_dict
_global_dict.clear() # 清空,防止重复运行时的旧数据干扰
print("[全局变量管理器] 初始化完成。")
def set_value(key, value):
"""
设置一个全局变量
:param key: 变量名,字符串类型
:param value: 变量值,可以是任何类型
"""
global _global_dict
_global_dict[key] = value
# 打印日志,方便调试,生产环境可以关闭
print(f"[全局变量管理器] 设置变量: '{key}' = {value}")
def get_value(key, default=None):
"""
获取一个全局变量的值
:param key: 变量名
:param default: 如果变量不存在,返回的默认值
:return: 变量的值,或默认值
"""
global _global_dict
try:
return _global_dict[key]
except KeyError:
if default is not None:
print(f"[全局变量管理器] 警告: 变量 '{key}' 不存在,返回默认值 {default}。")
return default
else:
print(f"[全局变量管理器] 错误: 变量 '{key}' 不存在!")
raise KeyError(f"全局变量 '{key}' 未定义")
这个管理器只有三个函数,但非常实用:
init(): 用于启动时清理环境(非必须)。set_value(key, value): 存东西。get_value(key, default): 取东西。我特意加了一个default参数,这样在变量不存在时,可以优雅地返回一个默认值而不是直接崩溃,让程序更健壮。
3.3 在设备初始化中集成全局变量
现在,我们改造一下之前的DevManager类,在初始化完成后,自动将设备句柄存入全局管理器。
# 在DevManager类的 __init__ 或 _initialize_device 方法末尾添加
import global_var # 导入我们刚才写的管理器
class DevManager:
def __init__(self, baud_rate=500000):
# ... 之前的初始化代码 ...
self._initialize_device()
self._register_global_handle()
def _register_global_handle(self):
"""将设备句柄注册到全局变量中"""
# 键名可以自定义,但建议用有意义的名称,如 'can_device_handle'
global_var.set_value("can_device_handle", self.dev_handle[0])
print(f"设备句柄已存入全局变量: can_device_handle -> {self.dev_handle[0]}")
3.4 在RF自定义库中使用全局变量
这是最精彩的部分。我们如何让Robot Framework的测试库也能方便地使用这个句柄呢?我们需要创建RF的自定义库。
假设我们创建一个用于CAN通信的RF库CanDriverLib.py:
# CanDriverLib.py
from robot.api.deco import keyword
import global_var # 导入全局变量管理器
# 导入Toomos SDK的相关函数
from usb2xxx import CAN_Send, CAN_Receive, CAN_SUCCESS
class CanDriverLib:
ROBOT_LIBRARY_SCOPE = 'GLOBAL' # 库作用域为全局,单例模式
def __init__(self):
# 在初始化时,并不直接获取句柄,因为此时设备可能还未初始化
# 我们将在每个需要句柄的方法中动态获取
pass
@keyword('发送CAN帧')
def send_can_frame(self, can_id, data, ext_flag=0, remote_flag=0):
"""
发送一帧CAN数据
:param can_id: CAN标识符
:param data: 数据,列表形式,如 [0x10, 0x02]
:param ext_flag: 是否为扩展帧,0为标准帧,1为扩展帧
:param remote_flag: 是否为远程帧,0为数据帧,1为远程帧
"""
# 关键步骤:从全局变量管理器中获取设备句柄
device_handle = global_var.get_value("can_device_handle")
if device_handle is False:
raise RuntimeError("未找到设备句柄,请先初始化设备!")
# 准备CAN帧结构体 (这里需要根据SDK定义调整)
can_frame = CAN_MSG()
can_frame.ID = can_id
can_frame.Extrem = ext_flag
can_frame.Remote = remote_flag
can_frame.DataLen = len(data)
for i, byte in enumerate(data):
can_frame.Data[i] = byte
# 调用SDK发送函数
ret = CAN_Send(device_handle, 0, byref(can_frame), 1) # 通道0,发送1帧
if ret != CAN_SUCCESS:
raise RuntimeError(f"CAN帧发送失败,错误码: {ret}")
return True
在Robot Framework的测试用例中,你就可以这样写了:
*** Settings ***
Library CanDriverLib.py
Library DevManager WITH NAME Device # 给库起个别名
*** Test Cases ***
示例测试用例_设备初始化与发送
[Setup] Device.初始化设备 # 这一步会执行DevManager的初始化,并设置全局变量
${result}= 发送CAN帧 can_id=0x7E0 data=0x10 0x02
Should Be True ${result}
通过这种方式,DevManager库负责初始化设备并设置全局句柄,而CanDriverLib库在需要发送CAN帧时,自动从全局管理器中获取句柄。两者完全解耦,但又通过全局变量管理器紧密协作。后续无论你增加多少个库(如UDS库、DBC解析库),它们都只需要依赖global_var来获取句柄,代码结构会非常清晰。
4. 避坑指南与最佳实践
理论讲完了,最后分享几个我实战中总结出来的“血泪教训”和技巧,希望能帮你少走弯路。
1. 设备连接失败怎么办?
- 检查驱动:这是最常见的问题。确保安装了Toomos官方最新的USB驱动。在设备管理器中查看设备是否带黄色叹号。
- 关闭占用程序:Toomus官方的上位机软件、或者其他测试工具如果正在使用设备,你的程序就无法打开它。务必关闭所有可能占用设备的程序。
- 尝试重启:简单粗暴但有效,重启电脑或重插USB设备有时能解决奇怪的连接问题。
2. CAN初始化失败或收不到数据?
- 波特率匹配:确保你代码里设置的波特率(如500000)和总线上其他节点(如待测的ECU)的波特率完全一致。一个比特的差别都通不了信。
- 终端电阻:CAN总线两端需要各有一个120欧姆的终端电阻。如果你的测试环境只有一台设备和一台ECU,确保至少有一端接了终端电阻。
- 线序检查:CAN_H和CAN_L不要接反。USB TO CAN设备的CAN接口通常是DB9或端子,对照引脚定义仔细检查。
3. 全局变量管理器的进阶用法
- 存储更多信息:除了设备句柄,你还可以把波特率、当前激活的CAN通道、甚至测试配置(如诊断会话类型、安全等级)也存到全局管理器里。
- 线程安全考虑:如果你的测试用例是并发执行的(RF本身通常不是,但如果你用了多线程),简单的字典可能不是线程安全的。可以考虑使用
threading.Lock加锁,或者使用更高级的数据结构。不过对于大多数RF顺序执行的测试场景,我们目前的实现已经足够。 - 命名规范:给全局变量起名时,建议加个前缀,比如
dev_handle、cfg_baudrate,这样在get_value时不容易弄错,也便于管理。
4. 关于RF库的设计
- 库的作用域:在RF库中设置
ROBOT_LIBRARY_SCOPE = 'GLOBAL'可以让库实例在整个测试执行过程中只初始化一次(单例),这对于设备连接这种重量级操作非常合适,避免了反复初始化的开销。 - 清晰的错误提示:在
get_value失败或SDK函数调用失败时,抛出带有明确信息的异常(如RuntimeError(“未初始化设备,请先调用‘初始化设备’关键字”))。这样当测试失败时,RF报告能直接指出问题根源,而不是一个晦涩的Python报错。
好了,关于设备初始化和全局变量管理,我想分享的核心内容就是这些。这套组合拳打下来,你的自动化测试项目就有了一个坚实、清晰的基础。设备准备好了,数据也有了安身之所,下一步我们就可以大展拳脚,开始封装具体的CAN收发、UDS服务,并编写真正的自动化测试用例了。记住,好的开始是成功的一半,前期在架构上多花点心思,后面的开发会顺畅很多。如果在实践中遇到问题,不妨回头看看这四个步骤和全局变量的设计,很多时候问题就出在这些基础的环节上。
更多推荐


所有评论(0)