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:TestInterfaceFirmwareUpdateServiceStubTestInterfaceConfigurationServiceStub
  • 使用 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
  • 分两部分发送数据:
    1. 首先发送软件项信息(文件名、大小、哈希等)
    2. 然后分块发送固件文件数据
  • 每发送 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 错误情况,特别是 UNAVAILABLEDEADLINE_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

技术要点

  • 实现了完整的固件更新流程,包括三个主要步骤:
    1. 检查原始固件版本
    2. 上传新固件
    3. 等待设备重启并验证更新结果
  • 记录了整个过程的时间消耗
  • 比较了更新前后的固件版本(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 整体流程

  1. 初始化阶段

    • 创建 FirmwareUpdateClient 实例
    • 调用 connect() 方法连接到服务器
    • 测试连接是否成功
  2. 固件更新阶段

    • 调用 firmware_upgrade() 方法开始更新流程
    • 检查原始固件版本
    • 上传新固件(使用流式 RPC)
    • 等待设备重启并重新连接
    • 检查更新后的固件版本
  3. 结束阶段

    • 关闭 gRPC 连接
    • 返回更新结果

4.2 详细流程

  1. 连接服务器

    • 创建 gRPC 通道
    • 初始化两个 stub
    • 测试通道是否就绪
    • 测试固件服务连接
  2. 上传固件

    • 计算固件文件的 MD5 哈希值
    • 创建会话
    • 发送软件项信息
    • 分块发送固件数据
    • 显示上传进度
    • 处理服务器响应
  3. 等待设备重启

    • 等待 20 秒(设备重启时间)
    • 轮询设备状态
    • 当设备重新上线时,获取新的固件版本信息
    • 比较更新前后的固件版本

5. 技术亮点

  1. 流式 RPC 实现:使用 gRPC 的流式 RPC 功能,实现了大文件的分块上传,避免了单次请求过大的问题。

  2. 错误处理:实现了完善的错误处理机制,包括连接重试、异常捕获等,提高了代码的健壮性。

  3. 状态管理:实现了设备重启后的状态检测和轮询机制,确保设备能够正确完成固件更新并重新上线。

  4. 性能优化

    • 配置了合理的 gRPC 选项,如最大消息长度、心跳设置等
    • 使用分块上传,减少了内存占用
    • 实现了进度显示,提高了用户体验
  5. 代码结构:代码结构清晰,职责分明,易于理解和维护。

6. 代码优化建议

  1. 日志改进

    • 可以使用结构化日志,便于后续的日志分析
    • 增加更详细的错误信息,便于问题定位
  2. 错误处理增强

    • 可以增加更多的错误类型判断,提高错误处理的精确性
    • 实现更细粒度的重试机制,针对不同的错误类型采取不同的重试策略
  3. 性能优化

    • 可以根据网络状况动态调整分块大小
    • 实现并行上传,提高上传速度
  4. 可配置性

    • 将更多的参数(如超时时间、重试次数等)配置化,便于根据不同的环境进行调整
  5. 测试覆盖

    • 增加单元测试,覆盖更多的场景
    • 实现集成测试,验证整个固件更新流程

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:TestInterfaceMMIStubTestInterfaceConfigurationServiceStub
  • 实现了简单的错误处理,捕获 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()

技术要点

  • 实现了一个完整的测试流程,包括:
    1. 设置配置文件
    2. 获取硬件信息
    3. 设置硬件信息
    4. 验证硬件信息设置
    5. 擦除硬件信息
    6. 获取软件信息
  • 提供了默认的测试硬件信息
  • 包含了异常处理,确保测试过程中的错误能够被捕获和记录
  • 使用 finally 块确保客户端能够正确断开连接

4. 代码执行流程分析

4.1 整体流程

  1. 初始化阶段

    • 创建 ConfigurationClient 实例
    • 调用 connect() 方法连接到服务器
    • 准备测试数据
  2. 配置管理阶段

    • 加载并设置配置文件
    • 执行硬件信息的增删改查操作
    • 查询软件信息
  3. 结束阶段

    • 断开与服务器的连接
    • 记录测试结果

4.2 详细流程

  1. 配置文件加载与设置

    • 读取 JSON 配置文件
    • 解析配置文件结构
    • 为每个硬件目标构建配置
    • 发送 SetProfile 请求
  2. 硬件信息管理

    • 获取当前硬件信息
    • 设置新的硬件信息
    • 验证硬件信息设置
    • 擦除硬件信息
  3. 软件信息查询

    • 发送 GetSWInfos 请求
    • 处理并显示软件信息

5. 技术亮点

  1. 配置文件解析:实现了从 JSON 配置文件到 protobuf 消息的自动转换,支持复杂的配置结构。

  2. 枚举值映射:实现了灵活的枚举值解析和映射机制,支持不同的命名约定和特殊映射。

  3. 错误处理:实现了完善的错误处理机制,包括 gRPC 错误捕获和记录。

  4. 测试覆盖:实现了全面的测试流程,覆盖了配置管理的各个方面。

  5. 代码结构:代码结构清晰,职责分明,易于理解和维护。

6. 代码优化建议

  1. 日志改进

    • 可以使用结构化日志,便于后续的日志分析
    • 增加更详细的错误信息,便于问题定位
  2. 错误处理增强

    • 可以增加更多的错误类型判断,提高错误处理的精确性
    • 实现更细粒度的重试机制,针对不同的错误类型采取不同的重试策略
  3. 性能优化

    • 对于大量硬件信息的设置,可以考虑批量操作,减少 RPC 调用次数
    • 可以使用异步 gRPC 调用,提高并发性能
  4. 可配置性

    • 将更多的参数(如超时时间、重试次数等)配置化,便于根据不同的环境进行调整
  5. 测试覆盖

    • 增加单元测试,覆盖更多的场景
    • 实现集成测试,验证整个配置管理流程

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
  • 实现了复杂的通信协议:
    1. 发送所有软件项信息
    2. 发送软件项完成通知
    3. 等待服务器请求上传或准备完成
    4. 当服务器请求时上传文件内容
  • 使用队列管理请求流,实现异步处理
  • 分块上传文件,支持大文件传输
  • 提供详细的进度报告和错误处理

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 整体流程

  1. 初始化阶段

    • 创建 SoftwareRepositoryClient 实例
    • 连接到服务器
    • 准备 DZ 容器加载配置
  2. DZ 容器解析阶段

    • 解析 manifest.yaml 文件
    • 解析 flashmap.json 文件
    • 提取软件区域配置和兼容产品信息
  3. 配置更新阶段(可选):

    • 更新配置文件中的软件区域配置
    • 发送 SetProfile 请求到服务器
  4. 文件解析与准备阶段

    • 为 MTD 目标解析文件路径
    • 处理额外的软件文件
    • 验证软件区域与文件的对应关系
  5. 文件上传阶段

    • 发送软件项信息
    • 等待服务器请求
    • 分块上传文件内容
    • 处理服务器响应
  6. 清理阶段

    • 执行启动后的清理操作
    • 记录测试结果

4.2 详细流程

  1. DZ 容器解析

    • 读取并解析 manifest.yaml 文件
    • 提取兼容产品列表
    • 读取并解析 flashmap.json 文件
    • 转换软件类型名称
    • 提取软件区域配置
  2. 配置更新

    • 读取配置文件
    • 更新软件区域配置
    • 写回配置文件
    • 发送 SetProfile 请求
  3. 文件解析与准备

    • 为每个 MTD 目标解析文件路径
    • 处理安全级别
    • 匹配软件类型与区域配置
    • 处理额外的软件文件
    • 验证所有软件区域是否都有对应的文件
  4. 文件上传

    • 准备软件项信息
    • 发送软件项信息
    • 发送软件项完成通知
    • 等待服务器请求
    • 分块上传文件内容
    • 处理服务器响应
    • 确认上传结果
  5. 清理操作

    • 发送 CleanupAfterBoot 请求
    • 处理响应结果

5. 技术亮点

  1. DZ 容器解析:实现了对 DZ 容器的完整解析,包括 manifest.yaml 和 flashmap.json 文件的处理。

  2. 软件类型映射:实现了灵活的软件类型映射机制,支持不同命名约定之间的转换。

  3. gRPC 双向流式通信:使用 gRPC 的双向流式 RPC 功能,实现了复杂的通信协议。

  4. 文件分块上传:实现了大文件的分块上传,支持进度报告。

  5. 错误处理:实现了完善的错误处理机制,包括文件解析错误、gRPC 错误等。

  6. 配置文件更新:支持自动更新配置文件中的软件区域配置。

  7. 测试覆盖:实现了全面的测试流程,覆盖了 DZ 容器加载的各个方面。

6. 代码优化建议

  1. 日志改进

    • 可以使用结构化日志,便于后续的日志分析
    • 增加更详细的错误信息,便于问题定位
  2. 错误处理增强

    • 可以增加更多的错误类型判断,提高错误处理的精确性
    • 实现更细粒度的重试机制,针对不同的错误类型采取不同的重试策略
  3. 性能优化

    • 对于大文件的上传,可以考虑使用并行上传,提高上传速度
    • 可以使用异步 gRPC 调用,提高并发性能
  4. 可配置性

    • 将更多的参数(如超时时间、重试次数等)配置化,便于根据不同的环境进行调整
  5. 测试覆盖

    • 增加单元测试,覆盖更多的场景
    • 实现集成测试,验证整个 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
  • 测试三个主要功能:
    1. 设置输出状态
    2. 读取输入状态
    3. 设置安全默认状态
  • 提供详细的错误处理和日志记录
  • 支持通过配置字典自定义测试用例

4. 代码执行流程分析

4.1 整体流程

  1. 初始化阶段

    • 创建 GPIOClient 实例
    • 调用 connect() 方法连接到服务器
    • 准备测试数据(如果是测试流程)
  2. GPIO 操作阶段

    • 设置 GPIO 输出状态
    • 读取 GPIO 输入状态
    • 设置安全默认状态(可选)
  3. 结束阶段

    • 记录操作结果
    • 处理异常情况

4.2 详细流程

  1. 连接服务器

    • 创建 gRPC 通道
    • 初始化 GPIO 服务 stub
    • 验证连接是否成功
  2. 设置 GPIO 输出

    • 创建 IORequest 请求
    • 发送 SetOutput 请求
    • 处理响应结果
  3. 读取 GPIO 输入

    • 创建 IORequest 请求
    • 发送 GetInput 请求
    • 处理响应结果,返回 GPIO 状态
  4. 设置安全默认状态

    • 创建 IOSafeDefaultsRequest 请求
    • 发送 SetSafeDefaults 请求
    • 处理响应结果

5. 技术亮点

  1. 简洁的 API 设计:提供了简单直观的方法来控制 GPIO,包括设置输出、读取输入和设置安全默认状态。

  2. 完善的错误处理:实现了全面的错误处理机制,包括连接错误、gRPC 错误和响应错误。

  3. 详细的日志记录:在每个操作中都提供了详细的日志记录,便于调试和监控。

  4. 灵活的配置:通过配置字典可以灵活地指定要测试的 GPIO 输出和输入。

  5. 类型提示:使用了 Python 的类型提示,提高了代码的可读性和可维护性。

  6. 会话管理:实现了会话创建和管理,确保每个请求都有唯一的会话标识。

6. 代码优化建议

  1. 超时时间配置

    • 可以将超时时间配置化,便于根据不同的环境进行调整
    • 可以为不同的操作设置不同的超时时间
  2. 错误处理增强

    • 可以增加更多的错误类型判断,提高错误处理的精确性
    • 可以实现重试机制,对于临时性错误进行自动重试
  3. 批量操作

    • 可以实现批量设置多个 GPIO 输出的功能,减少 gRPC 调用次数
    • 可以实现批量读取多个 GPIO 输入的功能,提高效率
  4. 状态缓存

    • 可以实现 GPIO 状态缓存,减少不必要的 gRPC 调用
    • 可以提供状态变化的回调机制,及时通知状态变化
  5. 测试覆盖

    • 增加单元测试,覆盖更多的场景
    • 实现集成测试,验证整个 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 整体流程

  1. 初始化阶段

    • 创建 ModeSelectClient 实例
    • 调用 connect() 方法连接到服务器
    • 准备测试数据(如果是测试流程)
  2. 模式选择阶段

    • 验证模式字符串是否有效
    • 发送模式选择请求
    • 处理响应结果
  3. 结束阶段

    • 记录操作结果
    • 处理异常情况

4.2 详细流程

  1. 连接服务器

    • 创建 gRPC 通道
    • 初始化模式选择服务 stub
    • 验证连接是否成功
  2. 选择模式

    • 验证客户端是否已连接
    • 验证模式字符串是否有效
    • 创建 ModeSelectRequest 请求
    • 发送 Select 请求
    • 处理响应结果
  3. 测试流程

    • 创建 ModeSelectClient 实例
    • 连接到服务器
    • 调用 select 方法选择指定的模式
    • 记录测试结果
    • 处理异常情况

5. 技术亮点

  1. 简洁的 API 设计:提供了简单直观的方法来选择启动模式,使用字符串表示模式名称,便于用户使用。

  2. 完善的错误处理:实现了全面的错误处理机制,包括连接错误、模式无效错误、gRPC 错误和响应错误。

  3. 详细的日志记录:在每个操作中都提供了详细的日志记录,便于调试和监控。

  4. 模式映射:实现了模式字符串到枚举值的映射,提高了代码的可读性和可维护性。

  5. 类型提示:使用了 Python 的类型提示,提高了代码的可读性和可维护性。

  6. 会话管理:实现了会话创建和管理,确保每个请求都有唯一的会话标识。

6. 代码优化建议

  1. 模式验证增强

    • 可以增加更多的模式验证逻辑,确保选择的模式适合当前设备状态
    • 可以提供模式兼容性检查,确保选择的模式与设备硬件兼容
  2. 错误处理增强

    • 可以增加更多的错误类型判断,提高错误处理的精确性
    • 可以实现重试机制,对于临时性错误进行自动重试
  3. 模式选择反馈

    • 可以增加模式选择后的状态查询,确保模式选择成功并生效
    • 可以提供模式切换的进度反馈,特别是对于需要重启的模式切换
  4. 配置化

    • 可以将模式映射配置化,便于根据不同的设备类型和固件版本进行调整
    • 可以将超时时间等参数配置化,便于根据不同的环境进行调整
  5. 测试覆盖

    • 增加单元测试,覆盖更多的场景
    • 实现集成测试,验证整个模式选择流程

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
posted @ 2026-03-26 17:07  mo686  阅读(12)  评论(0)    收藏  举报