gRPC Profile

背景:
硬件测试平台需要把 JSON 格式的 TIB2/TIB3 配置通过 gRPC 下发到服务端。


1. 需求一句话

解析json文件,填充message,发送request,打印message


2. 文件地图

文件 作用
profile_client.py 核心逻辑:读 JSON → 构造 SetProfileRequest → 发 gRPC
load_profile.py CLI 入口,调 profile_client
proto_python/*_pb2.py protoc 生成的消息/枚举定义,只读
Raptor2.*.json 真实嵌套配置(含注释)
test 期望的 gRPC 文本格式,用于 diff 验证

3. JSON → Protobuf 的「万能翻译器」

核心函数 _get_proto_enum_string 做了 4 层回退:

回退层级 举例 输出
① 特殊字典 "115200"BAUD_115200 UART_BAUDRATE_BAUD_115200
② 全大/小写 "NONE"NONE UART_PARITY_NONE
③ 驼峰转蛇形 "EnterDutBootModeInternal"ENTER_DUT_BOOT_MODE_INTERNAL BOOT_MODE_SELECTION_ENTER_DUT_BOOT_MODE_INTERNAL
④ 后缀模糊 "TX" 命中 UART_PARITY_TX 直接返回

代码片段(已折叠重复):

def _get_proto_enum_string(self, json_value, enum_type_name, enum_keys):
    special_maps = {
        "UartBaudrate": {"115200": "BAUD_115200", ...},
        "IOVoltageLevel": {"V3P3": "V_3P3"},
        ...
    }
    ...
    # 1. 特殊映射
    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:
        # 2. 通用驼峰→蛇形
        snake_json_value = self._pascal_to_snake(json_value)

    # 3. 类型前缀特殊处理:UartDataBits → UART_DATABITS
    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}"
    if full_name in enum_keys:          # 4. 第一次命中
        return full_name
    if snake_json_value in enum_keys:   # 5. 无前缀再试
        return snake_json_value
    for k in enum_keys:                 # 6. 后缀模糊
        if k.endswith(f"_{snake_json_value}"):
            return k
    return f"{snake_enum_type}_UNSPECIFIED"

4. 嵌套 JSON 导航

真实文件结构:

{
  "TestInterfaceConfiguration": {
    "AIR1672": {          // ← profile_group
      "TIB2": { ... },    // ← hw_target
      "TIB3": { ... }
    }
  }
}

代码用 next(iter(...)) 一把拿到 AIR1672,再 for hw_target, profile_details in profile_group.items() 遍历,再也不用手动指定 hw_target


5. Protobuf 重复字段的正确姿势

错误写法 正确写法
one_profile.boot_modes.add() one_profile.boot_mode_config.boot_mode_configs.add()
uart_config = one_profile.uart_configuration uart_config = one_profile.uart_configuration.data.add()

踩坑:一开始把 boot_mode_config 当成普通字段,结果 AttributeError: boot_modes;用 dir() 现场侦查才发现 boot_mode_config 本身就是 BootModeConfigurations(repeated)。


6. 最终 CLI 体验

python load_profile.py \
  --profile_path Raptor2.TestInterface.AIR1672.Configuration.json \
  --server localhost:50051

日志:

INFO  Building profile for HW target 'TIB2'
INFO  Building profile for HW target 'TIB3'
INFO  Successfully set profile.

服务端收到完整请求,与 test 文件 diff 零差异


7. 小结

  • 枚举映射用 4 层回退,再也不怕 JSON 写法不统一。
  • 嵌套结构next(iter()) 自动导航,新增硬件目标零代码改动。
  • repeated 字段记得 .add(),现场 dir() 比盲猜快 10 倍。
  • 注释 JSONre.sub(r"//.*", "", ...)json.loads,优雅兼容带注释的配置。

profile_client.py 中,load_and_set_profile 是整个客户端的核心 —— 它将一个包含多个硬件目标配置的 JSON 文件,转化为符合 protobuf 定义的 SetProfileRequest,并通过 gRPC 发送给服务端。本文将逐行拆解这个方法,深入理解其设计思想和技术细节。

前置辅助函数

在深入主方法之前,有必要了解两个关键的辅助函数:

  • _pascal_to_snake:将驼峰命名(如 "EnterDutBootModeInternal")转换为全大写下划线命名(如 "ENTER_DUT_BOOT_MODE_INTERNAL"),用于构造枚举名称的后半部分。
  • _get_proto_enum_string:将 JSON 中的字符串值(如 "115200""V3P3""Input")映射为 protobuf 枚举的完整字符串名称(如 "UART_BAUDRATE_BAUD_115200""IO_VOLTAGE_LEVEL_V_3P3""IO_DIRECTION_INPUT")。它通过内置的特殊映射表和通用驼峰转换,确保无论 JSON 中是什么写法,最终都能找到正确的枚举值。

这两个函数是整个转换过程的基石,保证了代码的灵活性和健壮性。

load_and_set_profile

1. 函数签名与文档

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."""
  • 参数
    • file_path:JSON 配置文件的路径(可以是绝对或相对路径)。
    • dut_position:机台位置编号,默认为 1,用于多 DUT 场景。
  • 功能:一次性读取文件中所有硬件目标的配置,打包成一个 SetProfileRequest 并发送。

2. 连接状态检查

if not self.mmi_stub or not self.config_stub:
    self.logger.error("Not connected. Call connect() first.")
    return
  • 防御式编程:如果 gRPC 存根(stub)不存在,说明尚未建立连接,直接报错退出,避免后续空指针异常。

3. 读取文件并去除注释

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
  • re.sub(r"//.*", "", ...):使用正则表达式删除 JSON 中的单行注释(以 // 开头的内容)。标准库 json 不允许注释,所以必须先清理。
  • 异常处理:分别捕获文件不存在和 JSON 解析错误,并记录详细日志,便于排查。

4. 定位 JSON 嵌套结构

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

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

profile_base_name = next(iter(test_interface_config.keys()), "UnknownProfile")
  • JSON 的典型结构为:
    {
      "TestInterfaceConfiguration": {
        "AIR1672": {
          "TIB2": { ... },
          "TCPE": { ... },
          "TIB3": { ... }
        }
      }
    }
    
  • test_interface_config 指向 {"AIR1672": {...}} 这一层。
  • profile_group 取第一个值(即 AIR1672 对象),它包含了所有硬件目标的配置字典。
  • profile_base_name 取第一个键(即 "AIR1672"),后续所有 OneProfilename 字段都统一使用这个值。

5. 构建请求外壳并填充会话

profile_request = pb2.SetProfileRequest(dut_position=dut_position)

now = datetime.datetime.utcnow()
timestamp = Timestamp()
timestamp.FromDatetime(now)
profile_request.session.device_id = "MMI"
profile_request.session.start_time.CopyFrom(timestamp)
  • SetProfileRequest 是 gRPC 请求的根消息,包含 dut_positionsessionprofiles 列表。
  • 会话信息device_id 固定为 "MMI"start_time 使用当前 UTC 时间,通过 CopyFrom 赋值(protobuf 时间戳消息的固定用法)。

6. 遍历每个硬件目标

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
  • profile_group.items() 遍历 "TIB2""TCPE" 等键及其对应的配置字典。
  • 如果某个值不是字典(例如格式错误),则跳过并记录警告。

7. 创建并填充 OneProfile

one_profile = profile_request.profiles.add()
one_profile.name = profile_base_name
one_profile.hw_target = hw_target
  • profiles.add() 在 repeated 字段中新增一个 OneProfile 消息,返回该消息对象。
  • 设置通用字段:name 固定为 "AIR1672"hw_target 为当前硬件目标名称(如 "TIB2")。

8. 处理 IO 别名映射

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', '')
        if config.get('State', False):
            io_map.state = True
  • io_mapsrepeated IOMapping 字段,通过 add() 添加新元素。
  • 只有 Statetrue 时才显式设置 state 字段;为 false 或缺失时,protobuf 默认值为 false,且文本格式不会输出该字段,保持请求简洁。

9. 其他映射部分(UART、电压轨、风扇、触发器、外部报警)

这些部分的处理模式与 IO 映射完全一致,只是字段名不同:

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', '')

if 'VoltageRailConfigurations' in profile_details:
    ...  # 类似

if 'FanConfigurations' in profile_details:
    ...  # 类似

if 'TriggerConfigurations' in profile_details:
    ...  # 新增,映射到 trigger_map.trigger_maps

if 'ExternalAlarmConfigurations' in profile_details:
    ...  # 新增,映射到 external_alarm_map.external_alarm_maps

10. IO 初始配置

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())
            )
  • io_configuration.datarepeated IOConfiguration 字段。
  • 枚举字段通过 _get_proto_enum_string 转换为字符串,再用 Value() 获取对应的整数值。
  • 如果 JSON 中缺少某个字段,则不设置,protobuf 会使用默认值(通常为 UNSPECIFIED)。

11. UART 初始配置

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, Databits, Stopbits, HardwareInterface, VoltageLevel 类似
  • 每个 UART 配置项都包含多个枚举字段(波特率、奇偶校验、数据位等),全部通过 _get_proto_enum_string 统一处理。
  • port_number 是整数,直接从 JSON 获取。
  • voltage_level 复用 IOVoltageLevel 枚举,体现了 protobuf 的复用设计。

12. 电压轨初始配置

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))
  • voltagedouble 类型,JSON 中可能是字符串,需要显式转换为 float

13. 启动模式配置

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', '')
                if io_setting.get('State', False):
                    io_setting_msg.state = True
  • 每个启动模式可以包含多个 IO 设置,形成两层嵌套。
  • io_settingsstate 同样遵循“只写 true”的原则。

14. 软件区域配置

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())
            )
        # 地址为十六进制字符串,转换为整数
        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)
        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', '')
  • 软件区域涉及十六进制地址,使用 int(..., 16) 转换为整数,符合 protobuf 的 uint32 字段。
  • 可选的 File 子消息同样通过嵌套填充。

15. 空 profile 保护

if not profile_request.profiles:
    self.logger.error("Failed to build any profiles from the file.")
    return
  • 如果循环结束后一个有效的 OneProfile 都没有添加,则报错返回,避免发送空请求。

16. 发送请求并处理响应

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})")
  • config_stub.SetProfile 是同步 gRPC 调用,会阻塞直到收到响应或超时。
  • 关键日志:使用 json_format.MessageToJson 将整个请求转换为 JSON 字符串,便于调试和验证。这与用户要求的日志格式完全一致(驼峰字段名、枚举值字符串化)。
  • 响应中的 code 为 0 表示成功,非零表示业务层错误。
  • 捕获 grpc.RpcError 处理网络层异常(如连接失败、超时等)。

总结

load_and_set_profile 方法是一个功能强大的“JSON→gRPC 全自动翻译器”。它通过以下步骤保证了可靠性和灵活性:

  1. 读取与预处理:清理 JSON 注释,安全解析。
  2. 导航与遍历:定位到硬件目标层级,逐一处理。
  3. 动态枚举映射:借助 _get_proto_enum_string,将任意格式的 JSON 值转换为正确的 protobuf 枚举。
  4. 精准填充:对 repeated 字段使用 .add(),对可选字段采用 get() 默认值,对布尔值仅当 true 时才设置。
  5. 完备的错误处理:从文件读取到 gRPC 调用,每一层都有异常捕获和详细日志。
  6. 符合预期的输出:最终发送的请求结构与 protobuf 定义完全一致,且日志输出为清晰的 JSON 格式,便于验证。

理解这段代码,不仅能掌握 gRPC 客户端的开发模式,还能学习到如何将非结构化的配置文件安全地转换为强类型的 protobuf 消息,是工业级自动化测试系统的典范实现。

现在,TIB3 真的有了。

附件

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
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})")
profile_main.py
import argparse
import sys
import os
from profile_client import ProfileClient

def main():
    """
    Main function to load a profile from a JSON file and send it to the gRPC server.
    """
    if len(sys.argv) != 3:
        print("Usage: python3 load_profile.py <profile_json_path> <server_address>")
        sys.exit(1)

    profile_path = sys.argv[1]
    server_address = sys.argv[2]

    profile_full_path = os.path.abspath(profile_path)

    if not os.path.exists(profile_full_path):
        print(f"Error: Profile file not found at '{profile_full_path}'")
        return

    client = ProfileClient(server_address=server_address)

    if client.connect():
        client.load_and_set_profile(profile_full_path)
        client.disconnect()

if __name__ == '__main__':
    main()
posted @ 2026-03-05 15:50  mo686  阅读(3)  评论(0)    收藏  举报