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 格式,极大地提升了可用性。

附录

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)    收藏  举报