Configuration management

本文将详细记录一次重构过程:将一个只能加载配置文件的 Python gRPC 客户端,升级为一个支持多种操作(如查询、设置、删除硬件和软件信息)的命令行接口(CLI)工具。

起点:一个简单的配置文件加载脚本

我们最初有一个名为 load_profile.py 的脚本。它的目标很明确:读取一个 JSON 格式的配置文件,并通过 gRPC 将其发送到服务器。其核心逻辑大致如下:

# 旧版 load_profile.py 逻辑
def main():
    # 1. 解析命令行参数(JSON路径,服务器地址)
    # 2. 检查文件是否存在
    # 3. 创建一个 ProfileClient 实例
    # 4. 连接到服务器
    # 5. 调用 client.load_and_set_profile()
    # 6. 断开连接

这个脚本能很好地完成它的本职工作,但它的功能太单一了。为了进行其他操作,比如查询硬件信息或擦除数据,我们需要一个更灵活、功能更丰富的工具。

目标:扩展客户端功能

我们的目标是让客户端支持以下五种核心操作,使其与 C# 版本的功能对齐:

  1. create-config: 加载并设置一个完整的 TIB(Test Interface Board)配置文件。
  2. get-hw: 从服务器获取所有硬件信息。
  3. set-hw: 从 JSON 文件中读取硬件信息并将其设置到服务器。
  4. get-sw: 从服务器获取所有软件信息。
  5. erase-hw: 根据名称擦除指定的硬件信息。

为了实现这一目标,我们对核心的 profile_client.py 文件进行了两部分重大改造:ProfileClient 类中添加功能方法使用 argparse 构建多命令 CLI 入口

第一步:在 ProfileClient 中实现核心功能

所有与 gRPC 服务器的交互逻辑都封装在 ProfileClient 类中。我们为上述每个新功能都在这个类中添加了对应的方法。

代码实现

以下是我们添加到 profile_client.py 中的新方法:

# /mnt/c/Users/ezhuzix/repo_o/nib_application/grpc/profile_client.py

class ProfileClient:
    # ... (已有的 connect, disconnect, load_and_set_profile 等方法) ...

    def get_hw_infos(self):
        """封装 GetHWInfos gRPC 调用"""
        if not self.config_stub:
            self.logger.error("Not connected. Call connect() first.")
            return None
        
        timestamp = Timestamp()
        timestamp.FromSeconds(int(time.time()))
        req = pb2.GetHWInfoRequest(
            session=type_pb2.SessionId(
                device_id=uuid.uuid4().hex,
                start_time=timestamp
            ),
            dut_position=1,
            hw_name_mask=""
        )
        return self.config_stub.GetHWInfos(req, timeout=10)

    def set_hw_infos(self, hw_info_list):
        """封装 SetHWInfo gRPC 调用"""
        # ... 类似地构建 SetHWInfoRequest 并发送 ...

    def get_sw_infos(self):
        """封装 GetSWInfos gRPC 调用"""
        # ... 类似地构建 GetSWInfoRequest 并发送 ...

    def erase_hw_info(self, hw_name):
        """封装 EraseHwInfo gRPC 调用"""
        if not self.config_stub:
            self.logger.error("Not connected. Call connect() first.")
            return None
            
        timestamp = Timestamp()
        timestamp.FromSeconds(int(time.time()))
        req = pb2.EraseHWInfoRequest(
            session=type_pb2.SessionId(
                device_id=uuid.uuid4().hex,
                start_time=timestamp
            ),
            dut_position=1,
            hw_name_to_erase=hw_name
        )
        return self.config_stub.EraseHwInfo(req, timeout=10)

代码解析:一次关于 Protobuf 的调试之旅

在实现这些功能时,我们遇到了一个典型的 Protobuf 使用问题。最初,我们尝试使用 pb2.Session(id=...) 来创建会话标识符,但这导致了 AttributeErrorValueError

  1. AttributeError: module 'test_interface_pb2' has no attribute 'Session': 这个错误告诉我们 Session 消息并不在 test_interface_pb2 模块中。通过检查 .proto 源文件,我们发现它实际上名为 SessionId,并且定义在 type.proto 文件中。
  2. ValueError: Protocol message SessionId has no "id" field.: 修正了模块后,我们遇到了新问题。代码尝试为 SessionId 设置一个 id 字段,但 .proto 定义显示,它需要的字段是 device_idstart_time
// 在 type.proto 中 SessionId 的真实定义
message SessionId {
    string device_id = 1;
    google.protobuf.Timestamp start_time = 2;
}

最终的解决方案 是导入正确的模块 (type_pb2) 和 Timestamp 消息,并使用正确的字段名来构建请求。这个调试过程提醒我们,在使用 Protobuf 时,.proto 文件是最终的“真相来源”。

第二步:使用 argparse 构建多命令 CLI

ProfileClient 具备了所有能力之后,我们需要一个用户友好的方式来调用这些能力。Python 内置的 argparse 库是构建命令行工具的完美选择。我们重写了 load_profile.py 的主函数入口,将其变成一个功能丰富的 CLI 分发器。

代码实现

# /mnt/c/Users/ezhuzix/repo_o/nib_application/grpc/load_profile.py

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="TestInterface Configuration CLI Tool")
    parser.add_argument("--server", default="192.168.2.61:50050", help="Server address 'host:port'")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # 为每个功能创建一个子命令
    p_create = subparsers.add_parser("create-config", help="...")
    p_create.add_argument("json_path", help="...")

    p_get_hw = subparsers.add_parser("get-hw", help="...")
    p_get_hw.add_argument("--output", help="...")
    
    # ... (为 set-hw, get-sw, erase-hw 定义其他子命令) ...

    args = parser.parse_args()

    client = ProfileClient(server_address=args.server)
    if not client.connect():
        sys.exit(1)

    try:
        # 根据用户输入的命令,调用对应的方法
        if args.command == "create-config":
            # ... 调用 client.load_and_set_profile() ...
        elif args.command == "get-hw":
            hw_infos = client.get_hw_infos()
            if hw_infos:
                # 将 Protobuf 响应转换为 JSON 并打印
                hw_list = json_format.MessageToDict(hw_infos).get("hwInfo", [])
                output_json = json.dumps(hw_list, indent=2)
                print(output_json)
        elif args.command == "erase-hw":
            response = client.erase_hw_info(args.hw_name)
            if response and response.code == 0:
                client.logger.info(f"Successfully erased...")
            else:
                client.logger.error(f"Failed to erase: {response.message}")
        # ... (处理其他命令) ...
    finally:
        client.disconnect()

代码解析

  • subparsers = parser.add_subparsers(...): 这是 argparse 的核心功能,它允许我们创建多个独立的子命令,每个子命令都可以有自己的一套参数。
  • dest="command": 这个参数非常重要,它告诉 argparse 将用户选择的子命令名称(如 "get-hw")存储在 args.command 属性中。
  • 逻辑分发: 主 try 块中的 if/elif 链条根据 args.command 的值,精确地调用 ProfileClient 中对应的方法,并将命令行参数(如 args.hw_name)传递给它。
  • 优雅的输出: 对于 get-hwget-sw 命令,我们使用了 google.protobuf.json_format 将服务器返回的 Protobuf 对象转换成易于阅读和处理的 JSON 格式,极大地提升了可用性。

load_and_set_profile

功能概述

load_and_set_profile 函数的核心作用是:从一个指定的 JSON 配置文件中读取数据,将其转换成一个符合特定协议格式(Protocol Buffers)的 SetProfileRequest 请求,然后将这个请求发送给一个远程配置服务。

可以把它理解为一个桥梁,连接了本地的配置文件和远程的配置服务。

代码详解

让我们一步步来看这个函数是如何实现上述功能的:

  1. 检查服务连接

    if not self.config_stub:
        self.logger.error("Not connected. Call connect() first.")
        return
    

    函数首先会检查 self.config_stub 是否存在。stub 通常在 gRPC 或类似的 RPC 框架中表示客户端与远程服务的连接。如果这个连接不存在,意味着客户端尚未连接到服务,函数会记录一条错误日志并立即返回,不再执行后续操作。

  2. 解析配置文件

    profile_group, profile_base_name = self._parse_profile_file(file_path)
    if profile_group is None:
        return
    

    它调用内部辅助函数 _parse_profile_file 来读取和解析用户传入的 file_path 所指向的 JSON 文件。这个辅助函数会返回解析出的配置数据 profile_group 和一个基础名称 profile_base_name。如果解析失败(例如,文件不存在或格式错误),profile_group 会是 None,函数也会提前退出。

  3. 构建请求对象

    request = pb2.SetProfileRequest(dut_position=self.dut_position)
    request.session.CopyFrom(_create_session())
    

    这里,它创建了一个 SetProfileRequest 对象。pb2 的命名习惯暗示这很可能是一个由 Protocol Buffers (Protobuf) 编译器生成的类。这个请求对象是最终要发送给远程服务的数据包。它被初始化时传入了 dut_position(被测设备的位置),并且通过 _create_session() 辅助函数创建并附加了会话信息,用于身份验证或追踪。

  4. 遍历并填充配置详情

    for hw_target, details in profile_group.items():
        # ...
        profile = request.profiles.add()
        profile.name = profile_base_name
        profile.hw_target = hw_target
        self._build_profile(profile, details)
    

    这是函数的核心循环。它遍历从 JSON 文件中解析出的 profile_group 字典。

    • 对于字典中的每一项(hw_target 是硬件目标的名称,details 是其对应的配置详情),它会在 request 对象的 profiles 列表中添加一个新的 profile 条目。
    • 然后,它设置这个 profilenamehw_target 字段。
    • 最后,它调用另一个关键的辅助函数 _build_profile,将 details 字典中的具体配置项填充到 profile 对象中。这一步是将通用的字典数据结构转换为 Protobuf 特定格式的关键。
  5. 最终检查与发送

    if not request.profiles:
        self.logger.error("No profiles built from file.")
        return
    
    self._send_set_profile(request)
    

    在循环结束后,它会检查 request.profiles 列表是否为空。如果为空,意味着虽然文件被成功解析,但没有生成任何有效的配置。此时会记录错误并返回。
    如果一切顺利,它会调用 _send_set_profile 方法,将精心构建好的 request 对象通过网络发送到远程服务。

总而言之,load_and_set_profile 函数是一个高级别的接口方法,它封装了“读取文件 -> 解析数据 -> 构建请求 -> 发送请求”这一系列复杂的步骤,为使用者提供了一个简单的方式来通过文件更新远程服务的配置。

_build_profile

功能概述

_build_profile 函数是一个核心的“转换器”或“填充器”。它的主要职责是将从 JSON 配置文件中解析出的、以 Python 字典形式存在的配置详情(details),转换并填充到一个结构化的 Protocol Buffers (Protobuf) 消息对象(profile)中

可以把它想象成一个数据映射引擎:它读取一个灵活的、人类可读的数据结构(字典),然后根据预设的规则,将其内容填入一个严格定义的、用于机器间通信的数据结构(Protobuf 消息)中。

代码详解

这个函数通过遍历 details 字典中不同的配置“节”(section)来逐步构建 profile 对象。让我们逐节来看:

  1. 别名映射节 (Alias Mapping Sections)

    • 这部分代码处理的是一些具有通用结构的配置,比如别名映射。
    • 它通过一个名为 _ALIAS_MAP_SECTIONS 的常量来驱动。这个常量定义了 JSON 中的键名(json_key)如何对应到 Protobuf 消息中的特定字段路径(proto_path)。
    • _get_nested_repeated(profile, proto_path): 这个辅助函数会根据字符串路径(如 'io_map.io_maps')在 profile 对象中找到目标字段。
    • repeated.add(): 因为这些字段是列表(在 Protobuf 中称为 repeated),它会为 JSON 中的每一项在 Protobuf 列表中创建一个新条目。
    • 最后,它将 AliasActualNameState 等值从 JSON 项复制到新创建的 Protobuf 条目中。
  2. IO 配置 (IOConfigurationInitialSetting)

    • 这一节专门处理 IOConfigurationInitialSetting
    • 它遍历 JSON 中的每个 IO 配置项,并在 profile.io_configuration.data 列表中添加一个新条目。
    • self._set_enum_field(...): 这是一个非常关键的辅助函数调用。JSON 中通常使用字符串来表示枚举值(例如 "OUTPUT"),而 Protobuf 在内部使用整数来表示。_set_enum_field 的作用就是将这个字符串安全地转换为对应的整数枚举值。
  3. UART 配置 (UartConfigurationInitialSetting)

    • 与 IO 配置类似,它处理 UartConfigurationInitialSetting
    • 它会填充端口号(port_number)等基本信息。
    • 它也大量使用 _set_enum_field 来转换波特率、数据位、停止位等多个枚举类型的字段。
  4. 电压轨配置 (VoltageRailConfigurationInitialSetting)

    • 这是一个比较简单的部分。它处理 VoltageRailConfigurationInitialSetting
    • 它将电压值从 JSON 中读取出来,并使用 float() 确保其为浮点数类型后,再赋给 Protobuf 字段。
  5. 启动模式配置 (BootModeConfigurations)

    • 这一节处理 BootModeConfigurations
    • 它首先设置启动模式本身(同样使用 _set_enum_field)。
    • 然后,它有一个嵌套循环,用于处理与该启动模式关联的多个 IOSettings,将每个 IO 的状态(state)也设置好。
  6. 软件区域配置 (SoftwareAreaConfigurations)

    • 最后,它处理 SoftwareAreaConfigurations,用于定义内存区域。
    • 除了使用 _set_enum_field 设置软件类型外,这里有一个重要的细节:
    • int(..., 16): 它在转换地址和区域大小时,使用了 int() 函数的第二个参数 16。这表示它期望 JSON 文件中的地址(如 StartAddress, AreaSize 等)是以十六进制字符串(例如 "0x8000")的形式提供的,并会将它们正确地转换为整数。

总结

_build_profile 是连接配置文件和网络请求的关键桥梁。它体现了在与强类型系统(如 gRPC/Protobuf)交互时的一个常见模式:

  1. 解析与验证:从一个灵活的数据源(JSON)读取数据。
  2. 映射与转换:逐个字段地将数据映射到目标数据结构。
  3. 类型安全:在映射过程中执行严格的类型转换,包括字符串到枚举、字符串到数字(特别是十六进制)、字符串到浮点数等,确保最终发送的数据是完全符合接口协议定义的。

这个函数通过将复杂的转换逻辑封装在一起,使得上层调用者(load_and_set_profile)可以保持简洁,只需关心更高层次的流程。

附录

profile_client.py
import os
import sys
import grpc
import logging
import json
import re
import datetime
# from google.protobuf import text_format
from google.protobuf import json_format
from google.protobuf.timestamp_pb2 import Timestamp  # ADDED

# add the path to the generated proto files
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(current_dir, 'proto_python'))

# Now we can import the generated files
try:
    import test_interface_pb2 as pb2
    import service_ms_test_interface_pb2_grpc as pb2_grpc
    import common_enums_pb2
    import type_pb2
except ImportError:
    print(f"Error: Could not import the generated protobuf files.")
    sys.exit(1)

class ProfileClient:
    """
    A client for managing device profiles via gRPC.
    """
    def __init__(self, server_address='localhost:50051'):
        self.server_address = server_address
        self.channel = None
        self.mmi_stub = None
        self.config_stub = None
        self.logger = logging.getLogger(self.__class__.__name__)
        logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    def connect(self):
        """Establishes a connection to the gRPC server."""
        try:
            self.channel = grpc.insecure_channel(self.server_address)
            self.mmi_stub = pb2_grpc.TestInterfaceMMIStub(self.channel)
            self.config_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
            self.logger.info(f"Successfully connected to {self.server_address}")
            return True
        except grpc.RpcError as e:
            self.logger.error(f"Failed to connect: {e}")
            return False

    def disconnect(self):
        """Closes the gRPC channel."""
        if self.channel:
            self.channel.close()
            self.logger.info("Disconnected from the server.")

    def _pascal_to_snake(self, name):
        # Converts a PascalCase or camelCase string to UPPER_SNAKE_CASE.
        # e.g., "EnterDutBootModeInternal" -> "ENTER_DUT_BOOT_MODE_INTERNAL"
        name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).upper()

    def _get_proto_enum_string(self, json_value, enum_type_name, enum_keys):
        """
        Constructs the full protobuf enum string from a JSON value by mapping naming conventions.
        Handles special cases for numeric values and voltage strings.
        """
        # Special mappings for enum types that need custom conversion
        special_maps = {
            "UartBaudrate": {
                "115200": "BAUD_115200",
                "57600": "BAUD_57600",
                "9600": "BAUD_9600",
                # add more as needed
            },
            "UartDatabits": {
                "8": "EIGHT",
                "7": "SEVEN",
            },
            "UartStopbits": {
                "1": "ONE",
                "2": "TWO",
            },
            "IOVoltageLevel": {
                "V3P3": "V_3P3",
                "V1P8": "V_1P8",
                "V9P0": "V_9P0",
            },
            "SoftwareType": {  # ADDED for software type mapping
                "pboot": "PBOOT",
                "uboot": "UBOOT",
                "ubootenv": "UBOOT_ENV",
                "boardparameters": "BOARD_PARAMETERS",
                "faap": "FAAP",
                "InitialFlashImage": "INITIAL_FLASH_IMAGE",
                "SlotContentTable": "SLOT_CONTENT_TABLE",
                "Sboot": "SBOOT",
                "XcsConfig": "XCS_CONFIG",
                "Productionparameters": "PRODUCTION_PARAMETERS",
            }
        }

        if not isinstance(json_value, str) or not json_value:
            # Default to UNSPECIFIED
            snake_enum_type = self._pascal_to_snake(enum_type_name)  # Convert enum type name to snake
            unspecified = f"{snake_enum_type}_UNSPECIFIED"
            self.logger.warning(f"No JSON value for {enum_type_name}, defaulting to {unspecified}")
            return unspecified

        # Check if a special mapping exists
        if enum_type_name in special_maps and json_value in special_maps[enum_type_name]:
            snake_json_value = special_maps[enum_type_name][json_value]
        else:
            # Convert the JSON value to snake case
            if json_value.isupper() or json_value.islower():
                snake_json_value = json_value.upper()
            else:
                snake_json_value = self._pascal_to_snake(json_value)

        # Construct the full enum name
        # Convert enum type name to snake case (e.g., "UartBaudrate" -> "UART_BAUDRATE")
        if enum_type_name.startswith("Uart"):
            snake_enum_type = "UART_" + enum_type_name[4:].upper()
        else:
            snake_enum_type = self._pascal_to_snake(enum_type_name)

        full_name = f"{snake_enum_type}_{snake_json_value}"

        # Verify the constructed name exists in the enum keys
        if full_name in enum_keys:
            return full_name

        # Fallback: try without the type prefix (for cases where the value already includes it)
        if snake_json_value in enum_keys:
            return snake_json_value

        # Try to find a key that ends with the snake_json_value
        for key in enum_keys:
            if key.endswith(f"_{snake_json_value}"):
                return key

        self.logger.warning(f"Could not map JSON value '{json_value}' to an enum in {enum_type_name}. Defaulting to {snake_enum_type}_UNSPECIFIED")
        return f"{snake_enum_type}_UNSPECIFIED"

    def load_and_set_profile(self, file_path, dut_position=1):
        """Loads all hardware profiles from a JSON file and sends them to the server."""
        if not self.mmi_stub or not self.config_stub:
            self.logger.error("Not connected. Call connect() first.")
            return

        self.logger.info(f"Loading all profiles from {file_path}")
        try:
            with open(file_path, 'r') as f:
                content = re.sub(r"//.*", "", f.read())
            full_config = json.loads(content)
        except (FileNotFoundError, json.JSONDecodeError) as e:
            self.logger.error(f"Failed to read or parse profile file: {e}")
            return

        # Correctly navigate the nested JSON structure
        test_interface_config = full_config.get('TestInterfaceConfiguration')
        if not test_interface_config:
            self.logger.error("No 'TestInterfaceConfiguration' found in the JSON file.")
            return

        # The profile group (e.g., 'AIR1672') is the first value in the TestInterfaceConfiguration dict
        profile_group = next(iter(test_interface_config.values()), None)
        if not profile_group:
            self.logger.error("No profile group (e.g., 'AIR1672') found inside 'TestInterfaceConfiguration'.")
            return
        
        # The base name is the key of the profile group
        profile_base_name = next(iter(test_interface_config.keys()), "UnknownProfile")

        profile_request = pb2.SetProfileRequest(dut_position=dut_position)

        # ADDED: Create and set session
        now = datetime.datetime.now()
        timestamp = Timestamp()
        timestamp.FromDatetime(now)
        profile_request.session.device_id = "MMI"
        profile_request.session.start_time.CopyFrom(timestamp)

        # profile_group is now a dict like {'TIB2': {...}, 'TIB3': {...}}
        for hw_target, profile_details in profile_group.items():
            if not isinstance(profile_details, dict):
                self.logger.warning(f"Skipping item '{hw_target}' as it's not a valid profile object.")
                continue

            self.logger.info(f"Building profile for HW target '{hw_target}'")
            one_profile = profile_request.profiles.add()

            one_profile.name = profile_base_name
            one_profile.hw_target = hw_target

            # --- IO Map (aliases) ---
            if 'IOConfigurations' in profile_details:
                for config in profile_details['IOConfigurations']:
                    io_map = one_profile.io_map.io_maps.add()
                    io_map.alias = config.get('Alias', '')
                    io_map.actual_name = config.get('ActualName', '')
                    # state is included only if True (default False will be omitted in text format)
                    if config.get('State', False):
                        io_map.state = True

            # --- UART Map (aliases) ---
            if 'UartConfigurations' in profile_details:
                for config in profile_details['UartConfigurations']:
                    uart_map = one_profile.uart_map.uart_maps.add()
                    uart_map.alias = config.get('Alias', '')
                    uart_map.actual_name = config.get('ActualName', '')

            # --- Voltage Rail Map (aliases) ---
            if 'VoltageRailConfigurations' in profile_details:
                for config in profile_details['VoltageRailConfigurations']:
                    vr_map = one_profile.voltage_rail_map.voltage_rail_maps.add()
                    vr_map.alias = config.get('Alias', '')
                    vr_map.actual_name = config.get('ActualName', '')

            # --- Fan Map (aliases) ---
            if 'FanConfigurations' in profile_details:
                for config in profile_details['FanConfigurations']:
                    fan_map = one_profile.fan_map.fan_maps.add()
                    fan_map.alias = config.get('Alias', '')
                    fan_map.actual_name = config.get('ActualName', '')

            # --- Trigger Map (aliases) ---  ADDED
            if 'TriggerConfigurations' in profile_details:
                for config in profile_details['TriggerConfigurations']:
                    trigger_map = one_profile.trigger_map.trigger_maps.add()
                    trigger_map.alias = config.get('Alias', '')
                    trigger_map.actual_name = config.get('ActualName', '')

            # --- External Alarm Map (aliases) ---  ADDED
            if 'ExternalAlarmConfigurations' in profile_details:
                for config in profile_details['ExternalAlarmConfigurations']:
                    ea_map = one_profile.external_alarm_map.external_alarm_maps.add()
                    ea_map.alias = config.get('Alias', '')
                    ea_map.actual_name = config.get('ActualName', '')

            # --- IO Configuration (initial settings) ---
            if 'IOConfigurationInitialSetting' in profile_details:
                for config in profile_details['IOConfigurationInitialSetting']:
                    io_config = one_profile.io_configuration.data.add()
                    io_config.name = config.get('Name', '')
                    # IoDirection
                    dir_str = config.get('IoDirection')
                    if dir_str:
                        io_config.io_direction = common_enums_pb2.IODirection.Value(
                            self._get_proto_enum_string(dir_str, "IoDirection", common_enums_pb2.IODirection.keys())
                        )
                    # IoVoltageLevel
                    volt_str = config.get('IoVoltageLevel')
                    if volt_str:
                        io_config.io_voltage_level = common_enums_pb2.IOVoltageLevel.Value(
                            self._get_proto_enum_string(volt_str, "IOVoltageLevel", common_enums_pb2.IOVoltageLevel.keys())
                        )

            # --- UART Configuration (initial settings) ---
            if 'UartConfigurationInitialSetting' in profile_details:
                for config in profile_details['UartConfigurationInitialSetting']:
                    uart_config = one_profile.uart_configuration.data.add()
                    uart_config.name = config.get('Name', '')
                    uart_config.port_number = config.get('PortNumber', 0)
                    # Baudrate
                    baud_str = config.get('Baudrate')
                    if baud_str:
                        uart_config.baudrate = common_enums_pb2.UartBaudrate.Value(
                            self._get_proto_enum_string(baud_str, "UartBaudrate", common_enums_pb2.UartBaudrate.keys())
                        )
                    # Parity
                    parity_str = config.get('Parity')
                    if parity_str:
                        uart_config.parity = common_enums_pb2.UartParity.Value(
                            self._get_proto_enum_string(parity_str, "UartParity", common_enums_pb2.UartParity.keys())
                        )
                    # Databits
                    databits_str = config.get('Databits')
                    if databits_str:
                        uart_config.databits = common_enums_pb2.UartDatabits.Value(
                            self._get_proto_enum_string(databits_str, "UartDatabits", common_enums_pb2.UartDatabits.keys())
                        )
                    # Stopbits
                    stopbits_str = config.get('Stopbits')
                    if stopbits_str:
                        uart_config.stopbits = common_enums_pb2.UartStopbits.Value(
                            self._get_proto_enum_string(stopbits_str, "UartStopbits", common_enums_pb2.UartStopbits.keys())
                        )
                    # Hardware Interface (using UartHardwareInterface)
                    hw_if_str = config.get('HardwareInterface')
                    if hw_if_str:
                        uart_config.hardware_interface = common_enums_pb2.UartHardwareInterface.Value(
                            self._get_proto_enum_string(hw_if_str, "UartHardwareInterface", common_enums_pb2.UartHardwareInterface.keys())
                        )
                    # Voltage Level (optional, for some UARTs)
                    volt_str = config.get('VoltageLevel')
                    if volt_str:
                        uart_config.voltage_level = common_enums_pb2.IOVoltageLevel.Value(
                            self._get_proto_enum_string(volt_str, "IOVoltageLevel", common_enums_pb2.IOVoltageLevel.keys())
                        )

            # --- Voltage Rail Configuration (initial settings) ---  MODIFIED (was VoltageRegulatorInitialSetting)
            if 'VoltageRailConfigurationInitialSetting' in profile_details:
                for config in profile_details['VoltageRailConfigurationInitialSetting']:
                    vr_config = one_profile.voltage_rail_configuration.data.add()
                    vr_config.name = config.get('Name', '')
                    vr_config.voltage = float(config.get('Voltage', 0.0))

            # --- Boot Mode Configurations ---
            if 'BootModeConfigurations' in profile_details:
                for config in profile_details['BootModeConfigurations']:
                    boot_mode = one_profile.boot_mode_config.boot_mode_configs.add()
                    mode_str = config.get('Mode')
                    if mode_str:
                        boot_mode.mode = common_enums_pb2.BootModeSelection.Value(
                            self._get_proto_enum_string(mode_str, "BootModeSelection", common_enums_pb2.BootModeSelection.keys())
                        )
                    if 'IOSettings' in config:
                        for io_setting in config['IOSettings']:
                            io_setting_msg = boot_mode.io_settings.add()
                            io_setting_msg.io_name = io_setting.get('IoName', '')
                            # state is included only if True
                            if io_setting.get('State', False):
                                io_setting_msg.state = True

            # --- Software Area Configurations ---  MODIFIED (now uses _get_proto_enum_string)
            if 'SoftwareAreaConfigurations' in profile_details:
                for config in profile_details['SoftwareAreaConfigurations']:
                    sw_area = one_profile.software_repos.software_area_configurations.add()
                    sw_type_str = config.get('SwType')
                    if sw_type_str:
                        sw_area.sw_type = common_enums_pb2.SoftwareType.Value(
                            self._get_proto_enum_string(sw_type_str, "SoftwareType", common_enums_pb2.SoftwareType.keys())
                        )
                    # Addresses are hex strings, convert to int
                    sw_area.start_address = int(config.get('StartAddress', '0'), 16)
                    sw_area.area_size = int(config.get('AreaSize', '0'), 16)
                    sw_area.erase_start_address = int(config.get('EraseStartAddress', '0'), 16)
                    sw_area.erase_area_size = int(config.get('EraseAreaSize', '0'), 16)
                    # File info (if present)
                    if 'File' in config:
                        file_info = config['File']
                        sw_area.file.name = file_info.get('Name', '')
                        sw_area.file.description = file_info.get('Description', '')
                        sw_area.file.version = file_info.get('Version', '')
                        sw_area.file.path = file_info.get('Path', '')

        if not profile_request.profiles:
            self.logger.error("Failed to build any profiles from the file.")
            return
            
        self.logger.info("Sending SetProfile request...")
        self.logger.debug(f"Request payload: {profile_request}")

        try:
            response = self.config_stub.SetProfile(profile_request)
            self.logger.info(f"Request content: {json_format.MessageToJson(profile_request)}")
            if response.code == 0:
                self.logger.info("Successfully set profile.")
                self.logger.info(f"Server response: {response}")
            else:
                self.logger.error(f"Failed to set profile. Response: {response}")
        except grpc.RpcError as e:
            self.logger.error(f"An RPC error occurred: {e.details()} (code: {e.code().name})")

    def get_hw_infos(self):
        """Wrapper for GetHWInfos gRPC call."""
        if not self.config_stub:
            self.logger.error("Not connected. Call connect() first.")
            return None
        now = datetime.datetime.now()
        timestamp = Timestamp()
        timestamp.FromDatetime(now)
        req = pb2.GetHWInfoRequest(
            session=type_pb2.SessionId(
                device_id=uuid.uuid4().hex,
                start_time=timestamp
            ),
            dut_position=1,
            hw_name_mask=""
        )
        return self.config_stub.GetHWInfos(req, timeout=10)

    def set_hw_infos(self, hw_info_list):
        """Wrapper for SetHWInfo gRPC call for a list of HW info."""
        if not self.config_stub:
            self.logger.error("Not connected. Call connect() first.")
            return
        for hw_info_dict in hw_info_list:
            hw_info = pb2.HWInfo()
            json_format.ParseDict(hw_info_dict, hw_info)
            
            now = datetime.datetime.now()
            timestamp = Timestamp()
            timestamp.FromDatetime(now)
            req = pb2.SetHWInfoRequest(
                session=type_pb2.SessionId(
                    device_id=uuid.uuid4().hex,
                    start_time=timestamp
                ),
                dut_position=1,
                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: # Assuming 0 is OK
                self.logger.error(f"Failed to set HW Info for {hw_info.hw_name}: {resp.message}")

    def get_sw_infos(self):
        """Wrapper for GetSWInfos gRPC call."""
        if not self.config_stub:
            self.logger.error("Not connected. Call connect() first.")
            return None
        now = datetime.datetime.now()
        timestamp = Timestamp()
        timestamp.FromDatetime(now)
        req = pb2.GetSWInfoRequest(
            session=type_pb2.SessionId(
                device_id=uuid.uuid4().hex,
                start_time=timestamp
            ),
            dut_position=1
        )
        return self.config_stub.GetSWInfos(req, timeout=10)

    def erase_hw_info(self, hw_name):
        """Wrapper for EraseHwInfo gRPC call."""
        if not self.config_stub:
            self.logger.error("Not connected. Call connect() first.")
            return None
        now = datetime.datetime.now()
        timestamp = Timestamp()
        timestamp.FromDatetime(now)
        req = pb2.EraseHWInfoRequest(
            session=type_pb2.SessionId(
                device_id=uuid.uuid4().hex,
                start_time=timestamp
            ),
            dut_position=1,
            hw_name_to_erase=hw_name
        )
        return self.config_stub.EraseHwInfo(req, timeout=10)
config_main.py
import argparse
import sys
import os
import json
from google.protobuf import json_format
from profile_client import ProfileClient

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="TestInterface Configuration CLI Tool")
    parser.add_argument("--server", default="192.168.2.61:50050", help="Server address 'host:port'")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # create-config command
    p_create = subparsers.add_parser("create-config", help="Load a full TIB profile from a JSON file.")
    p_create.add_argument("json_path", help="Path to the JSON profile file.")

    # get-hw command
    p_get_hw = subparsers.add_parser("get-hw", help="Get all hardware information from the server.")
    p_get_hw.add_argument("--output", help="Path to save the output JSON file.")

    # set-hw command
    p_set_hw = subparsers.add_parser("set-hw", help="Set hardware information from a JSON file.")
    p_set_hw.add_argument("input", help="Path to the input JSON file containing hardware info.")

    # get-sw command
    p_get_sw = subparsers.add_parser("get-sw", help="Get all software information from the server.")
    p_get_sw.add_argument("--output", help="Path to save the output JSON file.")

    # erase-hw command
    p_erase_hw = subparsers.add_parser("erase-hw", help="Erase hardware information by name.")
    p_erase_hw.add_argument("hw_name", help="The hardware name to erase.")

    args = parser.parse_args()

    client = ProfileClient(server_address=args.server)
    if not client.connect():
        sys.exit(1)

    try:
        if args.command == "create-config":
            profile_full_path = os.path.abspath(args.json_path)
            if not os.path.exists(profile_full_path):
                print(f"Error: Profile file not found at '{profile_full_path}'")
            else:
                client.load_and_set_profile(profile_full_path)

        elif args.command == "get-hw":
            hw_infos = client.get_hw_infos()
            if hw_infos:
                hw_list = json_format.MessageToDict(hw_infos).get("hwInfo", [])
                output_json = json.dumps(hw_list, indent=2)
                if args.output:
                    with open(args.output, "w") as f:
                        f.write(output_json)
                    client.logger.info(f"Hardware info saved to {args.output}")
                else:
                    print(output_json)

        elif args.command == "set-hw":
            try:
                with open(args.input, "r") as f:
                    hw_data = json.load(f)
                client.set_hw_infos(hw_data)
                client.logger.info(f"Successfully sent hardware info from {args.input}")
            except FileNotFoundError:
                client.logger.error(f"Input file not found: {args.input}")
            except json.JSONDecodeError:
                client.logger.error(f"Could not decode JSON from {args.input}")

        elif args.command == "get-sw":
            sw_infos = client.get_sw_infos()
            if sw_infos:
                sw_list = json_format.MessageToDict(sw_infos).get("swInfo", [])
                output_json = json.dumps(sw_list, indent=2)
                if args.output:
                    with open(args.output, "w") as f:
                        f.write(output_json)
                    client.logger.info(f"Software info saved to {args.output}")
                else:
                    print(output_json)

        elif args.command == "erase-hw":
            response = client.erase_hw_info(args.hw_name)
            if response and response.code == 0: # Assuming 0 is OK
                client.logger.info(f"Successfully erased hardware info for '{args.hw_name}'.")
            elif response:
                client.logger.error(f"Failed to erase hardware info for '{args.hw_name}': {response.message}")

    finally:
        client.disconnect()

结论

通过Python类和命令行工具,实现了配置文件的批量下发、硬件/软件信息的查询与修改。新增的硬件/软件信息操作接口使得客户端功能更加完善,能够满足日常测试中对设备配置的动态管理需求。

posted @ 2026-03-12 14:15  mo686  阅读(3)  评论(0)    收藏  举报