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# 版本的功能对齐:
create-config: 加载并设置一个完整的 TIB(Test Interface Board)配置文件。get-hw: 从服务器获取所有硬件信息。set-hw: 从 JSON 文件中读取硬件信息并将其设置到服务器。get-sw: 从服务器获取所有软件信息。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=...) 来创建会话标识符,但这导致了 AttributeError 和 ValueError。
AttributeError: module 'test_interface_pb2' has no attribute 'Session': 这个错误告诉我们Session消息并不在test_interface_pb2模块中。通过检查.proto源文件,我们发现它实际上名为SessionId,并且定义在type.proto文件中。ValueError: Protocol message SessionId has no "id" field.: 修正了模块后,我们遇到了新问题。代码尝试为SessionId设置一个id字段,但.proto定义显示,它需要的字段是device_id和start_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-hw和get-sw命令,我们使用了google.protobuf.json_format将服务器返回的 Protobuf 对象转换成易于阅读和处理的 JSON 格式,极大地提升了可用性。
load_and_set_profile。
功能概述
load_and_set_profile 函数的核心作用是:从一个指定的 JSON 配置文件中读取数据,将其转换成一个符合特定协议格式(Protocol Buffers)的 SetProfileRequest 请求,然后将这个请求发送给一个远程配置服务。
可以把它理解为一个桥梁,连接了本地的配置文件和远程的配置服务。
代码详解
让我们一步步来看这个函数是如何实现上述功能的:
-
检查服务连接
if not self.config_stub: self.logger.error("Not connected. Call connect() first.") return函数首先会检查
self.config_stub是否存在。stub通常在 gRPC 或类似的 RPC 框架中表示客户端与远程服务的连接。如果这个连接不存在,意味着客户端尚未连接到服务,函数会记录一条错误日志并立即返回,不再执行后续操作。 -
解析配置文件
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,函数也会提前退出。 -
构建请求对象
request = pb2.SetProfileRequest(dut_position=self.dut_position) request.session.CopyFrom(_create_session())这里,它创建了一个
SetProfileRequest对象。pb2的命名习惯暗示这很可能是一个由 Protocol Buffers (Protobuf) 编译器生成的类。这个请求对象是最终要发送给远程服务的数据包。它被初始化时传入了dut_position(被测设备的位置),并且通过_create_session()辅助函数创建并附加了会话信息,用于身份验证或追踪。 -
遍历并填充配置详情
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条目。 - 然后,它设置这个
profile的name和hw_target字段。 - 最后,它调用另一个关键的辅助函数
_build_profile,将details字典中的具体配置项填充到profile对象中。这一步是将通用的字典数据结构转换为 Protobuf 特定格式的关键。
- 对于字典中的每一项(
-
最终检查与发送
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 对象。让我们逐节来看:
-
别名映射节 (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 列表中创建一个新条目。- 最后,它将
Alias、ActualName和State等值从 JSON 项复制到新创建的 Protobuf 条目中。
-
IO 配置 (IOConfigurationInitialSetting)
- 这一节专门处理
IOConfigurationInitialSetting。 - 它遍历 JSON 中的每个 IO 配置项,并在
profile.io_configuration.data列表中添加一个新条目。 self._set_enum_field(...): 这是一个非常关键的辅助函数调用。JSON 中通常使用字符串来表示枚举值(例如"OUTPUT"),而 Protobuf 在内部使用整数来表示。_set_enum_field的作用就是将这个字符串安全地转换为对应的整数枚举值。
- 这一节专门处理
-
UART 配置 (UartConfigurationInitialSetting)
- 与 IO 配置类似,它处理
UartConfigurationInitialSetting。 - 它会填充端口号(
port_number)等基本信息。 - 它也大量使用
_set_enum_field来转换波特率、数据位、停止位等多个枚举类型的字段。
- 与 IO 配置类似,它处理
-
电压轨配置 (VoltageRailConfigurationInitialSetting)
- 这是一个比较简单的部分。它处理
VoltageRailConfigurationInitialSetting。 - 它将电压值从 JSON 中读取出来,并使用
float()确保其为浮点数类型后,再赋给 Protobuf 字段。
- 这是一个比较简单的部分。它处理
-
启动模式配置 (BootModeConfigurations)
- 这一节处理
BootModeConfigurations。 - 它首先设置启动模式本身(同样使用
_set_enum_field)。 - 然后,它有一个嵌套循环,用于处理与该启动模式关联的多个
IOSettings,将每个 IO 的状态(state)也设置好。
- 这一节处理
-
软件区域配置 (SoftwareAreaConfigurations)
- 最后,它处理
SoftwareAreaConfigurations,用于定义内存区域。 - 除了使用
_set_enum_field设置软件类型外,这里有一个重要的细节: int(..., 16): 它在转换地址和区域大小时,使用了int()函数的第二个参数16。这表示它期望 JSON 文件中的地址(如StartAddress,AreaSize等)是以十六进制字符串(例如"0x8000")的形式提供的,并会将它们正确地转换为整数。
- 最后,它处理
总结
_build_profile 是连接配置文件和网络请求的关键桥梁。它体现了在与强类型系统(如 gRPC/Protobuf)交互时的一个常见模式:
- 解析与验证:从一个灵活的数据源(JSON)读取数据。
- 映射与转换:逐个字段地将数据映射到目标数据结构。
- 类型安全:在映射过程中执行严格的类型转换,包括字符串到枚举、字符串到数字(特别是十六进制)、字符串到浮点数等,确保最终发送的数据是完全符合接口协议定义的。
这个函数通过将复杂的转换逻辑封装在一起,使得上层调用者(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类和命令行工具,实现了配置文件的批量下发、硬件/软件信息的查询与修改。新增的硬件/软件信息操作接口使得客户端功能更加完善,能够满足日常测试中对设备配置的动态管理需求。

浙公网安备 33010602011771号