歌尔在SIP(System in Package,系统级封装)模块的测试中,需要验证每个模块的序列号(SN)和Wi-Fi MAC地址等唯一标识符的准确性和可读写性。这类测试通常涉及与测试治具(如烧录器、通信板)的交互,通过串口、USB或GPIO等接口发送特定指令并解析返回数据。以下是一个基于Python的脚本范例,模拟了这一测试流程。
### 1. 测试流程与脚本设计思路
该脚本模拟了一个典型的自动化测试站点的核心环节,其逻辑流程如下图所示:
```mermaid
flowchart TD
A[开始测试] --> B[初始化测试环境<br>连接设备与日志];
B --> C{设备连接成功?};
C -- 是 --> D[执行SN读取与验证];
C -- 否 --> E[记录连接失败<br>测试失败];
D --> F{SN格式与校验和正确?};
F -- 是 --> G[执行Wi-Fi MAC地址读取与验证];
F -- 否 --> H[记录SN错误<br>测试失败];
G --> I{MAC地址格式与唯一性正确?};
I -- 是 --> J[记录所有数据<br>测试通过];
I -- 否 --> K[记录MAC地址错误<br>测试失败];
J --> L[生成测试报告];
H --> L;
E --> L;
K --> L;
```
整个流程遵循了生产线测试的“连接-验证-记录”核心逻辑,确保每个环节的失败都能被准确捕获和记录。
### 2. 完整的Python脚本范例
以下脚本使用 `pyserial` 库通过串口与SIP测试治具通信,并集成了基本的日志记录和报告生成功能。
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
歌尔SIP模块SN与Wi-Fi MAC地址自动化测试脚本
假设通过UART串口与测试治具通信,治具支持特定指令集。
"""
import serial
import re
import time
import logging
from datetime import datetime
import csv
import sys
class SIPModuleTester:
def __init__(self, port='COM3', baudrate=115200, timeout=2):
"""
初始化测试器
:param port: 串口号,如 COM3 (Windows) 或 /dev/ttyUSB0 (Linux)
:param baudrate: 波特率
:param timeout: 读写超时时间
"""
self.port = port
self.baudrate = baudrate
self.timeout = timeout
self.ser = None
self.test_results = {
'sn': '',
'mac': '',
'sn_valid': False,
'mac_valid': False,
'connection_status': False,
'error_message': ''
}
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(f'sip_test_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log'),
logging.StreamHandler(sys.stdout)
]
)
self.logger = logging.getLogger(__name__)
def connect(self):
"""建立与测试治具的串口连接"""
try:
self.ser = serial.Serial(
port=self.port,
baudrate=self.baudrate,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=self.timeout
)
if self.ser.is_open:
self.logger.info(f"成功连接到串口 {self.port}")
self.test_results['connection_status'] = True
# 可选:发送一个初始化指令或空指令清空缓冲区
time.sleep(0.5)
self.ser.reset_input_buffer()
return True
except serial.SerialException as e:
self.logger.error(f"连接串口 {self.port} 失败: {e}")
self.test_results['error_message'] = f"连接失败: {e}"
return False
def send_command_and_read_response(self, command, expected_terminator='\n', max_retries=2):
"""
向治具发送指令并读取响应
:param command: 字符串指令(会自动添加换行符)
:param expected_terminator: 预期的响应结束符
:param max_retries: 最大重试次数
:return: 成功则返回响应的字符串,失败返回None
"""
if not self.ser or not self.ser.is_open:
self.logger.error("串口未连接")
return None
full_command = command.strip() + '\n' # 假设治具以换行符作为命令结束
for attempt in range(max_retries):
try:
self.logger.debug(f"发送指令: {command.strip()}")
self.ser.write(full_command.encode('ascii'))
self.ser.flush()
# 读取响应(示例:读取直到遇到换行符或超时)
response = b''
start_time = time.time()
while time.time() - start_time < self.timeout:
if self.ser.in_waiting > 0:
byte = self.ser.read(1)
response += byte
if byte.decode('ascii', errors='ignore') == expected_terminator:
break
time.sleep(0.01)
decoded_response = response.decode('ascii', errors='ignore').strip()
self.logger.debug(f"收到响应: {decoded_response}")
return decoded_response
except serial.SerialTimeoutException:
self.logger.warning(f"指令 '{command}' 响应超时,尝试 {attempt + 1}/{max_retries}")
except Exception as e:
self.logger.error(f"指令 '{command}' 通信出错: {e}")
break
time.sleep(0.1) # 重试前短暂等待
self.logger.error(f"指令 '{command}' 重试 {max_retries} 次后失败")
return None
def read_and_validate_sn(self):
"""
步骤1: 读取并验证SN(序列号)
假设指令为 `GET_SN`,响应格式为 `SN=GOER1234567890A`
验证规则:以“GOER”开头,后跟12位数字字母组合,最后一位为校验码(示例)
"""
self.logger.info("开始测试SN...")
response = self.send_command_and_read_response('GET_SN')
if not response:
self.test_results['error_message'] = '读取SN无响应'
return False
# 解析响应,提取SN
sn_match = re.search(r'SN=(\w+)', response)
if not sn_match:
self.logger.error(f"SN响应格式错误: {response}")
self.test_results['error_message'] = f'SN响应格式错误: {response}'
return False
raw_sn = sn_match.group(1)
self.test_results['sn'] = raw_sn
self.logger.info(f"读取到原始SN: {raw_sn}")
# 示例验证规则(需根据实际规范调整)
# 规则1: 长度检查
if len(raw_sn) != 16:
self.logger.error(f"SN长度错误: {len(raw_sn)} (期望16)")
self.test_results['error_message'] = f'SN长度错误: {len(raw_sn)}'
return False
# 规则2: 固定前缀
if not raw_sn.startswith('GOER'):
self.logger.error(f"SN前缀错误: {raw_sn}")
self.test_results['error_message'] = f'SN前缀错误: {raw_sn}'
return False
# 规则3: 校验和验证(示例:最后一位是前15位的简单和校验)
try:
checksum_char = raw_sn[-1]
data_part = raw_sn[:-1]
# 示例校验算法:将前15位字符的ASCII码值求和,取模36后映射为字符(0-9,A-Z)
calculated_sum = sum(ord(c) for c in data_part) % 36
calculated_char = str(calculated_sum) if calculated_sum < 10 else chr(ord('A') + calculated_sum - 10)
if checksum_char != calculated_char:
self.logger.error(f"SN校验和失败: 计算值={calculated_char}, 实际值={checksum_char}")
self.test_results['error_message'] = f'SN校验和失败'
return False
except Exception as e:
self.logger.error(f"SN校验和计算异常: {e}")
self.test_results['error_message'] = f'SN校验和计算异常: {e}'
return False
self.logger.info("SN验证通过")
self.test_results['sn_valid'] = True
return True
def read_and_validate_mac(self):
"""
步骤2: 读取并验证Wi-Fi MAC地址
假设指令为 `GET_WIFI_MAC`,响应格式为 `MAC=AA:BB:CC:DD:EE:FF`
验证规则:符合标准MAC地址格式(6组十六进制数,分隔符为冒号)
"""
self.logger.info("开始测试Wi-Fi MAC地址...")
response = self.send_command_and_read_response('GET_WIFI_MAC')
if not response:
self.test_results['error_message'] = '读取MAC无响应'
return False
# 解析响应,提取MAC
mac_match = re.search(r'MAC=([0-9A-Fa-f:]+)', response)
if not mac_match:
self.logger.error(f"MAC响应格式错误: {response}")
self.test_results['error_message'] = f'MAC响应格式错误: {response}'
return False
raw_mac = mac_match.group(1).upper() # 统一转为大写
self.test_results['mac'] = raw_mac
self.logger.info(f"读取到原始MAC: {raw_mac}")
# 验证标准MAC地址格式 (xx:xx:xx:xx:xx:xx)
mac_pattern = re.compile(r'^([0-9A-F]{2}:){5}[0-9A-F]{2}$')
if not mac_pattern.match(raw_mac):
self.logger.error(f"MAC地址格式错误: {raw_mac}")
self.test_results['error_message'] = f'MAC地址格式错误: {raw_mac}'
return False
# 附加验证:检查是否为常见的无效地址(如全零、全F、广播地址等)
invalid_macs = ['00:00:00:00:00:00', 'FF:FF:FF:FF:FF:FF']
if raw_mac in invalid_macs:
self.logger.error(f"MAC地址为无效地址: {raw_mac}")
self.test_results['error_message'] = f'MAC地址为无效地址: {raw_mac}'
return False
# (可选)在此处可以添加与数据库中已测MAC地址的比对,防止重复
self.logger.info("MAC地址验证通过")
self.test_results['mac_valid'] = True
return True
def run_full_test(self):
"""执行完整的SN和MAC测试流程"""
self.logger.info("="*50)
self.logger.info("开始执行SIP模块SN与MAC测试")
if not self.connect():
self.logger.error("测试中止:设备连接失败")
return self.test_results
# 测试序列
test_passed = True
if not self.read_and_validate_sn():
test_passed = False
if not self.read_and_validate_mac():
test_passed = False
# 最终判定
overall_result = "PASS" if (test_passed and
self.test_results['sn_valid'] and
self.test_results['mac_valid']) else "FAIL"
self.test_results['overall_result'] = overall_result
self.logger.info(f"测试完成,最终结果: {overall_result}")
self.logger.info(f"SN: {self.test_results['sn']} (Valid: {self.test_results['sn_valid']})")
self.logger.info(f"MAC: {self.test_results['mac']} (Valid: {self.test_results['mac_valid']})")
# 保存结果到CSV报告
self.save_to_csv()
return self.test_results
def save_to_csv(self, filename='sip_test_report.csv'):
"""将测试结果追加保存到CSV文件"""
file_exists = False
try:
with open(filename, 'r') as f:
file_exists = True
except FileNotFoundError:
pass
with open(filename, 'a', newline='', encoding='utf-8') as csvfile:
fieldnames = ['timestamp', 'sn', 'mac', 'sn_valid', 'mac_valid', 'overall_result', 'error_message']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
if not file_exists:
writer.writeheader()
writer.writerow({
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'sn': self.test_results['sn'],
'mac': self.test_results['mac'],
'sn_valid': self.test_results['sn_valid'],
'mac_valid': self.test_results['mac_valid'],
'overall_result': self.test_results.get('overall_result', 'N/A'),
'error_message': self.test_results.get('error_message', '')
})
self.logger.info(f"测试结果已保存至 {filename}")
def disconnect(self):
"""断开连接"""
if self.ser and self.ser.is_open:
self.ser.close()
self.logger.info("串口连接已关闭")
if __name__ == "__main__":
# 使用示例
# 请根据实际环境修改串口号
tester = SIPModuleTester(port='COM3', baudrate=115200, timeout=3)
try:
results = tester.run_full_test()
print("\n=== 测试结果摘要 ===")
for key, value in results.items():
print(f"{key}: {value}")
except KeyboardInterrupt:
print("\n测试被用户中断")
finally:
tester.disconnect()
```
### 3. 脚本关键功能解析
| 模块/方法 | 核心功能 | 关键实现细节 |
| :--- | :--- | :--- |
| **`SIPModuleTester` 类** | 测试流程的封装与组织。 | 集中管理串口连接、测试状态、日志和结果。 |
| **`connect()`** | 建立与SIP测试治具的物理连接。 | 使用 `pyserial` 库,配置正确的波特率、数据位、停止位和奇偶校验。 |
| **`send_command_and_read_response()`** | 指令发送与响应接收的核心通信方法。 | 实现带重试机制的可靠通信,并处理超时和异常。 |
| **`read_and_validate_sn()`** | 执行SN的读取与验证。 | 1. 发送 `GET_SN` 指令。<br>2. 使用正则表达式 `r'SN=(\w+)'` 解析响应。<br>3. 实施三重验证:**长度检查**、**前缀匹配**(如“GOER”)、**校验和验证**。 |
| **`read_and_validate_mac()`** | 执行Wi-Fi MAC地址的读取与验证。 | 1. 发送 `GET_WIFI_MAC` 指令。<br>2. 使用正则表达式 `r'MAC=([0-9A-Fa-f:]+)'` 解析响应。<br>3. 使用严格的正则 `r'^([0-9A-F]{2}:){5}[0-9A-F]{2}$'` 验证格式。<br>4. 过滤常见无效地址(如全0、全F)。 |
| **`run_full_test()`** | 控制完整的测试流程。 | 按顺序执行连接、SN测试、MAC测试,并汇总最终结果。 |
| **`save_to_csv()`** | 持久化测试结果。 | 将每条测试记录(时间戳、SN、MAC、结果)追加到CSV文件,便于追溯和统计分析。 |
| **日志系统** | 记录测试全过程。 | 同时输出到控制台和按时间戳命名的日志文件,便于调试和审计。 |
### 4. 实际应用中的扩展与调整建议
1. **通信协议适配**:脚本中的 `GET_SN` 和 `GET_WIFI_MAC` 指令是示例。**实际必须替换为歌尔测试治具或SIP模块芯片(如博通、高通、乐鑫等方案)规定的确切AT指令、HCI命令或私有协议指令**。通信接口也可能是USB HID、I2C或SPI。
2. **验证规则强化**:示例中的SN校验算法是示意性的。**实际需要根据歌尔或客户提供的SN编码规范实现**,可能包含更复杂的校验位算法(如Luhn算法、CRC)、生产批号、日期码等信息解析。MAC地址的验证可能需要检查OUI(组织唯一标识符)是否属于指定厂商范围。
3. **集成到测试系统**:此类脚本通常作为 **“测试站”** 的一部分运行。需要将其集成到更高级的测试执行管理器(如基于 `pytest`/`unittest` 的测试框架)或生产MES(制造执行系统)中。主控程序会调用该脚本,并传入治具端口、产品型号等参数。
4. **错误恢复与重试**:生产线环境复杂,脚本应具备更强的鲁棒性。例如,在验证失败后,可以尝试重新上电模块、发送复位指令,再进行有限次数的重试。
5. **数据上传**:测试通过的SN和MAC地址通常需要**实时上传至MES或数据库**,与生产工单绑定,实现全流程追溯。脚本中的 `save_to_csv` 函数可扩展为调用REST API或写入共享数据库。
6. **治具控制**:完整的测试流程可能还包括控制继电板上电、下电,或通过GPIO触发测试开始信号。这可能需要集成额外的库(如 `python-periphery` 控制GPIO)或与PLC通信。
**总结**:本脚本提供了一个歌尔SIP测试中验证SN和Wi-Fi MAC地址的**完整、可运行的Python范例框架**。它涵盖了**串口通信、指令解析、数据验证、日志记录和结果保存**等核心环节。在实际部署前,开发工程师必须根据**具体的治具通信协议、SIP模块的数据手册以及公司的编码规范**,修改指令集、响应解析逻辑和验证算法。该脚本是构建自动化测试站点的坚实基础,通过扩展可以满足更复杂的生产测试需求。