Knowledge sharing
固件更新
1. 概述
基于 gRPC 实现的固件更新客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的固件更新流程,包括连接服务器、上传固件、等待设备重启并验证更新结果。
2. 核心代码结构
2.1 固件更新客户端类 FirmwareUpdateClient
FirmwareUpdateClient 是整个固件更新流程的核心实现,位于 service_client/fw_update.py 文件中。该类封装了与服务器交互的所有逻辑,包括连接管理、固件上传、版本检查等功能。
2.2 测试用例 test_fw_update
test_fw_update 函数位于 test_case/test_fw_update.py 文件中,是一个简单的测试函数,用于验证固件更新客户端的功能。
3. 关键技术点分析
3.1 gRPC 连接管理
def connect(self) -> bool:
"""Connect to server and initialize two stubs"""
self.logger.info(f"Connecting to {self.server_addr}")
# Retry connection
max_retries = 3
for retry in range(max_retries):
try:
self.channel = grpc.insecure_channel(
self.server_addr, options=self.GRPC_OPTIONS
)
# Initialize two stubs
self.firmware_update_stub = (
pb2_grpc.TestInterfaceFirmwareUpdateServiceStub(self.channel)
)
self.configuration_stub = (
pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
)
# Test channel
grpc.channel_ready_future(self.channel).result(timeout=10)
self.logger.info(f"Firmware client connected to: {self.server_addr}")
self.connected = True
# Test firmware service connection
if self.test_connection():
return True
else:
self.logger.warning(
f"Connection test failed, retry {retry+1}/{max_retries}"
)
except Exception as e:
self.logger.warning(
f"Connection attempt {retry+1}/{max_retries} failed: {e}"
)
time.sleep(2)
self.logger.error(f"Connection failed after {max_retries} attempts")
return False
技术要点:
- 使用
grpc.insecure_channel创建不安全的 gRPC 通道 - 配置了多项 gRPC 选项,如最大消息长度、心跳设置等
- 实现了连接重试机制,最多尝试 3 次
- 初始化两个不同的 stub:
TestInterfaceFirmwareUpdateServiceStub和TestInterfaceConfigurationServiceStub - 使用
grpc.channel_ready_future测试通道是否就绪
3.2 流式 RPC 实现固件上传
def upload_with_manual_info(
self, filepath: str, product_number: str, rstate: str, dut_position: int = 0
) -> bool:
"""Upload firmware"""
self.logger.info(f"Testing upload: {filepath}")
# Check file
if not os.path.exists(filepath):
self.logger.info(f"Cannot open file: {filepath}")
return False
file = self.get_file_info(filepath)
self.logger.info(f"File size: {file.size_byte} bytes")
try:
shared_session = self.create_session()
# Create request generator
def generate_upload_requests():
# 1. Send software item information
request = pb2.FirmwareUpdateRequest()
request.session.CopyFrom(shared_session)
request.dut_position = dut_position
# Create SoftwareItem
item = pb2.SoftwareItem()
item.description = f"Firmware update"
item.product_number = product_number
item.rstate = rstate
# item.sw_type = common_enums_pb2.SOFTWARE_TYPE_INITIAL_FLASH_IMAGE
item.sw_type = common_enums_pb2.SOFTWARE_TYPE_UNSPECIFIED
item.filename = file.name
item.hash = file.hash_md5
item.total_size = file.size_byte
request.item.CopyFrom(item)
yield request
self.logger.info(f"Software item sent")
# 2. Send file data
chunk_count = 0
total_sent = 0
with open(filepath, "rb") as f:
while True:
chunk = f.read(self.FIRMWARE_CHUNK_SIZE)
if not chunk:
break
bytes_read = len(chunk)
request = pb2.FirmwareUpdateRequest()
request.session.CopyFrom(shared_session)
request.dut_position = dut_position
# Create SoftwareItemContent
content = pb2.SoftwareItemContent()
content.swType = item.sw_type
content.data = chunk
request.content.CopyFrom(content)
total_sent += bytes_read
chunk_count += 1
# Display progress
if (
chunk_count % self.PROGRESS_UPDATE_INTERVAL == 0
or total_sent == file.size_byte
):
if file.size_byte > 0:
progress = (total_sent * 100) // file.size_byte
else:
progress = 0
self.logger.info(
f"Progress: {progress}% ({total_sent}/{file.size_byte} bytes)"
)
yield request
self.logger.info(f"All data sent ({chunk_count} chunks)")
# Call streaming RPC with longer timeout
response = self.firmware_update_stub.FirmwareUpdate(
generate_upload_requests(), timeout=120 # 2 minutes timeout
)
# Check response
if hasattr(response, "code"):
self.logger.info(
f"Server response: {response.message} (code: {response.code})"
)
return response.code == 0
else:
self.logger.info(f"Unexpected server response: {response}")
return True # Consider success even if response format is different
except grpc.RpcError as e:
self.logger.error(f"Upload failed: {e.details()}")
return False
except Exception as e:
self.logger.exception(f"Upload error: {e}")
return False
技术要点:
- 使用生成器函数
generate_upload_requests()实现流式 RPC - 分两部分发送数据:
- 首先发送软件项信息(文件名、大小、哈希等)
- 然后分块发送固件文件数据
- 每发送 10 个块或发送完成时,显示上传进度
- 使用
yield语句逐个发送请求 - 设置了 2 分钟的超时时间,以应对大文件上传
3.3 设备重启与状态检查
def wait_for_device_and_get_sw_info(
self, timeout_seconds: Optional[int] = None
) -> bool:
"""Wait for device restart and get software info with timeout"""
if timeout_seconds is None:
timeout_seconds = self.DEVICE_READY_TIMEOUT_SECONDS
self.logger.info(
f"Waiting for device to come back online (timeout: {timeout_seconds} seconds)..."
)
start_time = time.time()
attempt = 0
while time.time() - start_time < timeout_seconds:
attempt += 1
response = None
try:
with grpc.insecure_channel(
self.server_addr, options=self.GRPC_OPTIONS
) as self.channel:
grpc.channel_ready_future(self.channel).result(
timeout=self.POLLING_INTERVAL_SECONDS - 1
)
self.configuration_stub = (
pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
)
request = empty_pb2.Empty()
response = self.configuration_stub.GetSWInfos(request, timeout=3)
self.logger.info(f"Device is back online! New firmware is running.")
self.updated_sw_infos = self.parse_sw_info_response(response)
return True
except (grpc.RpcError, grpc.FutureTimeoutError) as e:
if isinstance(e, grpc.RpcError) and e.code() not in (
grpc.StatusCode.UNAVAILABLE,
grpc.StatusCode.DEADLINE_EXCEEDED,
):
self.logger.error(f"RPC error during polling: {e.details()}")
return False
except KeyboardInterrupt:
self.logger.error("\nPolling interrupted by user")
return False
elapsed = time.time() - start_time
if attempt % self.PROGRESS_LOG_INTERVAL == 0:
remaining = timeout_seconds - elapsed
self.logger.info(
f"Still waiting... ({elapsed:.0f}s elapsed, {remaining:.0f}s remaining)"
)
time.sleep(self.POLLING_INTERVAL_SECONDS)
self.logger.info(f"Device did not respond within {timeout_seconds} seconds")
return False
技术要点:
- 实现了设备重启后的轮询机制,等待设备重新上线
- 使用
with语句创建临时 gRPC 通道,确保资源正确释放 - 处理了多种 gRPC 错误情况,特别是
UNAVAILABLE和DEADLINE_EXCEEDED - 每 1 分钟(12 次轮询)显示一次等待状态
- 支持自定义超时时间,默认为 10 分钟
3.4 完整固件更新流程
def firmware_upgrade(
self,
filepath: str,
product_number: str = "",
rstate: str = "",
timeout_seconds: int = 600,
) -> bool:
self.start_time = time.time()
self.logger.info("Starting firmware upgrade process...")
# 1. Check original firmware version
self.logger.info(
"\n" + "=" * 20 + "Step 1: Checking original firmware info..." + "=" * 20
)
self.original_sw_infos = self.check_fw_version()
# 2. Execute firmware upload
self.logger.info("\n" + "=" * 20 + "Step 2: Uploading firmware..." + "=" * 20)
if not self.upload_with_manual_info(
filepath, product_number, rstate, self.position
):
self.logger.error("Firmware upload failed")
return False
upload_time = time.time() - self.start_time
self.logger.info(f"Firmware upload completed in {upload_time:.2f} seconds")
# 3. Wait for device restart and come back online
self.logger.info(
"\n"
+ "=" * 20
+ "Step 3: Waiting for device to restart and come back online..."
+ "=" * 20
)
time.sleep(20)
if not self.wait_for_device_and_get_sw_info(timeout_seconds):
self.logger.error(
"Device did not come back online or failed to get software info"
)
return False
total_time = time.time() - self.start_time
self.logger.info(
"\n" + "=" * 20 + "Firmware upgrade completed successfully!" + "=" * 20
)
self.logger.info(f"Total time: {total_time:.2f} seconds")
if self.original_sw_infos[0].rstate != self.updated_sw_infos[0].rstate:
self.logger.info(
f"R-state changed from {self.original_sw_infos[0].rstate} to {self.updated_sw_infos[0].rstate}"
)
else:
self.logger.info("R-state remains unchanged")
return True
技术要点:
- 实现了完整的固件更新流程,包括三个主要步骤:
- 检查原始固件版本
- 上传新固件
- 等待设备重启并验证更新结果
- 记录了整个过程的时间消耗
- 比较了更新前后的固件版本(R-state)
- 提供了详细的日志输出,便于调试和监控
3.5 测试用例实现
def test_fw_update(server_addr: str, fw_path: str, logger: logging.Logger) -> bool:
client = None
try:
if not os.path.exists(fw_path):
logger.error(f"Error: File not found: {fw_path}")
return False
client = FirmwareUpdateClient(server_addr, logger=logger)
if not client.connect():
logger.error(f"Client connect failed.")
return False
success = client.firmware_upgrade(fw_path)
return success
except KeyboardInterrupt:
logger.error(f"Operation interrupted by user.")
return False
except Exception as e:
logger.error(f"Unexpected error occurred: {e}")
traceback.print_exc()
return False
finally:
if client:
client.close()
技术要点:
- 实现了一个简单的测试函数,用于验证固件更新功能
- 包含了异常处理,确保即使出现错误也能正确关闭客户端
- 遵循了良好的代码结构,使用 try-except-finally 确保资源正确释放
4. 代码执行流程分析
4.1 整体流程
-
初始化阶段:
- 创建
FirmwareUpdateClient实例 - 调用
connect()方法连接到服务器 - 测试连接是否成功
- 创建
-
固件更新阶段:
- 调用
firmware_upgrade()方法开始更新流程 - 检查原始固件版本
- 上传新固件(使用流式 RPC)
- 等待设备重启并重新连接
- 检查更新后的固件版本
- 调用
-
结束阶段:
- 关闭 gRPC 连接
- 返回更新结果
4.2 详细流程
-
连接服务器:
- 创建 gRPC 通道
- 初始化两个 stub
- 测试通道是否就绪
- 测试固件服务连接
-
上传固件:
- 计算固件文件的 MD5 哈希值
- 创建会话
- 发送软件项信息
- 分块发送固件数据
- 显示上传进度
- 处理服务器响应
-
等待设备重启:
- 等待 20 秒(设备重启时间)
- 轮询设备状态
- 当设备重新上线时,获取新的固件版本信息
- 比较更新前后的固件版本
5. 技术亮点
-
流式 RPC 实现:使用 gRPC 的流式 RPC 功能,实现了大文件的分块上传,避免了单次请求过大的问题。
-
错误处理:实现了完善的错误处理机制,包括连接重试、异常捕获等,提高了代码的健壮性。
-
状态管理:实现了设备重启后的状态检测和轮询机制,确保设备能够正确完成固件更新并重新上线。
-
性能优化:
- 配置了合理的 gRPC 选项,如最大消息长度、心跳设置等
- 使用分块上传,减少了内存占用
- 实现了进度显示,提高了用户体验
-
代码结构:代码结构清晰,职责分明,易于理解和维护。
6. 代码优化建议
-
日志改进:
- 可以使用结构化日志,便于后续的日志分析
- 增加更详细的错误信息,便于问题定位
-
错误处理增强:
- 可以增加更多的错误类型判断,提高错误处理的精确性
- 实现更细粒度的重试机制,针对不同的错误类型采取不同的重试策略
-
性能优化:
- 可以根据网络状况动态调整分块大小
- 实现并行上传,提高上传速度
-
可配置性:
- 将更多的参数(如超时时间、重试次数等)配置化,便于根据不同的环境进行调整
-
测试覆盖:
- 增加单元测试,覆盖更多的场景
- 实现集成测试,验证整个固件更新流程
7. 总结
本文详细分析了基于 gRPC 实现的固件更新客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的固件更新流程,包括连接服务器、上传固件、等待设备重启并验证更新结果。
通过使用 gRPC 的流式 RPC 功能,该客户端能够高效地处理大文件的上传,同时通过完善的错误处理和状态管理机制,确保了固件更新过程的可靠性。
该实现展示了如何使用 gRPC 构建一个可靠的固件更新系统,对于类似的场景具有参考价值。同时,通过本文提出的优化建议,可以进一步提高系统的性能和可靠性。
8. 输入输出示例
输入输出示例
输入:
import logging
from test_case.test_fw_update import test_fw_update
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 测试固件更新
server_addr = "192.168.1.100:50051"
fw_path = "/path/to/firmware.bin"
result = test_fw_update(server_addr, fw_path, logger)
print(f"Firmware update result: {result}")
输出:
2026-03-26 10:00:00,000 - INFO - Connecting to 192.168.1.100:50051
2026-03-26 10:00:00,100 - INFO - Firmware client connected to: 192.168.1.100:50051
2026-03-26 10:00:00,150 - INFO - Testing firmware connection...
2026-03-26 10:00:00,200 - INFO - Firmware connection test succeeded
2026-03-26 10:00:00,200 - INFO - Starting firmware upgrade process...
2026-03-26 10:00:00,200 - INFO - ====================Step 1: Checking original firmware info...====================
2026-03-26 10:00:00,250 - INFO - Current software information:
2026-03-26 10:00:00,250 - INFO - - Name: Device Firmware
2026-03-26 10:00:00,250 - INFO - Product Number: FW-1234
2026-03-26 10:00:00,250 - INFO - R-State: v1.0.0
2026-03-26 10:00:00,250 - INFO - ====================Step 2: Uploading firmware...====================
2026-03-26 10:00:00,250 - INFO - Testing upload: /path/to/firmware.bin
2026-03-26 10:00:00,260 - INFO -
File info:
2026-03-26 10:00:00,260 - INFO - Filename: firmware.bin
2026-03-26 10:00:00,260 - INFO - File size: 10,485,760 bytes (10.00 MB)
2026-03-26 10:00:00,260 - INFO - Modified time: 2026-03-25 15:30:00
2026-03-26 10:00:00,260 - INFO - MD5 (lowercase): d41d8cd98f00b204e9800998ecf8427e
2026-03-26 10:00:00,260 - INFO - File size: 10485760 bytes
2026-03-26 10:00:00,270 - INFO - Software item sent
2026-03-26 10:00:01,270 - INFO - Progress: 10% (1048576/10485760 bytes)
2026-03-26 10:00:02,270 - INFO - Progress: 20% (2097152/10485760 bytes)
2026-03-26 10:00:03,270 - INFO - Progress: 30% (3145728/10485760 bytes)
2026-03-26 10:00:04,270 - INFO - Progress: 40% (4194304/10485760 bytes)
2026-03-26 10:00:05,270 - INFO - Progress: 50% (5242880/10485760 bytes)
2026-03-26 10:00:06,270 - INFO - Progress: 60% (6291456/10485760 bytes)
2026-03-26 10:00:07,270 - INFO - Progress: 70% (7340032/10485760 bytes)
2026-03-26 10:00:08,270 - INFO - Progress: 80% (8388608/10485760 bytes)
2026-03-26 10:00:09,270 - INFO - Progress: 90% (9437184/10485760 bytes)
2026-03-26 10:00:10,270 - INFO - Progress: 100% (10485760/10485760 bytes)
2026-03-26 10:00:10,270 - INFO - All data sent (10 chunks)
2026-03-26 10:00:10,300 - INFO - Server response: Firmware uploaded successfully (code: 0)
2026-03-26 10:00:10,300 - INFO - Firmware upload completed in 10.10 seconds
2026-03-26 10:00:10,300 - INFO - ====================Step 3: Waiting for device to restart and come back online...====================
2026-03-26 10:00:30,300 - INFO - Waiting for device to come back online (timeout: 600 seconds)...
2026-03-26 10:00:35,350 - INFO - Device is back online! New firmware is running.
2026-03-26 10:00:35,360 - INFO - Current software information:
2026-03-26 10:00:35,360 - INFO - - Name: Device Firmware
2026-03-26 10:00:35,360 - INFO - Product Number: FW-1234
2026-03-26 10:00:35,360 - INFO - R-State: v1.1.0
2026-03-26 10:00:35,360 - INFO - ====================Firmware upgrade completed successfully!====================
2026-03-26 10:00:35,360 - INFO - Total time: 35.16 seconds
2026-03-26 10:00:35,360 - INFO - R-state changed from v1.0.0 to v1.1.0
2026-03-26 10:00:35,360 - INFO - Connection closed, total runtime: 35.16 seconds
Firmware update result: True
配置管理
1. 概述
基于 gRPC 实现的设备配置管理客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的配置管理功能,包括加载和设置配置文件、硬件信息的增删改查、软件信息查询等。
2. 核心代码结构
2.1 配置客户端类 ConfigurationClient
ConfigurationClient 是整个配置管理的核心实现,位于 service_client/configuration.py 文件中。该类封装了与服务器交互的所有逻辑,包括连接管理、配置文件加载与设置、硬件信息管理等功能。
2.2 测试用例 test_configuration_service
test_configuration_service 函数位于 test_case/test_configuration.py 文件中,是一个全面的测试函数,用于验证配置客户端的各项功能。
3. 关键技术点分析
3.1 gRPC 连接管理
def connect(self) -> bool:
try:
self.mmi_stub = pb2_grpc.TestInterfaceMMIStub(self.channel)
self.config_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
self.logger.info("Configuration client stubs created successfully.")
return True
except grpc.RpcError as e:
self.logger.error(f"Failed to connect: {e}")
return False
技术要点:
- 接收一个已建立的 gRPC 通道作为参数
- 初始化两个不同的 stub:
TestInterfaceMMIStub和TestInterfaceConfigurationServiceStub - 实现了简单的错误处理,捕获 gRPC 连接错误
3.2 配置文件加载与解析
def load_and_set_profile(self, file_path: str) -> None:
"""Load profiles from a JSON file and send SetProfile request."""
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return
profile_group, profile_base_name = self._parse_profile_file(file_path)
if profile_group is None:
return
request = pb2.SetProfileRequest(dut_position=self.dut_position)
request.session.CopyFrom(_create_session())
for hw_target, details in profile_group.items():
if not isinstance(details, dict):
continue
self.logger.info(f"Building profile for HW target '{hw_target}'")
profile = request.profiles.add()
profile.name = profile_base_name
profile.hw_target = hw_target
self._build_profile(profile, details)
if not request.profiles:
self.logger.error("No profiles built from file.")
return
self._send_set_profile(request)
技术要点:
- 从 JSON 文件加载配置文件
- 解析配置文件,提取配置组和配置名称
- 为每个硬件目标构建配置文件
- 发送
SetProfile请求到服务器
3.3 枚举值解析与映射
def _resolve_enum_value(json_value: str, enum_type_name: str, enum_descriptor) -> tuple:
"""Resolve a JSON string to a protobuf enum integer value.
Returns (int_value, matched_key_name) or (0, unspecified_name) on failure.
"""
enum_keys = enum_descriptor.keys()
if enum_type_name.startswith("Uart") and len(enum_type_name) > 4:
snake_prefix = "UART_" + enum_type_name[4:].upper()
else:
snake_prefix = _pascal_to_snake(enum_type_name)
if not json_value:
return 0, f"{snake_prefix}_UNSPECIFIED"
special = _ENUM_SPECIAL_MAPS.get(enum_type_name, {})
if json_value in special:
suffix = special[json_value]
elif json_value.isupper() or json_value.islower():
suffix = json_value.upper()
else:
suffix = _pascal_to_snake(json_value)
full_name = f"{snake_prefix}_{suffix}"
if full_name in enum_keys:
return enum_descriptor.Value(full_name), full_name
if suffix in enum_keys:
return enum_descriptor.Value(suffix), suffix
for key in enum_keys:
if key.endswith(f"_{suffix}"):
return enum_descriptor.Value(key), key
return 0, f"{snake_prefix}_UNSPECIFIED"
技术要点:
- 将 JSON 字符串值解析为 protobuf 枚举整数值
- 支持特殊映射,如波特率值到枚举值的映射
- 支持不同的命名约定,如 PascalCase、camelCase 到 UPPER_SNAKE_CASE 的转换
- 提供了多种匹配策略,提高了枚举值解析的成功率
3.4 硬件信息管理
def set_hw_infos(self, hw_info_list: List[pb2.HWInfo]) -> None:
if not self.config_stub:
self.logger.error("Not connected. Call connect() first.")
return
for hw_info in hw_info_list:
req = pb2.SetHWInfoRequest(
session=_create_session(), dut_position=self.dut_position, hw_info=hw_info
)
self.logger.info(f"Setting HW Info for: {hw_info.hw_name}")
resp = self.config_stub.SetHWInfo(req, timeout=10)
if resp.code != 0:
self.logger.error(f"Failed to set HW Info for {hw_info.hw_name}: {resp.message}")
技术要点:
- 批量设置硬件信息
- 为每个硬件信息创建一个单独的请求
- 设置了 10 秒的超时时间
- 处理并记录错误响应
3.5 完整测试流程
def test_configuration_service(channel: grpc.Channel, profile_file:str, logger: logging.Logger,
test_hw_infos: list = None, dut_position: int = 1) -> None:
"""Full test of ConfigurationService: set profile, HW info CRUD, SW info query."""
profile_full_path = os.path.abspath(profile_file)
if not os.path.exists(profile_full_path):
logger.error(f"Profile file not found: '{profile_full_path}'")
return
client = ConfigurationClient(channel, logger, dut_position=dut_position)
if not client.connect():
logger.error("Failed to connect.")
return
# Defaults for test HW infos
if test_hw_infos is None:
test_hw_infos = [{
"serial_number": "E23G123456",
"product_number": "ROA1281122/1",
"product_rstate": "R1A",
"hw_name": "NIB",
"comment": "Use default values!",
"production_date": "2025-02-14",
}]
hw_names = [hw.get("hw_name", "") for hw in test_hw_infos]
try:
# 1. Set Profile
logger.info("\nTEST 1: SetProfile")
client.load_and_set_profile(profile_full_path)
# 2. Get HW Infos
logger.info("\nTEST 2: GetHWInfos")
hw_resp = client.get_hw_infos()
if hw_resp:
logger.info(f"HW Infos count: {len(hw_resp.hw_info)}")
for info in hw_resp.hw_info:
logger.info(f" HW: {info.hw_name}, SN: {info.serial_number}, "
f"PN: {info.product_number}, R-State: {info.product_rstate}")
else:
logger.warning("No HW info response received.")
# 3. Set HW Info
logger.info("\nTEST 3: SetHWInfo")
hw_pb_list = [_build_hw_info(hw) for hw in test_hw_infos]
client.set_hw_infos(hw_pb_list)
# 4. Verify Set HW Info
logger.info("\nTEST 4: Verify SetHWInfo (GetHWInfos again)")
hw_resp2 = client.get_hw_infos()
if hw_resp2:
for name in hw_names:
found = any(info.hw_name == name for info in hw_resp2.hw_info)
logger.info(f" hw_name '{name}' found: {found}")
else:
logger.warning("No HW info response received.")
# 5. Erase HW Info
logger.info("\nTEST 5: EraseHwInfo")
for name in hw_names:
erase_resp = client.erase_hw_info(name)
if erase_resp:
logger.info(f" Erase '{name}': code={erase_resp.code}, message='{erase_resp.message}'")
else:
logger.warning(f" No erase response for '{name}'.")
# 6. Get SW Infos
logger.info("\nTEST 6: GetSWInfos")
sw_resp = client.get_sw_infos()
if sw_resp:
logger.info(f"SW Infos count: {len(sw_resp.sw_info)}")
for info in sw_resp.sw_info:
logger.info(f" SW: {info.sw_name}, PN: {info.product_number}, R-State: {info.product_rstate}")
else:
logger.warning("No SW info response received.")
logger.info("\nAll configuration tests completed.")
except grpc.RpcError as e:
logger.error(f"RPC error during test: {e.details()} (code: {e.code().name})")
except Exception as e:
logger.exception(f"Unexpected error during test: {e}")
finally:
client.disconnect()
技术要点:
- 实现了一个完整的测试流程,包括:
- 设置配置文件
- 获取硬件信息
- 设置硬件信息
- 验证硬件信息设置
- 擦除硬件信息
- 获取软件信息
- 提供了默认的测试硬件信息
- 包含了异常处理,确保测试过程中的错误能够被捕获和记录
- 使用
finally块确保客户端能够正确断开连接
4. 代码执行流程分析
4.1 整体流程
-
初始化阶段:
- 创建
ConfigurationClient实例 - 调用
connect()方法连接到服务器 - 准备测试数据
- 创建
-
配置管理阶段:
- 加载并设置配置文件
- 执行硬件信息的增删改查操作
- 查询软件信息
-
结束阶段:
- 断开与服务器的连接
- 记录测试结果
4.2 详细流程
-
配置文件加载与设置:
- 读取 JSON 配置文件
- 解析配置文件结构
- 为每个硬件目标构建配置
- 发送
SetProfile请求
-
硬件信息管理:
- 获取当前硬件信息
- 设置新的硬件信息
- 验证硬件信息设置
- 擦除硬件信息
-
软件信息查询:
- 发送
GetSWInfos请求 - 处理并显示软件信息
- 发送
5. 技术亮点
-
配置文件解析:实现了从 JSON 配置文件到 protobuf 消息的自动转换,支持复杂的配置结构。
-
枚举值映射:实现了灵活的枚举值解析和映射机制,支持不同的命名约定和特殊映射。
-
错误处理:实现了完善的错误处理机制,包括 gRPC 错误捕获和记录。
-
测试覆盖:实现了全面的测试流程,覆盖了配置管理的各个方面。
-
代码结构:代码结构清晰,职责分明,易于理解和维护。
6. 代码优化建议
-
日志改进:
- 可以使用结构化日志,便于后续的日志分析
- 增加更详细的错误信息,便于问题定位
-
错误处理增强:
- 可以增加更多的错误类型判断,提高错误处理的精确性
- 实现更细粒度的重试机制,针对不同的错误类型采取不同的重试策略
-
性能优化:
- 对于大量硬件信息的设置,可以考虑批量操作,减少 RPC 调用次数
- 可以使用异步 gRPC 调用,提高并发性能
-
可配置性:
- 将更多的参数(如超时时间、重试次数等)配置化,便于根据不同的环境进行调整
-
测试覆盖:
- 增加单元测试,覆盖更多的场景
- 实现集成测试,验证整个配置管理流程
7. 总结
本文详细分析了基于 gRPC 实现的配置管理客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的配置管理功能,包括加载和设置配置文件、硬件信息的增删改查、软件信息查询等。
通过使用 gRPC 的同步调用功能,该客户端能够高效地与服务器进行通信,同时通过完善的错误处理和日志记录机制,确保了配置管理过程的可靠性。
该实现展示了如何使用 gRPC 构建一个可靠的配置管理系统,对于类似的场景具有参考价值。同时,通过本文提出的优化建议,可以进一步提高系统的性能和可靠性。
8. 输入输出示例
输入输出示例
输入:
import logging
import grpc
from test_case.test_configuration import test_configuration_service
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 创建 gRPC 通道
server_addr = "192.168.1.100:50051"
channel = grpc.insecure_channel(server_addr)
# 测试配置服务
profile_file = "/path/to/profile.json"
test_configuration_service(channel, profile_file, logger)
# 关闭通道
channel.close()
输出:
2026-03-26 10:00:00,000 - INFO - Configuration client stubs created successfully.
TEST 1: SetProfile
2026-03-26 10:00:00,100 - INFO - Loading profiles from /path/to/profile.json
2026-03-26 10:00:00,150 - INFO - Building profile for HW target 'Device1'
2026-03-26 10:00:00,200 - INFO - Sending SetProfile request (1 profiles)...
2026-03-26 10:00:00,250 - INFO - Successfully set profile. Response: code: 0
message: "Profile set successfully"
TEST 2: GetHWInfos
2026-03-26 10:00:00,300 - INFO - HW Infos count: 2
2026-03-26 10:00:00,300 - INFO - HW: MainBoard, SN: MB123456, PN: MB-001, R-State: R1A
2026-03-26 10:00:00,300 - INFO - HW: NIC, SN: NIC123456, PN: NIC-001, R-State: R1B
TEST 3: SetHWInfo
2026-03-26 10:00:00,350 - INFO - Setting HW Info for: NIB
2026-03-26 10:00:00,400 - INFO - Setting HW Info for: NIB
2026-03-26 10:00:00,450 - INFO - Setting HW Info for: NIB
TEST 4: Verify SetHWInfo (GetHWInfos again)
2026-03-26 10:00:00,500 - INFO - hw_name 'NIB' found: True
TEST 5: EraseHwInfo
2026-03-26 10:00:00,550 - INFO - Erase 'NIB': code=0, message='HW info erased successfully'
TEST 6: GetSWInfos
2026-03-26 10:00:00,600 - INFO - SW Infos count: 3
2026-03-26 10:00:00,600 - INFO - SW: Bootloader, PN: BL-001, R-State: R1A
2026-03-26 10:00:00,600 - INFO - SW: Firmware, PN: FW-001, R-State: R1B
2026-03-26 10:00:00,600 - INFO - SW: Application, PN: APP-001, R-State: R1C
2026-03-26 10:00:00,650 - INFO - All configuration tests completed.
2026-03-26 10:00:00,650 - INFO - Disconnected from the server.
DZ容器解析与软件仓库
1. 概述
详细分析基于 gRPC 实现的 DZ 容器解析与软件仓库客户端,包括其核心功能、代码结构和执行流程。该实现包含三个主要组件:
DZContainerParser:用于解析 DZ 容器的 manifest.yaml 和 flashmap.json 文件SoftwareRepositoryClient:用于通过 gRPC 加载 DZ 容器到设备test_dz_loading:用于测试 DZ 容器加载功能
2. 核心代码结构
2.1 DZ 容器解析器 DZContainerParser
DZContainerParser 位于 service_client/dz_container_parser.py 文件中,负责解析 DZ 容器的 manifest.yaml 和 flashmap.json 文件,提取软件区域配置和兼容产品信息。
2.2 软件仓库客户端 SoftwareRepositoryClient
SoftwareRepositoryClient 位于 service_client/software_repository_client.py 文件中,负责通过 gRPC 与服务器通信,加载 DZ 容器到设备。
2.3 测试用例 test_dz_loading
test_dz_loading 函数位于 test_case/test_software_repository.py 文件中,用于测试 DZ 容器加载功能。
3. 关键技术点分析
3.1 DZ 容器解析
def parse(self, manifest_path: str) -> bool:
"""Parse manifest.yaml and flashmap.json from DZ container."""
if not os.path.exists(manifest_path):
self.logger.error(f"Manifest file not found: {manifest_path}")
return False
self.directory = os.path.dirname(os.path.abspath(manifest_path))
try:
with open(manifest_path, 'r') as f:
yaml_data = yaml.safe_load(f.read())
self.manifest = json.loads(json.dumps(yaml_data))
except Exception as e:
self.logger.error(f"Failed to parse manifest: {e}")
return False
for payload in self.manifest.get("payloads", []) or []:
function = payload.get("function", "")
# Collect compatible product numbers from AUAPPLIC
if function == "AUAPPLIC":
for hw in payload.get("hw", []) or []:
pn = hw.get("hw_product_number")
if pn:
self.sw_compat_list.append(pn)
# Parse flashmap
if function == "FLASHMAP" and payload.get("url"):
filename = payload["url"].split("/")[-1]
flashmap_path = os.path.join(self.directory, filename)
try:
raw = open(flashmap_path, 'r').read()
translated = _translate_flashmap(raw)
flashmap = json.loads(translated)
self.sw_area_configs = flashmap.get("SoftwareAreaConfigurations", [])
except Exception as e:
self.logger.error(f"Failed to parse flashmap: {e}")
self.logger.info(f"DZ container parsed. Compatible products: {len(self.sw_compat_list)}, "
f"SW area configs: {len(self.sw_area_configs)}")
return True
技术要点:
- 解析 manifest.yaml 文件,提取兼容产品列表
- 解析 flashmap.json 文件,提取软件区域配置
- 支持 flashmap 中软件类型名称的转换,使其与配置文件兼容
- 提供详细的错误处理和日志记录
3.2 软件仓库客户端 gRPC 通信
def _send_to_server(self, sw_files: Dict[int, Tuple[str, pb2.SoftwareAreaConfiguration]],
dut_position: int) -> bool:
"""
Send software items to server using bidirectional streaming.
Protocol:
1. Send SoftwareItem requests for all files
2. Send SoftwareItemsFinalized
3. Wait for SoftwareItemUploadRequest or SoftwarePreparationsDone
4. Upload file content when requested
"""
if not self.sw_repo_stub:
self.logger.error("Not connected. Call connect() first.")
return False
session = _create_session()
# Prepare software items
items_info: List[Tuple[int, str, pb2.SoftwareItem]] = []
for sw_type, (file_path, _area_cfg) in sw_files.items():
file_size = os.path.getsize(file_path)
file_hash = _calculate_md5(file_path)
item = pb2.SoftwareItem(
description=common_enums_pb2.SoftwareType.Name(sw_type),
product_number="",
rstate="",
sw_type=sw_type,
filename=os.path.basename(file_path),
hash=file_hash,
total_size=file_size,
)
items_info.append((sw_type, file_path, item))
self.logger.info(f"Prepared: {item.description} - {item.filename} ({file_size} bytes)")
# Use a request queue for bidirectional streaming
request_queue = queue.Queue()
result = {"success": False, "error": None}
def request_iterator():
"""Generate requests from queue."""
while True:
req = request_queue.get()
if req is None:
break
yield req
try:
# Start the bidirectional stream
call = self.sw_repo_stub.InitiateBootSoftwareRequest(request_iterator())
# Step 1: Send all SoftwareItem requests
for sw_type, file_path, item in items_info:
req = pb2.SoftwareRequest(
session=session,
dut_position=dut_position,
item=item,
)
request_queue.put(req)
self.logger.info(f"Sent SoftwareItem: {item.description}")
# Step 2: Send SoftwareItemsFinalized
req_finalized = pb2.SoftwareRequest(
session=session,
dut_position=dut_position,
items_finalized=pb2.SoftwareItemsFinalized()
)
request_queue.put(req_finalized)
self.logger.info("Sent SoftwareItemsFinalized")
# Step 3: Process responses
files_uploaded = 0
for response in call:
msg_type = response.WhichOneof("msg")
if msg_type == "sw_item_upload_request":
upload_req = response.sw_item_upload_request
self.logger.info(f"Server requests upload: {common_enums_pb2.SoftwareType.Name(upload_req.swType)}")
# Find matching file
match = None
for sw_type, file_path, item in items_info:
if item.sw_type == upload_req.swType:
match = (sw_type, file_path, item)
break
if not match:
self.logger.error(f"No matching file for upload request: {upload_req.swType}")
continue
_, file_path, item = match
self.logger.info(f"Uploading {os.path.basename(file_path)}...")
# Upload file content in chunks
total_sent = 0
with open(file_path, "rb") as f:
while True:
chunk = f.read(self.CHUNK_SIZE)
if not chunk:
break
content_req = pb2.SoftwareRequest(
session=session,
dut_position=dut_position,
content=pb2.SoftwareItemContent(
swType=item.sw_type,
data=chunk,
),
)
request_queue.put(content_req)
total_sent += len(chunk)
if item.total_size > 0:
progress = (total_sent * 100) // item.total_size
self.logger.info(f" Progress: {progress}% ({total_sent}/{item.total_size})")
files_uploaded += 1
self.logger.info(f"Upload complete: {os.path.basename(file_path)}")
elif msg_type == "sw_preparations_done":
status = response.sw_preparations_done.status
self.logger.info(f"SW preparations done. Status: code={status.code}, msg='{status.message}'")
if status.code != 0:
self.logger.error(f"SW preparations failed: {status.message}")
result["success"] = False
else:
result["success"] = True
break
# Signal end of requests
request_queue.put(None)
self.logger.info(f"DZ loading complete. {files_uploaded} file(s) uploaded.")
return result["success"]
except grpc.RpcError as e:
self.logger.error(f"gRPC error during DZ loading: {e.details()} (code: {e.code().name})")
request_queue.put(None)
return False
except Exception as e:
self.logger.exception(f"Error during DZ loading: {e}")
request_queue.put(None)
return False
技术要点:
- 使用 gRPC 双向流式 RPC
InitiateBootSoftwareRequest - 实现了复杂的通信协议:
- 发送所有软件项信息
- 发送软件项完成通知
- 等待服务器请求上传或准备完成
- 当服务器请求时上传文件内容
- 使用队列管理请求流,实现异步处理
- 分块上传文件,支持大文件传输
- 提供详细的进度报告和错误处理
3.3 DZ 容器加载流程
def load_dz_container(self, dz_container_path: str, product_number: str,
product_rstate: str, security_level: str = "none",
dut_position: int = 1,
profile_path: str = "",
extra_sw_files: Optional[Dict[str, str]] = None) -> bool:
"""
Parse DZ container, update and set profile, resolve files, and send to server.
If profile_path is provided, the SoftwareAreaConfigurations in the profile
JSON will be replaced with entries from the DZ container's flashmap before
sending SetProfile to the server.
Args:
dz_container_path: Path to extracted DZ container directory.
product_number: DUT product number.
product_rstate: DUT R-state.
security_level: "none", "secure_locked", or "secure_unlocked".
dut_position: DUT position.
profile_path: Path to profile JSON file. If provided, will be updated
with flashmap data and sent via SetProfile before loading.
extra_sw_files: Optional dict mapping flashmap SwType name -> file path
for areas not covered by manifest.yaml payloads.
"""
manifest_path = os.path.join(dz_container_path, "manifest.yaml")
parser = DZContainerParser(self.logger)
if not parser.parse(manifest_path):
self.logger.error("Failed to parse DZ container.")
return False
# Update profile with flashmap SoftwareAreaConfigurations and set profile
if profile_path:
profile_full_path = os.path.abspath(profile_path)
if not os.path.exists(profile_full_path):
self.logger.error(f"Profile file not found: '{profile_full_path}'")
return False
if not parser.update_profile_sw_area_configs(profile_full_path):
self.logger.error("Failed to update profile with flashmap data.")
return False
from service_client.configuration import ConfigurationClient
config_client = ConfigurationClient(self.channel, self.logger, dut_position=dut_position)
if not config_client.connect():
self.logger.error("Failed to connect ConfigurationClient for SetProfile.")
return False
config_client.load_and_set_profile(profile_full_path)
self.logger.info("Profile updated with flashmap and set successfully.")
# Get SW area configs from DZ container flashmap
sw_area_configs = parser.get_software_repositories()
if not sw_area_configs:
self.logger.error("No software area configurations found in DZ container flashmap.")
return False
self.logger.info(f"DZ container SW area configs ({len(sw_area_configs)}):")
for cfg in sw_area_configs:
self.logger.info(f" {common_enums_pb2.SoftwareType.Name(cfg.sw_type)}: "
f"start=0x{cfg.start_address:X}, size=0x{cfg.area_size:X}")
# Resolve file paths for MTD targets found in manifest
sw_files: Dict[int, Tuple[str, pb2.SoftwareAreaConfiguration]] = {}
for mtd in _MEASUREMENT_CONFIGURATIONS:
file_path = parser.get_load_file_path(product_number, product_rstate, security_level, mtd)
if file_path and os.path.exists(file_path):
function = _MTD_TO_FUNCTION.get(mtd, "")
if mtd == "initialflashimage" and security_level == "secure_locked":
function = "FLASHIMG_SECLOCK"
elif mtd == "initialflashimage" and security_level == "secure_unlocked":
function = "FLASHIMG_SECUNLOCK"
sw_type = _FUNCTION_TO_SW_TYPE.get(function, common_enums_pb2.SOFTWARE_TYPE_UNSPECIFIED)
matching_cfg = next((c for c in sw_area_configs if c.sw_type == sw_type), None)
if matching_cfg:
sw_files[sw_type] = (file_path, matching_cfg)
self.logger.info(f" {mtd} -> {os.path.basename(file_path)} "
f"(addr=0x{matching_cfg.start_address:X})")
else:
self.logger.warning(f" {mtd}: no matching area config in flashmap")
else:
self.logger.info(f" {mtd}: not found in manifest (may need extra_sw_files)")
# Apply extra_sw_files for areas not covered by manifest payloads
if extra_sw_files:
for sw_type_name, file_path in extra_sw_files.items():
sw_type = _FLASHMAP_SW_TYPE_MAP.get(sw_type_name)
if sw_type is None:
self.logger.warning(f" Unknown SwType name in extra_sw_files: '{sw_type_name}'")
continue
if sw_type in sw_files:
self.logger.info(f" {sw_type_name}: already resolved from manifest, skipping extra")
continue
if not os.path.exists(file_path):
self.logger.warning(f" {sw_type_name}: extra file not found: {file_path}")
continue
matching_cfg = next((c for c in sw_area_configs if c.sw_type == sw_type), None)
if matching_cfg:
sw_files[sw_type] = (file_path, matching_cfg)
self.logger.info(f" {sw_type_name} (extra) -> {os.path.basename(file_path)} "
f"(addr=0x{matching_cfg.start_address:X})")
else:
self.logger.warning(f" {sw_type_name}: no matching area config in flashmap")
# Log summary of areas in flashmap that have no file
resolved_types = set(sw_files.keys())
for cfg in sw_area_configs:
if cfg.sw_type not in resolved_types:
self.logger.warning(f" Area {common_enums_pb2.SoftwareType.Name(cfg.sw_type)} "
f"in flashmap has no file assigned. "
f"Use extra_sw_files to provide it if needed.")
if not sw_files:
self.logger.error("No software files resolved from DZ container.")
return False
return self._send_to_server(sw_files, dut_position)
技术要点:
- 解析 DZ 容器,提取软件区域配置
- 更新配置文件中的软件区域配置(如果提供了配置文件路径)
- 解析 manifest.yaml 文件,为不同的 MTD 目标解析文件路径
- 支持通过 extra_sw_files 参数提供额外的软件文件
- 验证所有软件区域是否都有对应的文件
- 调用 _send_to_server 方法将软件文件发送到服务器
3.4 测试流程
def test_dz_loading(channel: grpc.Channel, logger: logging.Logger,
config: dict = None) -> None:
"""
Test DZ container loading via SoftwareRepositoryService.
Args:
channel: gRPC channel to the interface board.
logger: Logger instance.
config: DZ loading config dict. Uses DZ_LOADING_CONFIG if None.
Supports keys: dz_container_path, product_number, product_rstate,
security_level, profile_path, dut_position, extra_sw_files.
"""
logger.info(f"\nStart test DZ loading")
if config is None:
logger.error("No config provided for DZ loading test.")
return
dz_container_path = os.path.abspath(config.get("dz_container_path", ""))
logger.info(f"Loading DZ container from: '{dz_container_path}'")
if not os.path.isdir(dz_container_path):
logger.error(f"DZ container directory not found: '{dz_container_path}'")
return
client = SoftwareRepositoryClient(channel, logger)
if not client.connect():
logger.error("Failed to connect SoftwareRepositoryClient.")
return
try:
logger.info("\nTEST 1: Load DZ Container")
success = client.load_dz_container(**config)
if not success:
logger.error("DZ loading failed.")
return
logger.info("DZ loading completed successfully.")
logger.info("\nTEST 2: Cleanup After Boot")
cleanup_ok = client.cleanup_after_boot(dut_position=config.get("dut_position", 1))
if cleanup_ok:
logger.info("Cleanup after boot successful.")
else:
logger.error("Cleanup after boot failed.")
logger.info("\nAll DZ loading tests completed.")
except grpc.RpcError as e:
logger.error(f"RPC error during test: {e.details()} (code: {e.code().name})")
except Exception as e:
logger.exception(f"Unexpected error during test: {e}")
技术要点:
- 验证 DZ 容器目录是否存在
- 创建并连接 SoftwareRepositoryClient
- 调用 load_dz_container 方法加载 DZ 容器
- 调用 cleanup_after_boot 方法执行启动后的清理操作
- 提供详细的错误处理和日志记录
4. 代码执行流程分析
4.1 整体流程
-
初始化阶段:
- 创建 SoftwareRepositoryClient 实例
- 连接到服务器
- 准备 DZ 容器加载配置
-
DZ 容器解析阶段:
- 解析 manifest.yaml 文件
- 解析 flashmap.json 文件
- 提取软件区域配置和兼容产品信息
-
配置更新阶段(可选):
- 更新配置文件中的软件区域配置
- 发送 SetProfile 请求到服务器
-
文件解析与准备阶段:
- 为 MTD 目标解析文件路径
- 处理额外的软件文件
- 验证软件区域与文件的对应关系
-
文件上传阶段:
- 发送软件项信息
- 等待服务器请求
- 分块上传文件内容
- 处理服务器响应
-
清理阶段:
- 执行启动后的清理操作
- 记录测试结果
4.2 详细流程
-
DZ 容器解析:
- 读取并解析 manifest.yaml 文件
- 提取兼容产品列表
- 读取并解析 flashmap.json 文件
- 转换软件类型名称
- 提取软件区域配置
-
配置更新:
- 读取配置文件
- 更新软件区域配置
- 写回配置文件
- 发送 SetProfile 请求
-
文件解析与准备:
- 为每个 MTD 目标解析文件路径
- 处理安全级别
- 匹配软件类型与区域配置
- 处理额外的软件文件
- 验证所有软件区域是否都有对应的文件
-
文件上传:
- 准备软件项信息
- 发送软件项信息
- 发送软件项完成通知
- 等待服务器请求
- 分块上传文件内容
- 处理服务器响应
- 确认上传结果
-
清理操作:
- 发送 CleanupAfterBoot 请求
- 处理响应结果
5. 技术亮点
-
DZ 容器解析:实现了对 DZ 容器的完整解析,包括 manifest.yaml 和 flashmap.json 文件的处理。
-
软件类型映射:实现了灵活的软件类型映射机制,支持不同命名约定之间的转换。
-
gRPC 双向流式通信:使用 gRPC 的双向流式 RPC 功能,实现了复杂的通信协议。
-
文件分块上传:实现了大文件的分块上传,支持进度报告。
-
错误处理:实现了完善的错误处理机制,包括文件解析错误、gRPC 错误等。
-
配置文件更新:支持自动更新配置文件中的软件区域配置。
-
测试覆盖:实现了全面的测试流程,覆盖了 DZ 容器加载的各个方面。
6. 代码优化建议
-
日志改进:
- 可以使用结构化日志,便于后续的日志分析
- 增加更详细的错误信息,便于问题定位
-
错误处理增强:
- 可以增加更多的错误类型判断,提高错误处理的精确性
- 实现更细粒度的重试机制,针对不同的错误类型采取不同的重试策略
-
性能优化:
- 对于大文件的上传,可以考虑使用并行上传,提高上传速度
- 可以使用异步 gRPC 调用,提高并发性能
-
可配置性:
- 将更多的参数(如超时时间、重试次数等)配置化,便于根据不同的环境进行调整
-
测试覆盖:
- 增加单元测试,覆盖更多的场景
- 实现集成测试,验证整个 DZ 容器加载流程
7. 总结
本文详细分析了基于 gRPC 实现的 DZ 容器解析与软件仓库客户端,包括其核心功能、代码结构和执行流程。该实现包含三个主要组件:DZ 容器解析器、软件仓库客户端和测试用例。
通过使用 gRPC 的双向流式 RPC 功能,该客户端能够高效地处理大文件的上传,同时通过完善的错误处理和日志记录机制,确保了 DZ 容器加载过程的可靠性。
该实现展示了如何使用 gRPC 构建一个可靠的软件仓库系统,对于类似的场景具有参考价值。同时,通过本文提出的优化建议,可以进一步提高系统的性能和可靠性。
8. 输入输出示例
输入输出示例
输入:
import logging
import grpc
from test_case.test_software_repository import test_dz_loading
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 创建 gRPC 通道
server_addr = "192.168.1.100:50051"
channel = grpc.insecure_channel(server_addr)
# 配置 DZ 容器加载
config = {
"dz_container_path": "/path/to/dz_container",
"product_number": "ROA1281122/1",
"product_rstate": "R1A",
"security_level": "none",
"profile_path": "/path/to/profile.json",
"dut_position": 1,
"extra_sw_files": {
"ProductionParameters": "/path/to/production_parameters.bin"
}
}
# 测试 DZ 容器加载
test_dz_loading(channel, logger, config)
# 关闭通道
channel.close()
输出:
2026-03-26 10:00:00,000 - INFO -
Start test DZ loading
2026-03-26 10:00:00,000 - INFO - Loading DZ container from: '/path/to/dz_container'
2026-03-26 10:00:00,100 - INFO - SW repo client stubs created successfully.
TEST 1: Load DZ Container
2026-03-26 10:00:00,150 - INFO - DZ container parsed. Compatible products: 2, SW area configs: 6
2026-03-26 10:00:00,200 - INFO - Updated SoftwareAreaConfigurations for 'Device1' (6 entries from flashmap)
2026-03-26 10:00:00,250 - INFO - Configuration client stubs created successfully.
2026-03-26 10:00:00,300 - INFO - Loading profiles from /path/to/profile.json
2026-03-26 10:00:00,350 - INFO - Building profile for HW target 'Device1'
2026-03-26 10:00:00,400 - INFO - Sending SetProfile request (1 profiles)...
2026-03-26 10:00:00,450 - INFO - Successfully set profile. Response: code: 0
message: "Profile set successfully"
2026-03-26 10:00:00,450 - INFO - Profile updated with flashmap and set successfully.
2026-03-26 10:00:00,450 - INFO - DZ container SW area configs (6):
2026-03-26 10:00:00,450 - INFO - SOFTWARE_TYPE_PBOOT: start=0x0, size=0x100000
2026-03-26 10:00:00,450 - INFO - SOFTWARE_TYPE_SBOOT: start=0x100000, size=0x200000
2026-03-26 10:00:00,450 - INFO - SOFTWARE_TYPE_FAAP: start=0x300000, size=0x1000000
2026-03-26 10:00:00,450 - INFO - SOFTWARE_TYPE_SLOT_CONTENT_TABLE: start=0x1300000, size=0x100000
2026-03-26 10:00:00,450 - INFO - SOFTWARE_TYPE_XCS_CONFIG: start=0x1400000, size=0x100000
2026-03-26 10:00:00,450 - INFO - SOFTWARE_TYPE_INITIAL_FLASH_IMAGE: start=0x1500000, size=0x4000000
2026-03-26 10:00:00,500 - INFO - initialflashimage -> initialflashimage.bin (addr=0x1500000)
2026-03-26 10:00:00,500 - INFO - sboot -> sboot.bin (addr=0x100000)
2026-03-26 10:00:00,500 - INFO - pboot -> pboot.bin (addr=0x0)
2026-03-26 10:00:00,500 - INFO - slotcontenttable -> slotcontenttable.bin (addr=0x1300000)
2026-03-26 10:00:00,500 - INFO - xcsconfig -> xcsconfig.bin (addr=0x1400000)
2026-03-26 10:00:00,500 - INFO - faap -> faap.bin (addr=0x300000)
2026-03-26 10:00:00,500 - INFO - ProductionParameters (extra) -> production_parameters.bin (addr=0x1600000)
2026-03-26 10:00:00,550 - INFO - Prepared: SOFTWARE_TYPE_PBOOT - pboot.bin (1048576 bytes)
2026-03-26 10:00:00,550 - INFO - Prepared: SOFTWARE_TYPE_SBOOT - sboot.bin (2097152 bytes)
2026-03-26 10:00:00,550 - INFO - Prepared: SOFTWARE_TYPE_FAAP - faap.bin (16777216 bytes)
2026-03-26 10:00:00,550 - INFO - Prepared: SOFTWARE_TYPE_SLOT_CONTENT_TABLE - slotcontenttable.bin (1048576 bytes)
2026-03-26 10:00:00,550 - INFO - Prepared: SOFTWARE_TYPE_XCS_CONFIG - xcsconfig.bin (1048576 bytes)
2026-03-26 10:00:00,550 - INFO - Prepared: SOFTWARE_TYPE_INITIAL_FLASH_IMAGE - initialflashimage.bin (67108864 bytes)
2026-03-26 10:00:00,550 - INFO - Prepared: SOFTWARE_TYPE_PRODUCTION_PARAMETERS - production_parameters.bin (1048576 bytes)
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItem: SOFTWARE_TYPE_PBOOT
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItem: SOFTWARE_TYPE_SBOOT
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItem: SOFTWARE_TYPE_FAAP
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItem: SOFTWARE_TYPE_SLOT_CONTENT_TABLE
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItem: SOFTWARE_TYPE_XCS_CONFIG
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItem: SOFTWARE_TYPE_INITIAL_FLASH_IMAGE
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItem: SOFTWARE_TYPE_PRODUCTION_PARAMETERS
2026-03-26 10:00:00,600 - INFO - Sent SoftwareItemsFinalized
2026-03-26 10:00:00,650 - INFO - Server requests upload: SOFTWARE_TYPE_PBOOT
2026-03-26 10:00:00,650 - INFO - Uploading pboot.bin...
2026-03-26 10:00:00,650 - INFO - Progress: 100% (1048576/1048576)
2026-03-26 10:00:00,650 - INFO - Upload complete: pboot.bin
2026-03-26 10:00:00,700 - INFO - Server requests upload: SOFTWARE_TYPE_SBOOT
2026-03-26 10:00:00,700 - INFO - Uploading sboot.bin...
2026-03-26 10:00:00,700 - INFO - Progress: 50% (1048576/2097152)
2026-03-26 10:00:00,700 - INFO - Progress: 100% (2097152/2097152)
2026-03-26 10:00:00,700 - INFO - Upload complete: sboot.bin
2026-03-26 10:00:00,750 - INFO - Server requests upload: SOFTWARE_TYPE_FAAP
2026-03-26 10:00:00,750 - INFO - Uploading faap.bin...
2026-03-26 10:00:01,750 - INFO - Progress: 6% (1048576/16777216)
2026-03-26 10:00:02,750 - INFO - Progress: 12% (2097152/16777216)
2026-03-26 10:00:03,750 - INFO - Progress: 18% (3145728/16777216)
2026-03-26 10:00:04,750 - INFO - Progress: 25% (4194304/16777216)
2026-03-26 10:00:05,750 - INFO - Progress: 31% (5242880/16777216)
2026-03-26 10:00:06,750 - INFO - Progress: 37% (6291456/16777216)
2026-03-26 10:00:07,750 - INFO - Progress: 43% (7340032/16777216)
2026-03-26 10:00:08,750 - INFO - Progress: 50% (8388608/16777216)
2026-03-26 10:00:09,750 - INFO - Progress: 56% (9437184/16777216)
2026-03-26 10:00:10,750 - INFO - Progress: 62% (10485760/16777216)
2026-03-26 10:00:11,750 - INFO - Progress: 68% (11534336/16777216)
2026-03-26 10:00:12,750 - INFO - Progress: 75% (12582912/16777216)
2026-03-26 10:00:13,750 - INFO - Progress: 81% (13631488/16777216)
2026-03-26 10:00:14,750 - INFO - Progress: 87% (14680064/16777216)
2026-03-26 10:00:15,750 - INFO - Progress: 93% (15728640/16777216)
2026-03-26 10:00:16,750 - INFO - Progress: 100% (16777216/16777216)
2026-03-26 10:00:16,750 - INFO - Upload complete: faap.bin
2026-03-26 10:00:16,800 - INFO - Server requests upload: SOFTWARE_TYPE_SLOT_CONTENT_TABLE
2026-03-26 10:00:16,800 - INFO - Uploading slotcontenttable.bin...
2026-03-26 10:00:16,800 - INFO - Progress: 100% (1048576/1048576)
2026-03-26 10:00:16,800 - INFO - Upload complete: slotcontenttable.bin
2026-03-26 10:00:16,850 - INFO - Server requests upload: SOFTWARE_TYPE_XCS_CONFIG
2026-03-26 10:00:16,850 - INFO - Uploading xcsconfig.bin...
2026-03-26 10:00:16,850 - INFO - Progress: 100% (1048576/1048576)
2026-03-26 10:00:16,850 - INFO - Upload complete: xcsconfig.bin
2026-03-26 10:00:16,900 - INFO - Server requests upload: SOFTWARE_TYPE_INITIAL_FLASH_IMAGE
2026-03-26 10:00:16,900 - INFO - Uploading initialflashimage.bin...
2026-03-26 10:00:17,900 - INFO - Progress: 1% (1048576/67108864)
2026-03-26 10:00:18,900 - INFO - Progress: 3% (2097152/67108864)
2026-03-26 10:00:19,900 - INFO - Progress: 4% (3145728/67108864)
2026-03-26 10:00:20,900 - INFO - Progress: 6% (4194304/67108864)
2026-03-26 10:00:21,900 - INFO - Progress: 7% (5242880/67108864)
2026-03-26 10:00:22,900 - INFO - Progress: 9% (6291456/67108864)
2026-03-26 10:00:23,900 - INFO - Progress: 10% (7340032/67108864)
2026-03-26 10:00:24,900 - INFO - Progress: 12% (8388608/67108864)
2026-03-26 10:00:25,900 - INFO - Progress: 13% (9437184/67108864)
2026-03-26 10:00:26,900 - INFO - Progress: 15% (10485760/67108864)
2026-03-26 10:00:27,900 - INFO - Progress: 16% (11534336/67108864)
2026-03-26 10:00:28,900 - INFO - Progress: 18% (12582912/67108864)
2026-03-26 10:00:29,900 - INFO - Progress: 19% (13631488/67108864)
2026-03-26 10:00:30,900 - INFO - Progress: 21% (14680064/67108864)
2026-03-26 10:00:31,900 - INFO - Progress: 22% (15728640/67108864)
2026-03-26 10:00:32,900 - INFO - Progress: 24% (16777216/67108864)
2026-03-26 10:00:33,900 - INFO - Progress: 25% (17825792/67108864)
2026-03-26 10:00:34,900 - INFO - Progress: 27% (18874368/67108864)
2026-03-26 10:00:35,900 - INFO - Progress: 28% (19922944/67108864)
2026-03-26 10:00:36,900 - INFO - Progress: 30% (20971520/67108864)
2026-03-26 10:00:37,900 - INFO - Progress: 31% (22020096/67108864)
2026-03-26 10:00:38,900 - INFO - Progress: 33% (23068672/67108864)
2026-03-26 10:00:39,900 - INFO - Progress: 34% (24117248/67108864)
2026-03-26 10:00:40,900 - INFO - Progress: 36% (25165824/67108864)
2026-03-26 10:00:41,900 - INFO - Progress: 37% (26214400/67108864)
2026-03-26 10:00:42,900 - INFO - Progress: 39% (27262976/67108864)
2026-03-26 10:00:43,900 - INFO - Progress: 40% (28311552/67108864)
2026-03-26 10:00:44,900 - INFO - Progress: 42% (29360128/67108864)
2026-03-26 10:00:45,900 - INFO - Progress: 43% (30408704/67108864)
2026-03-26 10:00:46,900 - INFO - Progress: 45% (31457280/67108864)
2026-03-26 10:00:47,900 - INFO - Progress: 46% (32505856/67108864)
2026-03-26 10:00:48,900 - INFO - Progress: 48% (33554432/67108864)
2026-03-26 10:00:49,900 - INFO - Progress: 49% (34603008/67108864)
2026-03-26 10:00:50,900 - INFO - Progress: 51% (35651584/67108864)
2026-03-26 10:00:51,900 - INFO - Progress: 52% (36700160/67108864)
2026-03-26 10:00:52,900 - INFO - Progress: 54% (37748736/67108864)
2026-03-26 10:00:53,900 - INFO - Progress: 55% (38797312/67108864)
2026-03-26 10:00:54,900 - INFO - Progress: 57% (39845888/67108864)
2026-03-26 10:00:55,900 - INFO - Progress: 58% (40894464/67108864)
2026-03-26 10:00:56,900 - INFO - Progress: 60% (41943040/67108864)
2026-03-26 10:00:57,900 - INFO - Progress: 61% (42991616/67108864)
2026-03-26 10:00:58,900 - INFO - Progress: 63% (44040192/67108864)
2026-03-26 10:00:59,900 - INFO - Progress: 64% (45088768/67108864)
2026-03-26 10:01:00,900 - INFO - Progress: 66% (46137344/67108864)
2026-03-26 10:01:01,900 - INFO - Progress: 67% (47185920/67108864)
2026-03-26 10:01:02,900 - INFO - Progress: 69% (48234496/67108864)
2026-03-26 10:01:03,900 - INFO - Progress: 70% (49283072/67108864)
2026-03-26 10:01:04,900 - INFO - Progress: 72% (50331648/67108864)
2026-03-26 10:01:05,900 - INFO - Progress: 73% (51380224/67108864)
2026-03-26 10:01:06,900 - INFO - Progress: 75% (52428800/67108864)
2026-03-26 10:01:07,900 - INFO - Progress: 76% (53477376/67108864)
2026-03-26 10:01:08,900 - INFO - Progress: 78% (54525952/67108864)
2026-03-26 10:01:09,900 - INFO - Progress: 79% (55574528/67108864)
2026-03-26 10:01:10,900 - INFO - Progress: 81% (56623104/67108864)
2026-03-26 10:01:11,900 - INFO - Progress: 82% (57671680/67108864)
2026-03-26 10:01:12,900 - INFO - Progress: 84% (58720256/67108864)
2026-03-26 10:01:13,900 - INFO - Progress: 85% (59768832/67108864)
2026-03-26 10:01:14,900 - INFO - Progress: 87% (60817408/67108864)
2026-03-26 10:01:15,900 - INFO - Progress: 88% (61865984/67108864)
2026-03-26 10:01:16,900 - INFO - Progress: 90% (62914560/67108864)
2026-03-26 10:01:17,900 - INFO - Progress: 91% (63963136/67108864)
2026-03-26 10:01:18,900 - INFO - Progress: 93% (65011712/67108864)
2026-03-26 10:01:19,900 - INFO - Progress: 94% (66060288/67108864)
2026-03-26 10:01:20,900 - INFO - Progress: 96% (67108864/67108864)
2026-03-26 10:01:20,900 - INFO - Upload complete: initialflashimage.bin
2026-03-26 10:01:20,950 - INFO - Server requests upload: SOFTWARE_TYPE_PRODUCTION_PARAMETERS
2026-03-26 10:01:20,950 - INFO - Uploading production_parameters.bin...
2026-03-26 10:01:20,950 - INFO - Progress: 100% (1048576/1048576)
2026-03-26 10:01:20,950 - INFO - Upload complete: production_parameters.bin
2026-03-26 10:01:21,000 - INFO - SW preparations done. Status: code=0, msg='Software loaded successfully'
2026-03-26 10:01:21,000 - INFO - DZ loading complete. 7 file(s) uploaded.
2026-03-26 10:01:21,000 - INFO - DZ loading completed successfully.
TEST 2: Cleanup After Boot
2026-03-26 10:01:21,050 - INFO - Cleanup after boot successful.
2026-03-26 10:01:21,050 - INFO - All DZ loading tests completed.
GPIO 控制
1. 项目概述
本文将详细分析基于 gRPC 实现的 GPIO 控制客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的 GPIO 控制功能,包括设置输出状态、读取输入状态和设置安全默认状态。
2. 核心代码结构
2.1 GPIO 客户端类 GPIOClient
GPIOClient 是整个 GPIO 控制的核心实现,位于 service_client/gpio.py 文件中。该类封装了与服务器交互的所有逻辑,包括连接管理、GPIO 状态设置和读取等功能。
2.2 测试用例 test_gpio
test_gpio 函数位于 test_case/test_gpio.py 文件中,是一个全面的测试函数,用于验证 GPIO 客户端的各项功能。
3. 关键技术点分析
3.1 gRPC 连接管理
def connect(self) -> bool:
try:
self.gpio_stub = pb2_grpc.TestInterfaceGPIOServiceStub(self.channel)
self.logger.info("GPIO client stub created successfully.")
return True
except grpc.RpcError as e:
self.logger.error(f"Failed to connect: {e}")
return False
技术要点:
- 接收一个已建立的 gRPC 通道作为参数
- 初始化
TestInterfaceGPIOServiceStub用于与服务器通信 - 实现了简单的错误处理,捕获 gRPC 连接错误
- 返回连接状态,便于调用者判断连接是否成功
3.2 GPIO 输出控制
def set_output(self, alias: str, state: bool, timeout: float = 10.0) -> bool:
"""Set GPIO output state.
Args:
alias: GPIO alias from profile IO map (e.g. "TEST_GPIO_0").
state: Desired output state (True=high, False=low).
timeout: RPC timeout in seconds.
"""
if not self.gpio_stub:
self.logger.error("Not connected. Call connect() first.")
return False
try:
request = pb2.IORequest(
session=_create_session(),
dut_position=self.dut_position,
alias=alias,
state=state,
)
response = self.gpio_stub.SetOutput(request, timeout=timeout)
if response.code == 0:
self.logger.info(f"GPIO '{alias}' set to {'HIGH' if state else 'LOW'}")
return True
else:
self.logger.error(f"Failed to set GPIO '{alias}': code={response.code}, "
f"message='{response.message}'")
return False
except grpc.RpcError as e:
self.logger.error(f"gRPC error setting GPIO '{alias}': {e.details()}")
return False
技术要点:
- 接收 GPIO 别名、目标状态和超时时间作为参数
- 验证客户端是否已连接
- 创建
IORequest请求,包含会话信息、DUT 位置、GPIO 别名和目标状态 - 调用 gRPC 方法
SetOutput发送请求 - 处理响应,检查返回码并记录日志
- 处理 gRPC 错误,确保函数在遇到异常时能够优雅失败
3.3 GPIO 输入读取
def get_input(self, alias: str, timeout: float = 10.0) -> Optional[bool]:
"""Get GPIO input state.
Args:
alias: GPIO alias from profile IO map (e.g. "TEST_GPIO_4").
timeout: RPC timeout in seconds.
Returns:
GPIO state if successful, None if failed.
"""
if not self.gpio_stub:
self.logger.error("Not connected. Call connect() first.")
return None
try:
request = pb2.IORequest(
session=_create_session(),
dut_position=self.dut_position,
alias=alias,
)
response = self.gpio_stub.GetInput(request, timeout=timeout)
if response.status.code == 0:
self.logger.info(f"GPIO '{alias}' state: {'HIGH' if response.state else 'LOW'}")
return response.state
else:
self.logger.error(f"Failed to get GPIO '{alias}': code={response.status.code}, "
f"message='{response.status.message}'")
return None
except grpc.RpcError as e:
self.logger.error(f"gRPC error getting GPIO '{alias}': {e.details()}")
return None
技术要点:
- 接收 GPIO 别名和超时时间作为参数
- 验证客户端是否已连接
- 创建
IORequest请求,包含会话信息、DUT 位置和 GPIO 别名 - 调用 gRPC 方法
GetInput发送请求 - 处理响应,检查返回码并记录日志
- 返回 GPIO 状态(成功时)或 None(失败时)
- 处理 gRPC 错误,确保函数在遇到异常时能够优雅失败
3.4 安全默认状态设置
def set_safe_defaults(self, timeout: float = 10.0) -> bool:
"""Set all IO pins to safe default state.
Args:
timeout: RPC timeout in seconds.
"""
if not self.gpio_stub:
self.logger.error("Not connected. Call connect() first.")
return False
try:
request = pb2.IOSafeDefaultsRequest(
session=_create_session(),
dut_position=self.dut_position,
)
response = self.gpio_stub.SetSafeDefaults(request, timeout=timeout)
if response.code == 0:
self.logger.info("GPIO safe defaults set successfully.")
return True
else:
self.logger.error(f"Failed to set safe defaults: code={response.code}, "
f"message='{response.message}'")
return False
except grpc.RpcError as e:
self.logger.error(f"gRPC error setting safe defaults: {e.details()}")
return False
技术要点:
- 接收超时时间作为参数
- 验证客户端是否已连接
- 创建
IOSafeDefaultsRequest请求,包含会话信息和 DUT 位置 - 调用 gRPC 方法
SetSafeDefaults发送请求 - 处理响应,检查返回码并记录日志
- 处理 gRPC 错误,确保函数在遇到异常时能够优雅失败
3.5 测试流程
def test_gpio(channel: grpc.Channel, logger: logging.Logger,
gpio_config: dict = None, dut_position: int = 1) -> None:
"""Test GPIO service: SetOutput, GetInput, SetSafeDefaults.
Args:
channel: gRPC channel to the interface board.
logger: Logger instance.
gpio_config: Optional config dict with keys:
outputs: list of {alias, state} dicts
inputs: list of aliases to read
dut_position: DUT position.
"""
if gpio_config is None:
gpio_config = {}
outputs = gpio_config.get("outputs", [])
inputs = gpio_config.get("inputs", [])
client = GPIOClient(channel, logger, dut_position=dut_position)
if not client.connect():
logger.error("Failed to connect GPIOClient.")
return
try:
# 1. Set outputs
logger.info("\nTEST 1: SetOutput")
for item in outputs:
alias = item["alias"]
state = item["state"]
ok = client.set_output(alias, state)
logger.info(f" {alias} -> {'HIGH' if state else 'LOW'}: {'OK' if ok else 'FAIL'}")
# 2. Read inputs
logger.info("\nTEST 2: GetInput")
for alias in inputs:
state = client.get_input(alias)
if state is not None:
logger.info(f" {alias}: {'HIGH' if state else 'LOW'}")
else:
logger.error(f" {alias}: read failed")
# 3. Safe defaults
logger.info("\nTEST 3: SetSafeDefaults")
ok = client.set_safe_defaults()
logger.info(f" SetSafeDefaults: {'OK' if ok else 'FAIL'}")
logger.info("\nAll GPIO tests completed.")
except grpc.RpcError as e:
logger.error(f"RPC error during test: {e.details()} (code: {e.code().name})")
except Exception as e:
logger.exception(f"Unexpected error during test: {e}")
技术要点:
- 接收 gRPC 通道、日志记录器、GPIO 配置和 DUT 位置作为参数
- 从配置中提取要测试的输出和输入
- 创建并连接 GPIOClient
- 测试三个主要功能:
- 设置输出状态
- 读取输入状态
- 设置安全默认状态
- 提供详细的错误处理和日志记录
- 支持通过配置字典自定义测试用例
4. 代码执行流程分析
4.1 整体流程
-
初始化阶段:
- 创建 GPIOClient 实例
- 调用 connect() 方法连接到服务器
- 准备测试数据(如果是测试流程)
-
GPIO 操作阶段:
- 设置 GPIO 输出状态
- 读取 GPIO 输入状态
- 设置安全默认状态(可选)
-
结束阶段:
- 记录操作结果
- 处理异常情况
4.2 详细流程
-
连接服务器:
- 创建 gRPC 通道
- 初始化 GPIO 服务 stub
- 验证连接是否成功
-
设置 GPIO 输出:
- 创建 IORequest 请求
- 发送 SetOutput 请求
- 处理响应结果
-
读取 GPIO 输入:
- 创建 IORequest 请求
- 发送 GetInput 请求
- 处理响应结果,返回 GPIO 状态
-
设置安全默认状态:
- 创建 IOSafeDefaultsRequest 请求
- 发送 SetSafeDefaults 请求
- 处理响应结果
5. 技术亮点
-
简洁的 API 设计:提供了简单直观的方法来控制 GPIO,包括设置输出、读取输入和设置安全默认状态。
-
完善的错误处理:实现了全面的错误处理机制,包括连接错误、gRPC 错误和响应错误。
-
详细的日志记录:在每个操作中都提供了详细的日志记录,便于调试和监控。
-
灵活的配置:通过配置字典可以灵活地指定要测试的 GPIO 输出和输入。
-
类型提示:使用了 Python 的类型提示,提高了代码的可读性和可维护性。
-
会话管理:实现了会话创建和管理,确保每个请求都有唯一的会话标识。
6. 代码优化建议
-
超时时间配置:
- 可以将超时时间配置化,便于根据不同的环境进行调整
- 可以为不同的操作设置不同的超时时间
-
错误处理增强:
- 可以增加更多的错误类型判断,提高错误处理的精确性
- 可以实现重试机制,对于临时性错误进行自动重试
-
批量操作:
- 可以实现批量设置多个 GPIO 输出的功能,减少 gRPC 调用次数
- 可以实现批量读取多个 GPIO 输入的功能,提高效率
-
状态缓存:
- 可以实现 GPIO 状态缓存,减少不必要的 gRPC 调用
- 可以提供状态变化的回调机制,及时通知状态变化
-
测试覆盖:
- 增加单元测试,覆盖更多的场景
- 实现集成测试,验证整个 GPIO 控制流程
7. 总结
本文详细分析了基于 gRPC 实现的 GPIO 控制客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的 GPIO 控制功能,包括设置输出状态、读取输入状态和设置安全默认状态。
通过使用 gRPC 的同步调用功能,该客户端能够高效地与服务器进行通信,同时通过完善的错误处理和日志记录机制,确保了 GPIO 控制过程的可靠性。
该实现展示了如何使用 gRPC 构建一个可靠的 GPIO 控制系统,对于类似的场景具有参考价值。同时,通过本文提出的优化建议,可以进一步提高系统的性能和可靠性。
8. 输入输出示例
输入输出示例
输入:
import logging
import grpc
from test_case.test_gpio import test_gpio
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 创建 gRPC 通道
server_addr = "192.168.1.100:50051"
channel = grpc.insecure_channel(server_addr)
# 配置 GPIO 测试
gpio_config = {
"outputs": [
{"alias": "TEST_GPIO_0", "state": True},
{"alias": "TEST_GPIO_1", "state": False},
{"alias": "TEST_GPIO_2", "state": True}
],
"inputs": ["TEST_GPIO_4", "TEST_GPIO_5", "TEST_GPIO_6"]
}
# 测试 GPIO 控制
test_gpio(channel, logger, gpio_config=gpio_config, dut_position=1)
# 关闭通道
channel.close()
输出:
2026-03-26 10:00:00,000 - INFO - GPIO client stub created successfully.
TEST 1: SetOutput
2026-03-26 10:00:00,100 - INFO - GPIO 'TEST_GPIO_0' set to HIGH
2026-03-26 10:00:00,100 - INFO - TEST_GPIO_0 -> HIGH: OK
2026-03-26 10:00:00,150 - INFO - GPIO 'TEST_GPIO_1' set to LOW
2026-03-26 10:00:00,150 - INFO - TEST_GPIO_1 -> LOW: OK
2026-03-26 10:00:00,200 - INFO - GPIO 'TEST_GPIO_2' set to HIGH
2026-03-26 10:00:00,200 - INFO - TEST_GPIO_2 -> HIGH: OK
TEST 2: GetInput
2026-03-26 10:00:00,250 - INFO - GPIO 'TEST_GPIO_4' state: HIGH
2026-03-26 10:00:00,250 - INFO - TEST_GPIO_4: HIGH
2026-03-26 10:00:00,300 - INFO - GPIO 'TEST_GPIO_5' state: LOW
2026-03-26 10:00:00,300 - INFO - TEST_GPIO_5: LOW
2026-03-26 10:00:00,350 - INFO - GPIO 'TEST_GPIO_6' state: HIGH
2026-03-26 10:00:00,350 - INFO - TEST_GPIO_6: HIGH
TEST 3: SetSafeDefaults
2026-03-26 10:00:00,400 - INFO - GPIO safe defaults set successfully.
2026-03-26 10:00:00,400 - INFO - SetSafeDefaults: OK
2026-03-26 10:00:00,400 - INFO - All GPIO tests completed.
模式选择
1. 项目概述
本文将详细分析基于 gRPC 实现的模式选择客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的模式选择功能,允许用户通过 gRPC 接口选择设备的启动模式。
2. 核心代码结构
2.1 模式选择客户端类 ModeSelectClient
ModeSelectClient 是整个模式选择功能的核心实现,位于 service_client/mode_select.py 文件中。该类封装了与服务器交互的所有逻辑,包括连接管理和模式选择功能。
2.2 测试用例 test_mode_select
test_mode_select 函数位于 test_case/test_mode_select.py 文件中,是一个简单的测试函数,用于验证模式选择客户端的功能。
3. 关键技术点分析
3.1 gRPC 连接管理
def connect(self) -> bool:
try:
self.mode_select_stub = pb2_grpc.TestInterfaceModeSelectServiceStub(self.channel)
self.logger.info("Mode select client stub created successfully.")
return True
except grpc.RpcError as e:
self.logger.error(f"Failed to connect: {e}")
return False
技术要点:
- 接收一个已建立的 gRPC 通道作为参数
- 初始化
TestInterfaceModeSelectServiceStub用于与服务器通信 - 实现了简单的错误处理,捕获 gRPC 连接错误
- 返回连接状态,便于调用者判断连接是否成功
3.2 模式选择功能
def select(self, mode: str, timeout: float = 10.0) -> bool:
"""Select a boot mode.
Args:
mode: Boot mode enum name, e.g. "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL".
timeout: RPC timeout in seconds.
"""
if not self.mode_select_stub:
self.logger.error("Not connected. Call connect() first.")
return False
boot_mode_enum = _BOOT_MODE_MAP.get(mode)
if boot_mode_enum is None:
self.logger.error(f"Unknown boot mode: '{mode}'. "
f"Valid modes: {list(_BOOT_MODE_MAP.keys())}")
return False
try:
request = pb2.ModeSelectRequest(
session=_create_session(),
dut_position=self.dut_position,
boot_mode=boot_mode_enum,
)
response = self.mode_select_stub.Select(request, timeout=timeout)
if response.code == 0:
self.logger.info(f"Successfully selected mode '{mode}'")
return True
else:
self.logger.error(f"Failed to select mode '{mode}': code={response.code}, "
f"message='{response.message}'")
return False
except grpc.RpcError as e:
self.logger.error(f"gRPC error selecting mode '{mode}': {e.details()}")
return False
技术要点:
- 接收模式字符串和超时时间作为参数
- 验证客户端是否已连接
- 将模式字符串映射到对应的枚举值
- 创建
ModeSelectRequest请求,包含会话信息、DUT 位置和启动模式 - 调用 gRPC 方法
Select发送请求 - 处理响应,检查返回码并记录日志
- 处理 gRPC 错误,确保函数在遇到异常时能够优雅失败
3.3 测试流程
def test_mode_select(channel: grpc.Channel, logger: logging.Logger,
mode: str = "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL",
dut_position: int = 1) -> None:
"""Test mode selection service.
Args:
channel: gRPC channel to the interface board.
logger: Logger instance.
mode: Boot mode string to select.
"""
client = ModeSelectClient(channel, logger, dut_position=dut_position)
if not client.connect():
logger.error("Failed to connect ModeSelectClient.")
return
try:
logger.info(f"\nTEST: Select '{mode}'")
ok = client.select(mode)
logger.info(f" Result: {'OK' if ok else 'FAIL'}")
except grpc.RpcError as e:
logger.error(f"RPC error during test: {e.details()} (code: {e.code().name})")
except Exception as e:
logger.exception(f"Unexpected error during test: {e}")
技术要点:
- 接收 gRPC 通道、日志记录器、模式字符串和 DUT 位置作为参数
- 创建并连接
ModeSelectClient - 调用
select方法选择指定的模式 - 记录测试结果
- 提供详细的错误处理和日志记录
4. 代码执行流程分析
4.1 整体流程
-
初始化阶段:
- 创建
ModeSelectClient实例 - 调用
connect()方法连接到服务器 - 准备测试数据(如果是测试流程)
- 创建
-
模式选择阶段:
- 验证模式字符串是否有效
- 发送模式选择请求
- 处理响应结果
-
结束阶段:
- 记录操作结果
- 处理异常情况
4.2 详细流程
-
连接服务器:
- 创建 gRPC 通道
- 初始化模式选择服务 stub
- 验证连接是否成功
-
选择模式:
- 验证客户端是否已连接
- 验证模式字符串是否有效
- 创建
ModeSelectRequest请求 - 发送
Select请求 - 处理响应结果
-
测试流程:
- 创建
ModeSelectClient实例 - 连接到服务器
- 调用
select方法选择指定的模式 - 记录测试结果
- 处理异常情况
- 创建
5. 技术亮点
-
简洁的 API 设计:提供了简单直观的方法来选择启动模式,使用字符串表示模式名称,便于用户使用。
-
完善的错误处理:实现了全面的错误处理机制,包括连接错误、模式无效错误、gRPC 错误和响应错误。
-
详细的日志记录:在每个操作中都提供了详细的日志记录,便于调试和监控。
-
模式映射:实现了模式字符串到枚举值的映射,提高了代码的可读性和可维护性。
-
类型提示:使用了 Python 的类型提示,提高了代码的可读性和可维护性。
-
会话管理:实现了会话创建和管理,确保每个请求都有唯一的会话标识。
6. 代码优化建议
-
模式验证增强:
- 可以增加更多的模式验证逻辑,确保选择的模式适合当前设备状态
- 可以提供模式兼容性检查,确保选择的模式与设备硬件兼容
-
错误处理增强:
- 可以增加更多的错误类型判断,提高错误处理的精确性
- 可以实现重试机制,对于临时性错误进行自动重试
-
模式选择反馈:
- 可以增加模式选择后的状态查询,确保模式选择成功并生效
- 可以提供模式切换的进度反馈,特别是对于需要重启的模式切换
-
配置化:
- 可以将模式映射配置化,便于根据不同的设备类型和固件版本进行调整
- 可以将超时时间等参数配置化,便于根据不同的环境进行调整
-
测试覆盖:
- 增加单元测试,覆盖更多的场景
- 实现集成测试,验证整个模式选择流程
7. 总结
本文详细分析了基于 gRPC 实现的模式选择客户端,包括其核心功能、代码结构和执行流程。该客户端实现了完整的模式选择功能,允许用户通过 gRPC 接口选择设备的启动模式。
通过使用 gRPC 的同步调用功能,该客户端能够高效地与服务器进行通信,同时通过完善的错误处理和日志记录机制,确保了模式选择过程的可靠性。
该实现展示了如何使用 gRPC 构建一个可靠的模式选择系统,对于类似的场景具有参考价值。同时,通过本文提出的优化建议,可以进一步提高系统的性能和可靠性。
8. 输入输出示例
输入输出示例
输入:
import logging
import grpc
from test_case.test_mode_select import test_mode_select
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 创建 gRPC 通道
server_addr = "192.168.1.100:50051"
channel = grpc.insecure_channel(server_addr)
# 测试模式选择
mode = "BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL"
test_mode_select(channel, logger, mode=mode, dut_position=1)
# 关闭通道
channel.close()
输出:
2026-03-26 10:00:00,000 - INFO - Mode select client stub created successfully.
TEST: Select 'BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL'
2026-03-26 10:00:00,100 - INFO - Successfully selected mode 'BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL'
2026-03-26 10:00:00,100 - INFO - Result: OK

浙公网安备 33010602011771号