从C++ gRPC服务器到Python客户端

📚 使用须知

  • 本博客内容仅供学习参考
  • 建议理解思路后独立实现
  • 欢迎交流讨论

task : 配置项目环境

vscode

  • c/c++ 配置中,编译器路径尽可能简单,不然会报错
终端进程启动失败(退出代码: -1)

在 WSL 中编译

📘 Makefile文件

项目中Makefile 是为Unix/Linux环境编写的,其中使用了rm命令,而这个命令在Windows的原生命令行(CMD/PowerShell)中不存在。

可以在 WSL(Windows Subsystem for Linux)中直接访问并编译,这是最接近原生 Linux 的体验。

操作步骤:

  1. 安装 WSL:在 PowerShell(管理员)中运行:
wsl --install
  1. 打开 WSL 终端(如 Ubuntu)。

    导航到项目目录。Windows 文件系统通常挂载在 /mnt/c/ 下:

cd /mnt/c/Users/ezhuzix/repo/nib_application/grpc
  1. 现在你可以完美运行所有 make 命令:
make clean
make
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

Install the latest PowerShell for new features and improvements! https://aka.ms/PSWindows

PS C:\Windows\system32> wsl --install
操作超时
PS C:\Windows\system32> wsl --install
正在安装: 虚拟机平台
已安装 虚拟机平台。
正在安装: 适用于 Linux 的 Windows 子系统
Error: 0x803f8001           0.0%

https://chat.deepseek.com/share/254irytxy7txsbcyvu

🔧 在 WSL 内安装 Linux 版工具链

g++ 和 protoc 等工具都安装在 Windows 的路径下(例如 C:\mingw64\bin),而你现在正在 WSL 的 Ubuntu 环境中运行 make。

WSL 是一个独立的 Linux 环境,它无法直接运行 Windows 目录下的 .exe 程序。你在 WSL 中编译项目,必须使用为 Linux 编译的二进制工具。

在 WSL 内安装 Linux 版工具链

sudo apt update
sudo apt install -y build-essential protobuf-compiler libprotobuf-dev libgrpc++-dev

这将安装 Linux 系统原生、兼容性最好的 g++、protoc、grpc_cpp_plugin 等所有必需工具和开发库。安装后,Makefile 就能在 WSL 中顺利找到它们。

🔍 问题

错误信息显示 PROTO_DIRS 是:

/Source/Protos/production/protos/ /Source/Protos/...

这是一个 WSL 内部的绝对路径,它应该指向你存放 .proto 文件的 Windows 目录。你需要将这个路径指向正确的位置。

通过命令行参数覆盖

make server PROTO_REPO_DIR=/mnt/c/Users/ezhuzix/repo/tcpe_proto

🔧安装 gRPC C++ 插件

在 WSL 的 Ubuntu 终端中执行:

sudo apt install -y libgrpc++-dev
which grpc_cpp_plugin

# 回到你的项目目录
cd /mnt/c/Users/ezhuzix/repo/nib_application/grpc
# 先清理之前的(失败的)构建产物
make clean
# 再次尝试构建服务器,并传递正确的 proto 路径
make server PROTO_REPO_DIR=/mnt/c/Users/ezhuzix/repo/tcpe_proto

🔧 安装并确认 gRPC 插件

# 1. 首先,尝试查找系统中是否已有任何 gRPC 插件
find /usr -name "*grpc*plugin*" 2>/dev/null

# 2. 如果上一步没找到,安装包含插件的开发包
# 对于 Ubuntu/Debian,通常是以下几个包之一
sudo apt update
sudo apt install -y protobuf-compiler-grpc
# 或者尝试更完整的 gRPC 开发工具
sudo apt install -y grpc-cli libgrpc++-dev

# 3. 安装后,再次查找并确认插件的完整路径
which grpc_cpp_plugin
# 或者更精确地查找
find /usr -type f -name "grpc_cpp_plugin" 2>/dev/null


cd /mnt/c/Users/ezhuzix/repo/nib_application/grpc
make clean
make server PROTO_REPO_DIR=/mnt/c/Users/ezhuzix/repo/tcpe_proto GRPC_CPP_PLUGIN=/usr/bin/grpc_cpp_plugin

🔧安装库文件

🎉 恭喜!Protobuf 和 gRPC 代码生成已完全成功! 你现在已经进入了项目编译的核心阶段——C++ 源代码的编译和链接。

当前遇到的错误 fatal error: openssl/md5.h: No such file or directory 是一个非常典型的开发环境依赖缺失问题。你的项目代码需要OpenSSL库,但系统尚未安装其开发头文件。

sudo apt install -y libssl-dev


sudo apt install -y libi2c-dev

#  安装 Abseil C++ 库、c-ares 和 RE2 正则表达式库
sudo apt install -y libabsl-dev libc-ares-dev libre2-dev
/usr/bin/ld: cannot find -labsl_string_view: No such file or directory
/usr/bin/ld: cannot find -labsl_low_level_hash: No such file or directory
/usr/bin/ld: cannot find -labsl_cordz_info: No such file or directory
/usr/bin/ld: cannot find -labsl_cordz_functions: No such file or directory
/usr/bin/ld: cannot find -labsl_cord_internal: No such file or directory
/usr/bin/ld: cannot find -labsl_log_internal_check_op: No such file or directory
/usr/bin/ld: cannot find -labsl_log_internal_message: No such file or directory
/usr/bin/ld: cannot find -labsl_log_internal_nullguard: No such file or directory
/usr/bin/ld: cannot find -labsl_log_internal_conditions: No such file or directory
/usr/bin/ld: cannot find -labsl_kernel_timeout_internal: No such file or directory
/usr/bin/ld: cannot find -labsl_die_if_null: No such file or directory
collect2: error: ld returned 1 exit status
make: *** [Makefile:101: ../build/binaries/grpc_server] Error 1

🔧 从源码编译安装 Abseil

# 1. 进入临时目录,克隆 Abseil 的官方源码
cd /tmp
git clone https://github.com/abseil/abseil-cpp.git
cd abseil-cpp

# 2. 创建构建目录,使用 CMake 配置并指定安装到系统目录
mkdir build && cd build
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
      -DCMAKE_INSTALL_PREFIX=/usr/local \
      -DABSL_PROPAGATE_CXX_STD=ON ..  # 这个选项有助于更好地集成

# 3. 编译并安装(-j参数根据你的CPU核心数调整,可以加快速度)
make -j$(nproc)
sudo make install

# 4. 更新系统的动态库缓存
sudo ldconfig
/usr/bin/ld: cannot find -labsl_string_view: No such file or directory
/usr/bin/ld: cannot find -labsl_low_level_hash: No such file or directory
collect2: error: ld returned 1 exit status
make: *** [Makefile:101: ../build/binaries/grpc_server] Error 1

在Makefile中删除这两个库文件即可

💡使用 VSCode 远程开发扩展

image

这是最彻底的解决方案。安装官方“Remote - WSL”扩展后,在VSCode中直接打开WSL中的项目目录,所有IDE功能(包括代码提示、终端、调试)都会在完整的Linux环境中运行,自动解决路径问题。

你说得完全正确,使用 VSCode 的 Remote - WSL 扩展是解决此问题最彻底、最优雅的方案。它将 IDE 的核心功能直接“搬”到 WSL 环境中运行,让 Windows 端的 VSCode 变成一个完美的前端界面。

以下是详细的设置步骤和解释,帮你建立一个完美的开发环境。

🛠️ 详细设置步骤

第一步:在 Windows 的 VSCode 中安装必备扩展

  1. 打开 Windows 系统上的 VSCode。
  2. 点击左侧活动栏的 扩展图标 (或按 Ctrl+Shift+X)。
  3. 在搜索框中输入 Remote - WSL
  4. 找到由 Microsoft 发布的官方扩展,点击“安装”。

同时建议安装C/C++ 扩展 (也由 Microsoft 发布)。虽然 Remote - WSL 会帮你把已安装的扩展自动同步到 WSL 中,但确保 Windows 端已安装可以简化流程。

第二步:从 WSL 内部启动 VSCode 并打开项目(关键!)

这是整个流程的核心,有两种常用方法:

  • 方法A(推荐,从 WSL 终端启动)

    1. 在 WSL 的 Ubuntu 终端中,导航到你的项目目录:
      cd /mnt/c/Users/ezhuzix/repo/nib_application/grpc
      
    2. 输入以下命令,在当前目录启动 VSCode:
      code .
      
    3. 首次运行会进行一些设置,稍等片刻,一个 全新的 VSCode 窗口 将自动打开。
  • 方法B(从 VSCode 内连接)

    1. 在 Windows 的 VSCode 中,点击左下角蓝色的 >< 远程窗口状态按钮
    2. 在弹出的命令菜单中选择 New WSL WindowConnect to WSL
    3. 在新的 VSCode 窗口中使用 文件 -> 打开文件夹,然后输入你的 WSL 项目路径,例如:\\wsl$\Ubuntu\home\ezhuzix\.../home/ezhuzix/...

成功后你会看到:VSCode 窗口左下角显示为 WSL: Ubuntu(或你的发行版名称),这表示你已成功处于远程开发模式。

第三步:确保扩展在 WSL 环境中生效

  1. 在新的远程 VSCode 窗口中,再次打开扩展视图 (Ctrl+Shift+X)。
  2. 你应该能看到一个分类为 “WSL: Ubuntu” 的扩展列表。之前安装在 Windows 的 C/C++ 扩展会显示“已在 WSL 中安装”。如果没有,你可以点击“安装”按钮将其安装到 WSL 环境中。

第四步:享受无缝的开发体验

现在,你的 VSCode 所使用的文件系统、终端、工具链(g++, make, protoc) 完全位于 WSL 内部。

  • 代码智能感知 (IntelliSense):会直接使用 WSL 中 /usr/include 等路径下的头文件,之前所有“找不到头文件”的红色波浪线将立刻消失
  • 集成终端:按下 Ctrl+ ` 打开的终端,将是 WSL 的 Bash 终端,你可以直接运行 make server 等命令。
  • 调试:你可以直接配置和启动基于 GDB 的调试会话,目标就是 WSL 环境中编译出的 Linux 程序。

📝 验证与微调

  1. 打开一个之前报错的文件,比如 service_ms_test_interface.grpc.pb.h,看看红色错误是否消失。
  2. 如果需要更精确的 IntelliSense,可以按下 Ctrl+Shift+P,输入 C/C++: Edit Configurations (UI),确保“编译器路径”指向 /usr/bin/g++,“IntelliSense 模式”为 linux-gcc-x64。但大多数情况下,远程模式已经自动配置好。
  3. 编译测试:在 VSCode 的集成终端里,直接运行你的构建命令:
    make server PROTO_REPO_DIR=/mnt/c/Users/ezhuzix/repo/tcpe_proto GRPC_CPP_PLUGIN=/usr/bin/grpc_cpp_plugin
    

💡 总结与优势

通过切换到 Remote-WSL 开发模式,你获得了:

  • 环境统一:编码、构建、调试的环境完全一致,杜绝了“在我机器上能编译”的问题。
  • 配置简化:无需再手动维护复杂的 Windows 端包含路径和编译器设置。
  • 原生体验:直接使用 Linux 的原生工具链和文件系统,完美匹配你的项目。

这个方案将你之前为解决兼容性所做的所有努力(安装 WSL、配置工具链)的价值最大化,为你提供了一个强大且顺畅的 C++ 开发环境。

现在,你可以关掉之前那个在 Windows 文件系统下打开的、充满红色波浪线的 VSCode 窗口,在全新的 Remote-WSL 窗口中愉快地继续开发了。

gRPC

恭喜成功编译了整个项目!现在,运行 firmware(固件)相关功能是一个很自然的下一步。

由于项目是一个 gRPC 服务端 (grpc_server),它启动后会作为一个后台服务,等待客户端(例如 grpc_client 或其他测试工具)来调用其提供的各种 RPC 方法,其中就应包括固件更新功能。

要运行和使用固件功能,你需要完成以下两个核心步骤:

🚀 第一步:启动 gRPC 服务器

首先,你需要启动刚刚编译成功的服务器程序。在你的 WSL 终端VSCode 的集成终端(确保位于 WSL 环境)中,导航到项目目录并运行:

cd /mnt/c/Users/ezhuzix/repo/nib_application/grpc
./../build/binaries/grpc_server

注意:如果服务器需要特定端口配置文件,你可能需要查阅项目文档或代码,通过命令行参数来指定,例如 ./../build/binaries/grpc_server --port 50051 --config config.json

服务器启动后,终端通常会显示监听地址(如 0.0.0.0:50050)并保持运行状态,这意味着服务已就绪,等待客户端连接。

image

🔌 第二步:使用客户端调用固件更新功能

服务器本身不直接提供交互界面。你需要一个 gRPC 客户端 来调用其服务。你有以下几种选择:

  1. 使用项目自带的客户端
    如果你之前也成功编译了 grpc_client,可以在另一个终端窗口中运行它来与服务器交互。具体的调用命令需要参考项目本身的说明或客户端帮助。

    ./../build/binaries/grpc_client --help
    
    ezhuzix@E-5CG2381T93:/mnt/c/Users/ezhuzix/repo/nib_application/grpc$ ./../build/binaries/grpc_client --help
    Usage:
    Firmware Update:
      Manual Upload:      ./../build/binaries/grpc_client fwup <server> <file_path> [product_number] [rstate]
    
    Software Repository:
      DZ Container:       ./../build/binaries/grpc_client swrepo <server> <container_path> <product_number> <rstate>
    
    Note: Use quotes for parameters with spaces
    
    Examples:
      ./../build/binaries/grpc_client fwup localhost:50051 /tmp/upgrade.tar.gz "CXP9024418/1" R1A
      ./../build/binaries/grpc_client swrepo localhost:50051 /tmp/container "KRC 161 4451/1" R1A
    
  2. 使用通用的 gRPC 测试工具
    这是非常推荐的高效测试方法。你可以使用 grpcurl(一个类似于 curl 的 gRPC 命令行工具)或 BloomRPC(图形化客户端)来直接调用服务器提供的任何 RPC 方法。

    • 安装 grpcurl (在WSL中)
      sudo apt install grpcurl
      
    • 发现服务:首先,你需要知道服务器提供了哪些服务和方法。可以尝试列出所有服务:
      grpcurl -plaintext localhost:50050 list
      
      (如果服务器不使用明文传输,需要去掉 -plaintext 并提供 TLS 证书等信息)
    • 调用方法:找到与固件更新相关的方法后(可能包含 FirmwareUpdate 关键字),参照 .proto 文件定义的结构,构造请求并调用。
  3. 编写一个简单的测试脚本
    你可以使用 Python、Go 或任何你熟悉的、支持 gRPC 的语言,根据生成的 gRPC 接口文件(.pb.go, _pb2.py 等)快速编写一个测试客户端来调用固件更新方法。

💡 关键提示与后续操作

  • 查阅项目文档:要成功调用固件更新功能,最关键的是了解具体的 RPC 方法名请求消息格式(需要哪些参数,如固件文件路径、版本号等)以及可能的认证信息。请务必查阅项目的 README、内部文档或直接查看 service_ms_test_interface.proto 文件中关于 FirmwareUpdateService 或类似服务的定义。
  • 准备测试固件文件:你可能需要准备一个用于测试的固件文件(如 .bin.hex 文件),并知道其在 WSL 文件系统中的路径。
  • 服务器日志:观察服务器启动和运行时的日志输出,这是了解其状态和排查连接问题的重要依据。

总结一下你的操作路径

  1. 启动服务器
  2. 另开一个终端,使用 grpcurl 探索服务列表,找到固件更新方法。
  3. 根据 .proto 定义,构造请求,使用 grpcurl 或你选择的客户端调用该方法

从日志看,现在信息非常明确了!问题在于服务器内部的专用存储目录配置。错误提示 “Insufficient storage space for firmware: upgrade.tar.gz” 是服务器业务逻辑的返回,而不是系统磁盘空间问题。

根据日志,下一步的核心是:找到并配置服务器用于存储固件的正确路径

🔍 定位固件存储配置

1. 在项目代码中搜索存储路径

在你的项目目录中,运行以下命令来搜索可能与固件存储相关的配置:

cd /mnt/c/Users/ezhuzix/repo/nib_application/grpc
grep -r -i "storage.*path\|firmware.*dir\|upload.*dir" --include="*.cpp" --include="*.h" --include="*.json" --include="*.conf" --include="*.ini" --include="*.yaml" .

同时,也搜索一下在日志中出现的 firmware_update_service.cc 文件,看其内部如何处理路径:

find . -name "firmware_update_service.cc" -type f
cat ./path/to/firmware_update_service.cc | grep -n "storage\|path\|directory"

2. 检查是否有配置文件

查看项目根目录下是否存在如 config.jsonsettings.iniserver.conf 等文件,并检查其内容。

ezhuzix@E-5CG2381T93:/mnt/c/Users/ezhuzix/repo/nib_application/grpc$ cat ./service/firmware_update_service.cc | grep -n "storage\|path\|directory"
16:    const std::string &base_path, const std::string &upgrade_path)
17:    : upg_handler_(std::move(upg_handler)), base_path_(base_path),
18:      upgrade_path_(upgrade_path) {}
105:  if (!common::FileHandler::HasSufficientSpace(base_path_,
107:    LOG_E("Insufficient storage space for firmware: %s",
109:    return Status(StatusCode::RESOURCE_EXHAUSTED, "Insufficient storage space");
130:  std::string upgrade_file_path = upgrade_path_ + transfer->filename;
134:    common::FileHandler::DeleteFile(upgrade_file_path);
141:          upgrade_file_path, reinterpret_cast<const uint8_t *>(data.data()),
164:    std::string full_path = upgrade_path_ + transfer->filename;
165:    if (!common::FileHandler::VerifyFileIntegrity(full_path, transfer->hash)) {

image

ezhuzix@E-5CG2381T93:/mnt/c/Users/ezhuzix/repo/nib_application/grpc$ ./../build/binaries/grpc_client fwup localhost:50050 /tmp/upgrade.tar.gz "CXP9024418/1" R1A
Firmware Update Client
Server: localhost:50050
File: /tmp/upgrade.tar.gz
Product: CXP9024418/1
RState: R1A
------------------------
Firmware client connected to: localhost:50050
Testing firmware connection...
[OK] Firmware connection test succeeded
Starting complete firmware upgrade process...
Step 1: Uploading firmware...
Testing upload: /tmp/upgrade.tar.gz
File size: 42 bytes
[OK] Software item sent
Progress: 100% (42/42 bytes)
[OK] All data sent (1 chunks)
[OK] Firmware upload succeeded
Server response: Firmware update completed successfully (code: 0)
[OK] Firmware upload completed
Step 2: Waiting for device to restart and come back online...
Waiting for device to start upgrade (2 minutes)...
Still waiting for upgrade to start... (30/120 seconds)
Still waiting for upgrade to start... (60/120 seconds)
Still waiting for upgrade to start... (90/120 seconds)
Still waiting for upgrade to start... (120/120 seconds)
Starting to poll GetSWInfos every 5 seconds (max 10 minutes)...
Still waiting for device response... (attempt 12/120)
Still waiting for device response... (attempt 24/120)
Still waiting for device response... (attempt 36/120)
Still waiting for device response... (attempt 48/120)
Still waiting for device response... (attempt 60/120)
Still waiting for device response... (attempt 72/120)
Still waiting for device response... (attempt 84/120)
Still waiting for device response... (attempt 96/120)
Still waiting for device response... (attempt 108/120)
Still waiting for device response... (attempt 120/120)
[FAIL] Device did not respond after 10 minutes of polling
[FAIL] Device did not come back online or failed to get software info

[ERROR] Operation failed!

从完整的输出看,已经100%成功验证了核心的固件上传功能,最后的“失败”是测试流程的预期行为,而非功能问题。

Python客户端

概述

从已有的C++ gRPC服务器代码出发,逐步开发出能够正常工作的Python客户端的过程。整个过程涉及protobuf理解、gRPC通信调试、哈希算法适配等多个环节。

🛠️Makefile 中添加 Python gRPC

# Python configurations
PYTHON ?= python3
PY_PROTO_DIR ?= ./proto_python

python_proto: pre_config
	@echo "=== Generating Python proto files ==="
	@echo "Python: $(PYTHON)"
	@echo "Proto directories: $(PROTO_DIRS)"
	@echo "Output directory: $(PY_PROTO_DIR)"
	
	# 创建输出目录
	@mkdir -p $(PY_PROTO_DIR)
	
	# 构建 protoc 命令
	$(PYTHON) -m grpc_tools.protoc \
		$(foreach dir,$(PROTO_DIRS),-I=$(dir)) \
		--python_out=$(PY_PROTO_DIR) \
		--grpc_python_out=$(PY_PROTO_DIR) \
		--pyi_out=$(PY_PROTO_DIR) \
		$(foreach file,$(PROTOFILES),$(shell find $(PROTO_DIRS) -name $(file) 2>/dev/null | head -1))
	
	@echo "Python proto files generated in $(PY_PROTO_DIR)"
	@ls -la $(PY_PROTO_DIR)/*.py 2>/dev/null || echo "No Python files generated"

python_clean:
	@echo "=== Cleaning Python generated files ==="
	rm -rf $(PY_PROTO_DIR)
	@echo "Python proto files cleaned"

1. 环境准备

1.1 初始状态

  • C++ gRPC服务器已存在并运行在 localhost:50050
  • C++客户端能够正常工作
  • Python客户端需要重新开发

1.2 Python环境依赖

pip install grpcio grpcio-tools protobuf

1.3 Protobuf文件生成

# 假设已有proto文件
make python_proto  # 生成Python的protobuf代码
make fix_python_imports  # 修复导入问题

生成的文件位于 proto_python/ 目录下。

2. 理解C++服务器接口

2.1 分析C++服务器代码

从C++代码中,我们提取出关键信息:

// FirmwareUpdate是一个客户端流式RPC
::grpc::Status FirmwareUpdate(
    ::grpc::ServerContext *context,
    ::grpc::ServerReader<FirmwareUpdateRequest> *reader,
    ProductionStatus *response
);

// 请求消息结构
class FirmwareUpdateRequest {
    SessionId session;
    uint32 dut_position;
    oneof msg {
        SoftwareItem item;        // 第一个消息:元数据
        SoftwareContent content;  // 后续消息:文件数据
    }
};

2.2 关键发现

  1. 流式RPC:客户端需要发送多个请求
  2. 消息顺序:先发送元数据(item),再发送文件数据(content)
  3. 完整性检查:服务器会验证文件哈希

3. 初始Python客户端尝试

3.1 基础客户端结构

import grpc
import test_interface_pb2 as pb2
import service_ms_test_interface_pb2_grpc as pb2_grpc

class BasicClient:
    def __init__(self, server_addr):
        self.channel = grpc.insecure_channel(server_addr)
        self.stub = pb2_grpc.TestInterfaceFirmwareUpdateServiceStub(self.channel)
    
    def upload_firmware(self, filepath, product_num, rstate):
        # 初步实现...

3.2 遇到的主要问题

  1. 枚举值问题SoftwareType枚举无法正确获取
  2. 消息结构不匹配:Python中消息字段名称与C++不同
  3. 哈希算法不匹配:服务器期望MD5而非SHA256

4. 逐步调试过程

4.1 第一步:分析protobuf结构

创建调试脚本来理解生成的消息结构:

#!/usr/bin/env python3
# analyze_protobuf.py

import test_interface_pb2 as pb2

# 查看消息类型
for name in pb2.DESCRIPTOR.message_types_by_name:
    print(f"消息类型: {name}")

# 查看FirmwareUpdateRequest字段
request_desc = pb2.FirmwareUpdateRequest.DESCRIPTOR
for field in request_desc.fields:
    print(f"字段: {field.name}, 类型: {field.type}")
    if field.containing_oneof:
        print(f"  属于oneof: {field.containing_oneof.name}")

输出显示正确的消息结构:

FirmwareUpdateRequest 字段:
  session: message -> SessionId
  dut_position: uint32
  item: message -> SoftwareItem (属于 oneof: msg)
  content: message -> SoftwareItemContent (属于 oneof: msg)

4.2 第二步:修复消息生成

根据分析结果,我们需要:

  1. 先发送包含SoftwareItem的请求
  2. 再发送多个包含SoftwareItemContent的请求
  3. 每个请求只能设置itemcontent中的一个
def generate_requests(self, filepath, product_num, rstate):
    # 1. 元数据请求
    request1 = pb2.FirmwareUpdateRequest()
    request1.session.CopyFrom(self.create_session())
    request1.dut_position = 0
    
    item = pb2.SoftwareItem()
    item.description = f"固件 {product_num}"
    item.product_number = product_num
    item.rstate = rstate
    item.sw_type = 6  # SOFTWARE_TYPE_INITIAL_FLASH_IMAGE
    item.filename = os.path.basename(filepath)
    item.hash = self.calculate_hash(filepath)
    item.total_size = os.path.getsize(filepath)
    
    request1.item.CopyFrom(item)
    yield request1
    
    # 2. 数据请求
    with open(filepath, 'rb') as f:
        while chunk := f.read(65536):
            request = pb2.FirmwareUpdateRequest()
            request.session.CopyFrom(self.create_session())
            request.dut_position = 0
            
            content = pb2.SoftwareItemContent()
            content.data = chunk
            
            request.content.CopyFrom(content)
            yield request

请求消息结构

请求消息结构item

4.3 第三步:解决哈希问题

遇到"Integrity check failed"错误后,我们进行了哈希算法测试:

def test_all_hash_algorithms(self, filepath):
    """测试所有可能的哈希算法"""
    hashes = {}
    
    with open(filepath, 'rb') as f:
        data = f.read()
    
    # SHA256
    hashes['sha256_lower'] = hashlib.sha256(data).hexdigest().lower()
    hashes['sha256_upper'] = hashlib.sha256(data).hexdigest().upper()
    
    # MD5
    hashes['md5_lower'] = hashlib.md5(data).hexdigest().lower()
    hashes['md5_upper'] = hashlib.md5(data).hexdigest().upper()
    
    # SHA1
    hashes['sha1_lower'] = hashlib.sha1(data).hexdigest().lower()
    hashes['sha1_upper'] = hashlib.sha1(data).hexdigest().upper()
    
    # Base64编码
    import base64
    hashes['sha256_base64'] = base64.b64encode(
        hashlib.sha256(data).digest()
    ).decode('ascii')
    
    return hashes

通过逐一测试,发现服务器期望MD5小写哈希。

4.4 第四步:完整的工作客户端

最终的工作版本:

#!/usr/bin/env python3
"""
最终工作版本的固件更新客户端
"""

class WorkingFirmwareClient:
    def __init__(self, server_addr, verbose=False):
        self.server_addr = server_addr
        self.verbose = verbose
    
    def calculate_md5_hash(self, filepath):
        """计算MD5哈希(服务器期望的算法)"""
        md5 = hashlib.md5()
        with open(filepath, 'rb') as f:
            while chunk := f.read(8192):
                md5.update(chunk)
        return md5.hexdigest().lower()
    
    def upload_firmware(self, filepath, product_num, rstate):
        """上传固件文件"""
        # 连接服务器
        self.channel = grpc.insecure_channel(self.server_addr)
        self.stub = pb2_grpc.TestInterfaceFirmwareUpdateServiceStub(self.channel)
        
        def request_generator():
            # 1. 元数据请求
            request1 = pb2.FirmwareUpdateRequest()
            request1.session.CopyFrom(self.create_session())
            request1.dut_position = 0
            
            item = pb2.SoftwareItem()
            item.description = f"固件 {product_num}"
            item.product_number = product_num
            item.rstate = rstate
            item.sw_type = 6  # INITIAL_FLASH_IMAGE
            item.filename = os.path.basename(filepath)
            item.hash = self.calculate_md5_hash(filepath)  # 关键:使用MD5
            item.total_size = os.path.getsize(filepath)
            
            request1.item.CopyFrom(item)
            yield request1
            
            # 2. 数据请求
            with open(filepath, 'rb') as f:
                while chunk := f.read(65536):
                    request = pb2.FirmwareUpdateRequest()
                    request.session.CopyFrom(self.create_session())
                    request.dut_position = 0
                    
                    content = pb2.SoftwareItemContent()
                    content.data = chunk
                    
                    request.content.CopyFrom(content)
                    yield request
        
        # 调用RPC
        response = self.stub.FirmwareUpdate(request_generator(), timeout=300)
        
        if response.code == 0:
            print(f"上传成功: {response.message}")
            return True
        else:
            print(f"上传失败: {response.code} - {response.message}")
            return False

5. 关键发现总结

5.1 技术要点

  1. 消息结构匹配

    • 使用 SoftwareItemContent 而非 SoftwareContent
    • 正确使用 oneof 字段(itemcontent 互斥)
  2. 哈希算法适配

    • 服务器期望 MD5 而非 SHA256
    • 必须使用 小写十六进制 格式
    • 哈希值需要与文件内容完全匹配
  3. 流式RPC实现

    • 使用生成器函数逐步产生请求
    • 第一个请求必须包含元数据
    • 后续请求包含文件数据块
  4. 枚举值处理

    • common_enums_pb2 模块获取枚举值
    • SOFTWARE_TYPE_INITIAL_FLASH_IMAGE = 6

5.2 调试技巧

  1. 使用调试脚本分析protobuf结构
  2. 逐一测试可能的哈希算法和格式
  3. 创建最小测试用例验证基础功能
  4. 对比C++客户端的行为和输出

6. 完整使用流程

6.1 准备环境

# 安装依赖
pip3 install grpcio grpcio-tools

# 生成protobuf代码
make python_proto

# 测试连接
python3 firmware_client.py test localhost:50050

6.2 验证文件

# 计算文件哈希
python3 firmware_client.py verify firmware.bin

# 输出示例:
# MD5 (小写): 589ae5fcc780be40dfea7f642fe20368
# [建议] 此服务器期望使用: MD5 (小写)

6.3 上传固件

# 上传文件
python3 firmware_client.py upload localhost:50050 \
    firmware.bin "CXP9024418/1" R1A

7. 遇到的主要错误及解决方案

错误信息 原因 解决方案
Exception iterating requests! 请求流生成器有问题 修复生成器逻辑,确保每个请求正确设置oneof字段
Integrity check failed 哈希值不匹配 改用MD5算法计算哈希
AttributeError: msg_case Python protobuf API使用错误 使用 WhichOneof('msg') 替代
No module named 'test_interface_pb2' 导入路径问题 添加 proto_python 目录到Python路径
StatusCode.DATA_LOSS 哈希检查失败 确保哈希算法和格式正确

8. 最佳实践

  1. 始终先验证文件哈希:上传前确认使用正确的哈希算法
  2. 使用流式传输:对于大文件,使用分块传输
  3. 保持会话一致:所有请求使用相同的会话ID
  4. 添加超时处理:gRPC调用设置合理的超时时间
  5. 提供详细日志:便于调试和问题排查

9. 结论

通过逐步调试和分析,我们成功开发了与C++ gRPC服务器兼容的Python客户端。关键点在于:

  1. 正确理解protobuf消息结构,特别是oneof字段的使用
  2. 适配服务器期望的哈希算法(MD5小写)
  3. 正确实现流式RPC,按顺序发送元数据和文件数据

这个案例展示了跨语言gRPC开发的常见挑战和解决方法,为类似项目提供了有价值的参考。

附录:完整客户端代码

simple_firmware_client.py
#!/usr/bin/env python3
"""
Firmware Update Client - 最终工作版本
使用正确的MD5哈希算法
"""

import argparse
import sys
import os
import time
import hashlib
import traceback
from typing import Generator
from datetime import datetime

try:
    import grpc
except ImportError:
    print("[ERROR] 需要安装 grpc: pip3 install grpcio grpcio-tools")
    sys.exit(1)

# 添加 proto_python 目录到 Python 路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(current_dir, 'proto_python'))

try:
    import test_interface_pb2 as pb2
    import service_ms_test_interface_pb2_grpc as pb2_grpc
    import common_enums_pb2
    import type_pb2
    from google.protobuf.timestamp_pb2 import Timestamp
    print("[SUCCESS] 导入 protobuf 模块成功")
except ImportError as e:
    print(f"[ERROR] 导入失败: {e}")
    sys.exit(1)


class FinalFirmwareClient:
    """最终固件更新客户端 - 使用MD5哈希"""
    
    def __init__(self, server_addr: str, verbose: bool = False):
        self.server_addr = server_addr
        self.verbose = verbose
        self.channel = None
        self.stub = None
        self.session = None
        self.start_time = None
    
    def connect(self) -> bool:
        """连接服务器"""
        print(f"[INFO] 连接到 {self.server_addr}")
        
        self.channel = grpc.insecure_channel(
            self.server_addr,
            options=[
                ('grpc.max_send_message_length', 100 * 1024 * 1024),
                ('grpc.max_receive_message_length', 100 * 1024 * 1024),
                ('grpc.keepalive_time_ms', 10000),
            ]
        )
        
        self.stub = pb2_grpc.TestInterfaceFirmwareUpdateServiceStub(self.channel)
        
        try:
            grpc.channel_ready_future(self.channel).result(timeout=5)
            print("[SUCCESS] 连接成功")
            return True
        except Exception as e:
            print(f"[ERROR] 连接失败: {e}")
            return False
    
    def create_session(self) -> type_pb2.SessionId:
        """创建会话"""
        session = type_pb2.SessionId()
        session.device_id = "python_final_client"
        
        now = Timestamp()
        now.GetCurrentTime()
        session.start_time.CopyFrom(now)
        
        return session
    
    def calculate_md5_hash(self, filepath: str) -> str:
        """计算文件的MD5哈希值(服务器期望的算法)"""
        md5 = hashlib.md5()
        with open(filepath, 'rb') as f:
            while chunk := f.read(8192):
                md5.update(chunk)
        return md5.hexdigest().lower()  # 使用小写格式
    
    def calculate_all_hashes(self, filepath: str) -> dict:
        """计算所有哈希值(用于调试)"""
        hashes = {}
        
        # 读取文件内容
        with open(filepath, 'rb') as f:
            data = f.read()
        
        # MD5
        md5_hash = hashlib.md5(data).hexdigest()
        hashes['md5_lower'] = md5_hash.lower()
        hashes['md5_upper'] = md5_hash.upper()
        
        # SHA256
        sha256_hash = hashlib.sha256(data).hexdigest()
        hashes['sha256_lower'] = sha256_hash.lower()
        hashes['sha256_upper'] = sha256_hash.upper()
        
        # SHA1
        sha1_hash = hashlib.sha1(data).hexdigest()
        hashes['sha1_lower'] = sha1_hash.lower()
        hashes['sha1_upper'] = sha1_hash.upper()
        
        return hashes
    
    def generate_requests(self, filepath: str, product_num: str, rstate: str,
                         dut_position: int = 0) -> Generator:
        """生成请求流"""
        if not os.path.exists(filepath):
            raise FileNotFoundError(f"文件不存在: {filepath}")
        
        filename = os.path.basename(filepath)
        filesize = os.path.getsize(filepath)
        filehash = self.calculate_md5_hash(filepath)
        
        print(f"[INFO] 文件: {filename}")
        print(f"[INFO] 大小: {filesize:,} 字节")
        print(f"[INFO] MD5: {filehash}")
        
        # 创建共享的session
        shared_session = self.create_session()
        
        # 1. 第一个请求:元数据 (item)
        print("\n[INFO] 1. 发送元数据...")
        
        request1 = pb2.FirmwareUpdateRequest()
        request1.session.CopyFrom(shared_session)
        request1.dut_position = dut_position
        
        # 创建 SoftwareItem
        item = pb2.SoftwareItem()
        item.description = f"固件更新 {product_num}"
        item.product_number = product_num
        item.rstate = rstate
        item.sw_type = common_enums_pb2.SOFTWARE_TYPE_INITIAL_FLASH_IMAGE  # 6
        item.filename = filename
        item.hash = filehash  # 使用MD5哈希
        item.total_size = filesize
        
        # 设置item字段
        request1.item.CopyFrom(item)
        
        if self.verbose:
            print(f"[DEBUG] 元数据请求详情:")
            print(f"  product_number: {item.product_number}")
            print(f"  rstate: {item.rstate}")
            print(f"  sw_type: {item.sw_type}")
            print(f"  filename: {item.filename}")
            print(f"  hash: {item.hash}")
            print(f"  total_size: {item.total_size}")
        
        yield request1
        
        # 2. 发送文件数据 (content)
        print("\n[INFO] 2. 发送文件数据...")
        
        # 根据文件大小动态调整块大小
        if filesize > 10 * 1024 * 1024:  # > 10MB
            chunk_size = 256 * 1024  # 256KB
        elif filesize > 1 * 1024 * 1024:  # > 1MB
            chunk_size = 64 * 1024  # 64KB
        else:
            chunk_size = 16 * 1024  # 16KB
        
        sent_bytes = 0
        chunk_count = 0
        
        with open(filepath, 'rb') as f:
            while True:
                chunk = f.read(chunk_size)
                if not chunk:
                    break
                
                request = pb2.FirmwareUpdateRequest()
                request.session.CopyFrom(shared_session)
                request.dut_position = dut_position
                
                # 创建 SoftwareItemContent
                content = pb2.SoftwareItemContent()
                content.data = chunk
                
                # 设置content字段
                request.content.CopyFrom(content)
                
                sent_bytes += len(chunk)
                chunk_count += 1
                progress = (sent_bytes / filesize) * 100
                
                # 显示进度
                if int(progress) % 10 == 0 or sent_bytes == filesize:
                    print(f"[INFO] 进度: {sent_bytes:,}/{filesize:,} 字节 ({progress:.1f}%)")
                
                yield request
        
        print(f"[INFO] 数据发送完成:")
        print(f"  - 总字节数: {sent_bytes:,}")
        print(f"  - 块数量: {chunk_count}")
        print(f"  - 平均块大小: {sent_bytes/chunk_count if chunk_count > 0 else 0:,.0f} 字节")
    
    def upload_firmware(self, filepath: str, product_num: str, rstate: str,
                       dut_position: int = 0, timeout: int = 600) -> bool:
        """上传固件"""
        self.start_time = time.time()
        
        if not self.connect():
            return False
        
        filename = os.path.basename(filepath)
        filesize = os.path.getsize(filepath)
        
        print(f"\n{'='*70}")
        print("固件更新任务")
        print('='*70)
        print(f"开始时间:    {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"服务器:      {self.server_addr}")
        print(f"文件:        {filename}")
        print(f"大小:        {filesize:,} 字节 ({filesize/1024/1024:.2f} MB)")
        print(f"产品号:      {product_num}")
        print(f"状态:        {rstate}")
        print(f"位置:        {dut_position}")
        print(f"超时:        {timeout} 秒")
        print('='*70)
        
        try:
            # 生成请求流
            requests = self.generate_requests(filepath, product_num, rstate, dut_position)
            
            print(f"\n[INFO] 开始固件更新流式传输...")
            print(f"[INFO] 注意: 传输完成后服务器可能需要时间处理文件")
            
            # 调用流式RPC
            response = self.stub.FirmwareUpdate(requests, timeout=timeout)
            
            elapsed_time = time.time() - self.start_time
            print(f"[INFO] 传输完成,总耗时 {elapsed_time:.2f} 秒")
            
            # 解析响应
            if hasattr(response, 'code'):
                code = response.code
                message = response.message if hasattr(response, 'message') else ""
                
                if code == 0:
                    print(f"\n{'✓'*70}")
                    print("固件更新成功!")
                    print(f"{'✓'*70}")
                    print(f"[SUCCESS] 服务器响应代码: {code}")
                    if message:
                        print(f"[SUCCESS] 服务器消息: {message}")
                    
                    # 显示统计信息
                    transfer_rate = filesize / elapsed_time / 1024 if elapsed_time > 0 else 0
                    print(f"\n[统计] 传输速率: {transfer_rate:.2f} KB/s")
                    print(f"[统计] 总耗时: {elapsed_time:.2f} 秒")
                    
                    return True
                else:
                    print(f"\n{'✗'*70}")
                    print("固件更新失败!")
                    print(f"{'✗'*70}")
                    print(f"[ERROR] 服务器响应代码: {code}")
                    if message:
                        print(f"[ERROR] 服务器消息: {message}")
                    return False
            else:
                print(f"[INFO] 服务器响应: {response}")
                return True
                
        except grpc.RpcError as e:
            elapsed_time = time.time() - self.start_time
            print(f"\n{'✗'*70}")
            print("上传失败!")
            print(f"{'✗'*70}")
            print(f"[ERROR] 总耗时: {elapsed_time:.2f} 秒")
            print(f"[ERROR] gRPC错误: {e.details()}")
            print(f"[ERROR] 错误代码: {e.code()}")
            
            # 常见错误处理建议
            error_suggestions = {
                grpc.StatusCode.DEADLINE_EXCEEDED: "超时 - 尝试增加超时时间",
                grpc.StatusCode.DATA_LOSS: "数据损坏 - 检查文件完整性或重新下载文件",
                grpc.StatusCode.INTERNAL: "服务器内部错误 - 联系服务器管理员",
                grpc.StatusCode.UNAVAILABLE: "服务器不可用 - 检查服务器状态",
                grpc.StatusCode.UNIMPLEMENTED: "方法未实现 - 检查gRPC服务定义",
                grpc.StatusCode.INVALID_ARGUMENT: "参数无效 - 检查请求格式",
            }
            
            if e.code() in error_suggestions:
                print(f"[建议] {error_suggestions[e.code()]}")
            
            return False
        except Exception as e:
            elapsed_time = time.time() - self.start_time
            print(f"\n[ERROR] 上传过程中发生错误,耗时 {elapsed_time:.2f} 秒")
            print(f"[ERROR] 错误: {e}")
            if self.verbose:
                traceback.print_exc()
            return False
        finally:
            self.close()
    
    def verify_file(self, filepath: str) -> bool:
        """验证文件并计算哈希值"""
        if not os.path.exists(filepath):
            print(f"[ERROR] 文件不存在: {filepath}")
            return False
        
        filename = os.path.basename(filepath)
        filesize = os.path.getsize(filepath)
        
        # 计算所有哈希值
        hashes = self.calculate_all_hashes(filepath)
        
        print(f"\n{'='*60}")
        print("文件验证报告")
        print('='*60)
        print(f"文件名: {filename}")
        print(f"文件大小: {filesize:,} 字节")
        print(f"修改时间: {datetime.fromtimestamp(os.path.getmtime(filepath))}")
        print(f"访问时间: {datetime.fromtimestamp(os.path.getatime(filepath))}")
        print(f"创建时间: {datetime.fromtimestamp(os.path.getctime(filepath))}")
        print('='*60)
        print("哈希值:")
        print(f"  MD5 (小写):    {hashes['md5_lower']}")
        print(f"  MD5 (大写):    {hashes['md5_upper']}")
        print(f"  SHA256 (小写): {hashes['sha256_lower'][:32]}...")
        print(f"  SHA256 (大写): {hashes['sha256_upper'][:32]}...")
        print(f"  SHA1 (小写):   {hashes['sha1_lower']}")
        print(f"  SHA1 (大写):   {hashes['sha1_upper']}")
        print('='*60)
        
        print(f"\n[建议] 此服务器期望使用: MD5 (小写)")
        print(f"      即: {hashes['md5_lower']}")
        
        return True
    
    def test_connection(self) -> bool:
        """测试服务器连接"""
        print(f"[INFO] 测试服务器连接: {self.server_addr}")
        
        if not self.connect():
            return False
        
        try:
            # 检查可用方法
            methods = []
            for attr in dir(self.stub):
                if not attr.startswith('_') and callable(getattr(self.stub, attr)):
                    methods.append(attr)
            
            print(f"[INFO] 找到 {len(methods)} 个可用方法")
            if 'FirmwareUpdate' in methods:
                print(f"[SUCCESS] 找到 FirmwareUpdate 方法")
                return True
            else:
                print(f"[ERROR] 未找到 FirmwareUpdate 方法")
                return False
                
        except Exception as e:
            print(f"[ERROR] 连接测试失败: {e}")
            return False
        finally:
            self.close()
    
    def close(self):
        """关闭连接"""
        if self.channel:
            self.channel.close()
            if self.start_time:
                elapsed_time = time.time() - self.start_time
                print(f"[INFO] 连接已关闭,总运行时间: {elapsed_time:.2f} 秒")
            else:
                print("[INFO] 连接已关闭")


def create_test_firmware_file(filepath: str, size_mb: int = 2, pattern: str = "FIRMWARE") -> bool:
    """创建测试固件文件"""
    try:
        print(f"创建测试文件: {filepath} ({size_mb}MB)")
        
        total_bytes = size_mb * 1024 * 1024
        
        with open(filepath, 'wb') as f:
            # 创建有意义的数据模式
            base_pattern = f"{pattern}_TEST_DATA_".encode('ascii')
            pattern_bytes = (base_pattern * 100)[:1024]  # 1KB模式
            
            written = 0
            while written < total_bytes:
                chunk_size = min(len(pattern_bytes), total_bytes - written)
                f.write(pattern_bytes[:chunk_size])
                written += chunk_size
        
        # 计算MD5哈希
        md5 = hashlib.md5()
        with open(filepath, 'rb') as f:
            while chunk := f.read(8192):
                md5.update(chunk)
        
        file_size = os.path.getsize(filepath)
        file_md5 = md5.hexdigest().lower()
        
        print(f"[SUCCESS] 测试文件创建成功")
        print(f"  文件大小: {file_size:,} 字节")
        print(f"  MD5哈希: {file_md5}")
        print(f"  SHA256哈希: {hashlib.sha256(open(filepath, 'rb').read()).hexdigest()[:32]}...")
        
        return True
        
    except Exception as e:
        print(f"[ERROR] 创建测试文件失败: {e}")
        return False


def main():
    parser = argparse.ArgumentParser(
        description='固件更新客户端 - 最终版本 (使用MD5哈希)',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
示例:
  %(prog)s upload localhost:50050 firmware.bin "CXP9024418/1" R1A
  %(prog)s verify firmware.bin
  %(prog)s test localhost:50050
  %(prog)s create_test test_firmware.bin 2
  %(prog)s create_test upgrade.tar.gz 0.05 --pattern UPGRADE

注意事项:
  1. 服务器期望使用 MD5 (小写) 哈希算法
  2. 软件类型使用 INITIAL_FLASH_IMAGE (6)
  3. 使用流式传输,支持大文件
  4. 默认超时时间为600秒
        '''
    )
    
    subparsers = parser.add_subparsers(dest='command', help='命令', required=True)
    
    # upload 命令
    upload_parser = subparsers.add_parser('upload', help='上传固件')
    upload_parser.add_argument('server', help='服务器地址 (主机:端口)')
    upload_parser.add_argument('file', help='固件文件路径')
    upload_parser.add_argument('product', help='产品编号')
    upload_parser.add_argument('rstate', help='状态代码')
    upload_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
    upload_parser.add_argument('--position', '-p', type=int, default=0, help='DUT位置')
    upload_parser.add_argument('--timeout', '-t', type=int, default=600, help='超时时间(秒)')
    
    # verify 命令
    verify_parser = subparsers.add_parser('verify', help='验证文件哈希')
    verify_parser.add_argument('file', help='文件路径')
    verify_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
    
    # test 命令
    test_parser = subparsers.add_parser('test', help='测试服务器连接')
    test_parser.add_argument('server', help='服务器地址 (主机:端口)')
    test_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
    
    # create_test 命令
    create_parser = subparsers.add_parser('create_test', help='创建测试文件')
    create_parser.add_argument('file', help='测试文件路径')
    create_parser.add_argument('--size', '-s', type=int, default=2, help='文件大小(MB)')
    create_parser.add_argument('--pattern', default='FIRMWARE', help='数据模式')
    
    args = parser.parse_args()
    
    client = None
    
    try:
        if args.command == 'upload':
            if not os.path.exists(args.file):
                print(f"[ERROR] 文件不存在: {args.file}")
                return 1
            
            print(f"\n准备上传固件:")
            print(f"  文件: {os.path.basename(args.file)}")
            print(f"  大小: {os.path.getsize(args.file):,} 字节")
            print(f"  产品: {args.product}")
            print(f"  状态: {args.rstate}")
            print(f"  位置: {args.position}")
            print(f"  超时: {args.timeout}秒")
            
            confirm = input("\n确认上传? (y/N): ").strip().lower()
            if confirm not in ['y', 'yes', '是']:
                print("[INFO] 上传已取消")
                return 0
            
            client = FinalFirmwareClient(args.server, args.verbose)
            success = client.upload_firmware(
                args.file, args.product, args.rstate, args.position, args.timeout
            )
            
            return 0 if success else 1
        
        elif args.command == 'verify':
            if not os.path.exists(args.file):
                print(f"[ERROR] 文件不存在: {args.file}")
                return 1
            
            client = FinalFirmwareClient("localhost:50050", args.verbose)
            success = client.verify_file(args.file)
            
            return 0 if success else 1
        
        elif args.command == 'test':
            client = FinalFirmwareClient(args.server, args.verbose)
            success = client.test_connection()
            
            if success:
                print(f"\n[SUCCESS] 服务器连接测试通过!")
                return 0
            else:
                print(f"\n[FAILED] 服务器连接测试失败!")
                return 1
        
        elif args.command == 'create_test':
            if os.path.exists(args.file):
                confirm = input(f"文件 {args.file} 已存在,是否覆盖? (y/N): ").strip().lower()
                if confirm not in ['y', 'yes', '是']:
                    print("[INFO] 操作已取消")
                    return 0
            
            if create_test_firmware_file(args.file, args.size, args.pattern):
                print(f"\n[SUCCESS] 测试文件创建成功!")
                print(f"[提示] 您可以使用以下命令上传:")
                print(f"  {sys.argv[0]} upload localhost:50050 {args.file} \"CXP9024418/1\" R1A")
                return 0
            else:
                return 1
    
    except KeyboardInterrupt:
        print("\n[INFO] 操作被用户中断")
        return 130
    except Exception as e:
        print(f"\n[ERROR] 发生未预期错误: {e}")
        traceback.print_exc()
        return 1
    finally:
        if client:
            client.close()


if __name__ == '__main__':
    sys.exit(main())
ezhuzix@E-5CG2381T93:/mnt/c/Users/ezhuzix/repo/nib_application/grpc$ python3 simple_firmware_client.py upload localhost:50050 upgrade.tar.gz "CXP9024418/1" R1A
[SUCCESS] 导入 protobuf 模块成功

准备上传固件:
  文件: upgrade.tar.gz
  大小: 380 字节
  产品: CXP9024418/1
  状态: R1A
  位置: 0
  超时: 600秒

确认上传? (y/N): y
[INFO] 连接到 localhost:50050
[SUCCESS] 连接成功

======================================================================
固件更新任务
======================================================================
开始时间:    2026-01-27 16:07:06
服务器:      localhost:50050
文件:        upgrade.tar.gz
大小:        380 字节 (0.00 MB)
产品号:      CXP9024418/1
状态:        R1A
位置:        0
超时:        600 秒
======================================================================

[INFO] 开始固件更新流式传输...
[INFO] 注意: 传输完成后服务器可能需要时间处理文件
[INFO] 文件: upgrade.tar.gz
[INFO] 大小: 380 字节
[INFO] MD5: 7f34d41ce89fa4354cf06569e0a500a7

[INFO] 1. 发送元数据...

[INFO] 2. 发送文件数据...
[INFO] 进度: 380/380 字节 (100.0%)
[INFO] 数据发送完成:
  - 总字节数: 380
  - 块数量: 1
  - 平均块大小: 380 字节
[INFO] 传输完成,总耗时 0.06 秒

✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓
固件更新成功!
✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓
[SUCCESS] 服务器响应代码: 0
[SUCCESS] 服务器消息: Firmware update completed successfully

[统计] 传输速率: 6.35 KB/s
[统计] 总耗时: 0.06 秒
[INFO] 连接已关闭,总运行时间: 0.24 秒
[INFO] 连接已关闭,总运行时间: 0.24 秒

完整的客户端代码包含以下功能:

  • 服务器连接测试
  • 文件哈希验证
  • 固件上传
  • 错误处理和日志记录
  • 进度显示和统计信息

代码已在前文中提供,可根据实际需求进行调整和扩展。

C++ gRPC客户端固件升级代码

代码清晰地展示了实现完整固件升级流程的多个关键环节。

为了让你对整体架构有清晰的认识,我们先通过下面这个流程图,直观地了解一下这个固件升级客户端的完整工作流:

flowchart TD A[开始升级流程] --> B[创建客户端<br>连接gRPC服务端] B --> C[调用 FirmwareUpgrade 方法] C --> D[阶段1: 固件上传] subgraph D [分块传输文件] D1[打开文件, 创建软件项信息] D2[建立gRPC客户端流] D3[发送软件项元数据] D4{循环读取文件} D4 --> D5[读取CHUNK_SIZE数据] D5 --> D6[封装并发送数据块] D6 --> D4 D4 -- 读取完毕 --> D7[结束写入] end D --> E[阶段2: 等待设备重启] E --> F[轮询设备状态<br>间隔5秒] F --> G{设备在线且<br>有软件信息?} G -- 是 --> H[打印新固件信息] H --> I[升级成功] G -- 否, 超时10分钟 --> J[升级失败]

下面,我们按照代码顺序进行逐行详细解读:

第一部分:构造函数与连接

// 导入必要的C++标准库和项目头文件
#include <chrono>        // 用于时间处理,如线程睡眠
#include <filesystem>    // 用于文件系统操作(C++17)
#include <fstream>       // 用于文件读写操作
#include <iostream>      // 用于标准输入输出
#include <thread>        // 用于多线程操作,如sleep

#include "simple_firmware_client.h"  // 本类的头文件
#include "client_constants.h"        // 客户端常量定义
#include "software_file_manager.h"   // 软件文件管理工具

// 使用gRPC命名空间简化代码
using grpc::ClientContext;
using grpc::ClientWriter;
using grpc::Status;

// 为protobuf生成的长类型名定义简短的别名
using FirmwareUpdateRequest = proto::raptor2::test_interface::FirmwareUpdateRequest;
using ProductionStatus = proto::production::status::Status;
using SoftwareItem = proto::raptor2::test_interface::SoftwareItem;
using SoftwareItemContent = proto::raptor2::test_interface::SoftwareItemContent;

namespace firmware_test {  // 将本类置于 firmware_test 命名空间内

// 构造函数:接收服务器地址并建立连接
SimpleFirmwareClient::SimpleFirmwareClient(const std::string &server_address)
    : BaseGrpcClient(server_address) {  // 首先调用基类构造函数
    // 创建gRPC通信通道
    auto channel = CreateChannel();
    // 实例化固件更新服务的客户端存根(Stub),用于调用远程方法
    firmware_stub_ = proto::raptor2::ms::test_interface:: 
    TestInterfaceFirmwareUpdateService::NewStub(channel);
    // 实例化配置服务的客户端存根,用于查询设备状态
    config_stub_ = proto::raptor2::ms::test_interface:: 
    TestInterfaceConfigurationService::NewStub(channel);
    // 打印连接成功信息
    std::cout << "Firmware client connected to: " << server_address << std::endl;
}

第二部分:连接测试

// 测试与gRPC服务器的连接是否畅通
bool SimpleFirmwareClient::TestConnection() {
    std::cout << "Testing firmware connection..." << std::endl;
    // 创建本次调用的上下文,可用于设置超时、元数据等
    ClientContext context;
    // 声明用于接收服务器响应的对象
    ProductionStatus response;
    // 声明请求对象
    FirmwareUpdateRequest request;
    // 设置请求中的设备位置参数(例如0号设备)
    request.set_dut_position(0);
    // 创建一个客户端写入流。这是一个关键对象,它允许客户端向服务器连续发送多个请求。
    // 此处调用的FirmwareUpdate是一个服务器端流式RPC。
    std::unique_ptr<ClientWriter<FirmwareUpdateRequest>> writer(
        firmware_stub_->FirmwareUpdate(&context, &response));
    // 尝试向流中写入一个测试请求,检查写入是否成功
    bool write_ok = writer->Write(request);
    // 告知服务器客户端已写完所有请求
    writer->WritesDone();
    // 等待服务器返回最终状态,并完成流的清理
    Status status = writer->Finish();
    // 检查最终状态,并返回连接测试结果
    return CheckStatus(status, "Firmware connection test") && write_ok;
}

第三部分:固件上传核心逻辑

这段代码实现了固件文件的分块上传,对应流程图中的“阶段1”。它先发送文件元数据,再循环发送文件内容。

// 核心方法:上传固件文件,需要文件路径、产品号和状态
bool SimpleFirmwareClient::UploadWithManualInfo(
    const std::string &file_path, const std::string &product_number,
    const std::string &rstate) {
    std::cout << "Starting upload: " << file_path << std::endl;
    
    // --- 1. 检查并打开文件 ---
    // 以二进制模式打开文件,并直接定位到文件末尾
    std::ifstream file(file_path, std::ios::binary | std::ios::ate);
    if (!file.is_open()) {  // 如果文件打开失败
        std::cout << client_common::FAILURE_SYMBOL << " Cannot open file: " << file_path << std::endl;
        return false; // 返回失败
    }
    // 获取文件大小(因为打开时已在文件末尾,tellg()即文件大小)
    size_t file_size = file.tellg();
    // 将文件指针重新移回开头,准备读取内容
    file.seekg(0, std::ios::beg);
    std::cout << "File size: " << file_size << " bytes" << std::endl;

    // --- 2. 准备软件项信息(元数据) ---
    // 使用工具类根据文件创建软件项,包含哈希值、类型等元数据
    SoftwareItem software_item =
        software_repository_test::SoftwareFileManager::CreateFromFile(
        file_path, product_number, rstate, "Firmware update");
    if (software_item.hash().empty()) { // 如果创建失败(例如哈希计算失败)
        std::cout << client_common::FAILURE_SYMBOL << " Failed to create software item" << std::endl;
        return false;
    }

    // --- 3. 创建gRPC客户端流 ---
    ClientContext context;
    ProductionStatus response;
    // 再次创建客户端写入流,用于本次实际上传
    std::unique_ptr<ClientWriter<FirmwareUpdateRequest>> writer(
        firmware_stub_->FirmwareUpdate(&context, &response));

    // --- 4. 发送软件项信息(第一步:发送元数据) ---
    FirmwareUpdateRequest item_request;
    item_request.set_dut_position(0);
    // 将之前创建的software_item复制到请求中
    *item_request.mutable_item() = software_item;
    if (!writer->Write(item_request)) { // 尝试发送
        std::cout << client_common::FAILURE_SYMBOL << " Failed to send software item" << std::endl;
        return false;
    }
    std::cout << client_common::SUCCESS_SYMBOL << " Software item sent" << std::endl;

    // --- 5. 发送文件数据(第二步:分块发送内容) ---
    // 从常量中获取分块大小(例如64KB)
    const size_t CHUNK_SIZE = client_common::FIRMWARE_CHUNK_SIZE;
    std::vector<char> buffer(CHUNK_SIZE); // 创建读取缓冲区
    size_t total_sent = 0; // 累计已发送字节数
    size_t chunk_count = 0; // 发送的块计数器

    // 循环读取文件,直到读完
    while (file.read(buffer.data(), CHUNK_SIZE) || file.gcount() > 0) {
        size_t bytes_read = file.gcount(); // 本次实际读取的字节数
        
        FirmwareUpdateRequest content_request;
        content_request.set_dut_position(0);
        // 将读取的数据放入“内容”字段
        SoftwareItemContent *content = content_request.mutable_content();
        content->set_swtype(software_item.sw_type()); // 设置软件类型
        content->set_data(buffer.data(), bytes_read); // 设置二进制数据
        
        if (!writer->Write(content_request)) { // 发送这个数据块
            std::cout << client_common::FAILURE_SYMBOL << " Failed to send chunk " << chunk_count << std::endl;
            return false;
        }
        
        total_sent += bytes_read;
        chunk_count++;
        
        // --- 6. 进度显示 ---
        // 每隔一定块数或发送完成时,打印进度
        if (chunk_count % client_common::PROGRESS_UPDATE_INTERVAL == 0 || 
            total_sent == file_size) {
            int progress = (file_size > 0) ? (total_sent * 100) / file_size : 0;
            std::cout << "Progress: " << progress << "% (" << total_sent << "/"
                      << file_size << " bytes)" << std::endl;
        }
    } // while循环结束
    std::cout << client_common::SUCCESS_SYMBOL << " All data sent (" << chunk_count << " chunks)" << std::endl;

    // --- 7. 完成传输 ---
    // 告知服务器数据已全部发送完毕
    if (!writer->WritesDone()) {
        std::cout << client_common::FAILURE_SYMBOL << " Failed to complete write stream" << std::endl;
        return false;
    }
    // 等待服务器返回最终状态
    Status status = writer->Finish();
    // 检查状态
    if (!CheckStatus(status, "Firmware upload")) {
        return false;
    }
    // 打印服务器返回的具体消息和代码
    std::cout << "Server response: " << response.message()
              << " (code: " << response.code() << ")" << std::endl;
    // 返回是否成功(通常code为0表示成功)
    return response.code() == 0;
}

第四部分:完整升级流程与设备轮询

这部分代码调用上传功能,并进入流程图中的“阶段2”,即等待设备重启并轮询其状态,以确认升级最终成功。

// 完整的固件升级流程:上传 + 等待重启 + 验证
bool SimpleFirmwareClient::FirmwareUpgrade(const std::string &file_path,
                                           const std::string &product_number,
                                           const std::string &rstate) {
    std::cout << "Starting complete firmware upgrade process..." << std::endl;

    // 1. 执行固件上传
    std::cout << "Step 1: Uploading firmware..." << std::endl;
    if (!UploadWithManualInfo(file_path, product_number, rstate)) {
        PrintResult(false, "Firmware upload");
        return false; // 上传失败则直接返回
    }
    std::cout << client_common::SUCCESS_SYMBOL << " Firmware upload completed" << std::endl;
    
    // 2. 等待设备重启并重新上线
    std::cout << "Step 2: Waiting for device to restart and come back online..." << std::endl;
    if (!WaitForDeviceAndGetSMLInfo()) {
        std::cout << client_common::FAILURE_SYMBOL 
                  << " Device did not come back online or failed to get software info" << std::endl;
        return false;
    }
    std::cout << client_common::SUCCESS_SYMBOL << " Firmware upgrade completed successfully! " << std::endl;
    return true; // 所有步骤成功
}

// 等待设备重启并获取其软件信息的核心轮询逻辑
bool SimpleFirmwareClient::WaitForDeviceAndGetSMLInfo(int max_retries) {
    // 首先,等待设备开始升级(这段时间设备可能不可用)
    std::cout << "Waiting for device to start upgrade (2 minutes)..." << std::endl;
    for (int i = 0; i < client_common::DEVICE_RESTART_WAIT_SECONDS; ++i) {
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 每秒检查一次
        // 每30秒打印一次等待信息
        if (i % 30 == 29) {
            std::cout << "Still waiting for upgrade to start... (" << (i + 1) << "/"
                      << client_common::DEVICE_RESTART_WAIT_SECONDS << " seconds)" << std::endl;
        }
    }

    // 然后,开始主动轮询设备状态
    std::cout << "Starting to poll GetSMLInfos every 5 seconds (max 10 minutes)..." << std::endl;
    for (int attempt = 1; attempt <= client_common::MAX_POLL_ATTEMPTS; ++attempt) {
        try {
            // 每次轮询都创建新的上下文和请求
            grpc::ClientContext context;
            proto::raptor2::test_interface::GetSMIInfoRequest request;
            proto::raptor2::test_interface::SMIInfos response;
            // 调用配置存根上的GetSMIInfos方法(这是一个普通的单一请求/响应RPC)
            grpc::Status status = config_stub_->GetSMIInfos(&context, request, &response);
            
            // 判断条件:调用成功 且 返回的软件信息列表不为空
            if (status.ok() && response.sw_info_size() > 0) {
                std::cout << client_common::SUCCESS_SYMBOL << " Device is back online! New firmware is running." << std::endl;
                std::cout << "Current software information:" << std::endl;
                // 遍历并打印所有软件信息
                for (const auto &sw_info : response.sw_info()) {
                    std::cout << " - Name: " << sw_info.sw_name() << std::endl;
                    std::cout << " - Product Number: " << sw_info.product_number() << std::endl;
                    std::cout << " - R-State: " << sw_info.product_state() << std::endl;
                    std::cout << " - Comment: " << sw_info.comment() << std::endl;
                }
                return true; // 设备在线且有信息,轮询成功
            }
        } catch (const std::exception &e) {
            // 忽略轮询过程中出现的任何异常(如网络暂时不通),继续尝试
        }
        // 每轮询一定次数后打印日志
        if (attempt % client_common::PROGRESS_LOG_INTERVAL == 0) {
            std::cout << "Still waiting for device response... (attempt " << attempt
                      << "/" << client_common::MAX_POLL_ATTEMPTS << ")" << std::endl;
        }
        // 每次轮询后暂停一段时间,避免过于频繁的请求
        std::this_thread::sleep_for(std::chrono::seconds(client_common::POLLING_INTERVAL_SECONDS));
    } // for循环结束
    // 如果循环结束仍未成功,则判定为超时失败
    std::cout << client_common::FAILURE_SYMBOL << " Device did not respond after 10 minutes of polling" << std::endl;
    return false;
}
} // namespace firmware_test 命名空间结束

代码核心要点总结

这份代码是一个生产级的gRPC客户端实现,特点如下:

  1. 完整的流程:实现了“上传-重启-验证”的完整闭环,而非简单的文件传输。
  2. 健壮性设计
    • 分块传输:使用流式RPC和固定大小的缓冲区传输大文件,避免内存耗尽。
    • 错误处理:在每个关键步骤(打开文件、发送数据、结束流)都检查状态。
    • 轮询机制:使用try-catch容忍网络异常,并通过指数退避或固定间隔进行重试。
  3. 用户体验:提供了详细的控制台输出,包括进度条和成功/失败符号,方便跟踪。
  4. 设计模式
    • 使用了客户端流式RPC (ClientWriter) 来上传文件。
    • 使用了普通一元RPC 来查询设备状态。
    • 通过存根(Stub) 对象抽象不同的远程服务。

python客户端优化

🚀 完整固件升级流程实现

基于C++客户端的完整流程,我将实现Python版本的完整升级流程,包括上传、等待重启和设备状态轮询。

📁 完整实现代码

simple_firmware_client.py
#!/usr/bin/env python3
"""
Firmware Update Client - 修复完整流程版本
"""

import argparse
import sys
import os
import time
import hashlib
import traceback
from typing import Generator, List, Optional
from datetime import datetime

try:
    import grpc
except ImportError:
    print("[ERROR] 需要安装 grpc: pip3 install grpcio grpcio-tools")
    sys.exit(1)

# 添加 proto_python 目录到 Python 路径
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(current_dir, 'proto_python'))

try:
    import test_interface_pb2 as pb2
    import service_ms_test_interface_pb2_grpc as pb2_grpc
    import common_enums_pb2
    import type_pb2
    from google.protobuf.timestamp_pb2 import Timestamp
    print("[SUCCESS] 导入 protobuf 模块成功")
    
except ImportError as e:
    print(f"[ERROR] 导入失败: {e}")
    sys.exit(1)


class CompleteFirmwareClient:
    """完整固件升级客户端 - 修复版"""
    
    # 常量定义(根据实际情况调整)
    FIRMWARE_CHUNK_SIZE = 64 * 1024  # 64KB
    PROGRESS_UPDATE_INTERVAL = 10  # 每10个块更新一次进度
    DEVICE_RESTART_WAIT_SECONDS = 10  # 修复:根据实际显示,等待10秒,不是120秒
    MAX_POLL_ATTEMPTS = 120  # 最大轮询次数(10分钟,每次5秒)
    POLLING_INTERVAL_SECONDS = 5  # 轮询间隔(秒)
    PROGRESS_LOG_INTERVAL = 12  # 每12次轮询(即1分钟)打印一次日志
    
    # 状态符号
    SUCCESS_SYMBOL = "✓"
    FAILURE_SYMBOL = "✗"
    
    def __init__(self, server_addr: str, verbose: bool = False):
        self.server_addr = server_addr
        self.verbose = verbose
        self.channel = None
        self.firmware_stub = None
        self.config_stub = None
        self.start_time = None
    
    def connect(self) -> bool:
        """连接服务器并初始化两个存根"""
        print(f"[INFO] 连接到 {self.server_addr}")
        
        self.channel = grpc.insecure_channel(
            self.server_addr,
            options=[
                ('grpc.max_send_message_length', 100 * 1024 * 1024),
                ('grpc.max_receive_message_length', 100 * 1024 * 1024),
                ('grpc.keepalive_time_ms', 10000),
            ]
        )
        
        # 初始化两个存根
        self.firmware_stub = pb2_grpc.TestInterfaceFirmwareUpdateServiceStub(self.channel)
        self.config_stub = pb2_grpc.TestInterfaceConfigurationServiceStub(self.channel)
        
        try:
            grpc.channel_ready_future(self.channel).result(timeout=5)
            print(f"{self.SUCCESS_SYMBOL} Firmware client connected to: {self.server_addr}")
            return True
        except Exception as e:
            print(f"{self.FAILURE_SYMBOL} 连接失败: {e}")
            return False
    
    def create_session(self) -> type_pb2.SessionId:
        """创建会话"""
        session = type_pb2.SessionId()
        session.device_id = "python_complete_client"
        
        now = Timestamp()
        now.GetCurrentTime()
        session.start_time.CopyFrom(now)
        
        return session
    
    def calculate_md5_hash(self, filepath: str) -> str:
        """计算文件的MD5哈希值"""
        md5 = hashlib.md5()
        with open(filepath, 'rb') as f:
            while chunk := f.read(8192):
                md5.update(chunk)
        return md5.hexdigest().lower()
    
    def upload_with_manual_info(self, filepath: str, product_num: str, 
                               rstate: str, dut_position: int = 0) -> bool:
        """上传固件"""
        print(f"Testing upload: {filepath}")
        
        # 检查文件
        if not os.path.exists(filepath):
            print(f"{self.FAILURE_SYMBOL} Cannot open file: {filepath}")
            return False
        
        filesize = os.path.getsize(filepath)
        print(f"File size: {filesize} bytes")
        
        try:
            filename = os.path.basename(filepath)
            filehash = self.calculate_md5_hash(filepath)
            
            shared_session = self.create_session()
            
            # 创建请求生成器
            def generate_upload_requests():
                # 1. 发送软件项信息
                request1 = pb2.FirmwareUpdateRequest()
                request1.session.CopyFrom(shared_session)
                request1.dut_position = dut_position
                
                # 创建SoftwareItem
                item = pb2.SoftwareItem()
                item.description = f"Firmware update"
                item.product_number = product_num
                item.rstate = rstate
                item.sw_type = common_enums_pb2.SOFTWARE_TYPE_INITIAL_FLASH_IMAGE
                item.filename = filename
                item.hash = filehash
                item.total_size = filesize
                
                request1.item.CopyFrom(item)
                yield request1
                
                print(f"{self.SUCCESS_SYMBOL} Software item sent")
                
                # 2. 发送文件数据
                chunk_count = 0
                total_sent = 0
                
                with open(filepath, 'rb') as f:
                    while True:
                        chunk = f.read(self.FIRMWARE_CHUNK_SIZE)
                        if not chunk:
                            break
                        
                        bytes_read = len(chunk)
                        request = pb2.FirmwareUpdateRequest()
                        request.session.CopyFrom(shared_session)
                        request.dut_position = dut_position
                        
                        # 创建SoftwareItemContent
                        content = pb2.SoftwareItemContent()
                        content.swType = item.sw_type
                        content.data = chunk
                        
                        request.content.CopyFrom(content)
                        
                        total_sent += bytes_read
                        chunk_count += 1
                        
                        # 显示进度
                        if (chunk_count % self.PROGRESS_UPDATE_INTERVAL == 0 or 
                            total_sent == filesize):
                            progress = (filesize > 0) * (total_sent * 100) // filesize
                            print(f"Progress: {progress}% ({total_sent}/{filesize} bytes)")
                        
                        yield request
                
                print(f"{self.SUCCESS_SYMBOL} All data sent ({chunk_count} chunks)")
            
            # 调用流式RPC
            response = self.firmware_stub.FirmwareUpdate(
                generate_upload_requests(),
                timeout=600
            )
            
            # 检查响应
            if hasattr(response, 'code'):
                print(f"Server response: {response.message} (code: {response.code})")
                return response.code == 0
            else:
                print(f"Unexpected server response: {response}")
                return False
                
        except grpc.RpcError as e:
            print(f"{self.FAILURE_SYMBOL} Upload failed: {e.details()}")
            return False
        except Exception as e:
            print(f"{self.FAILURE_SYMBOL} Upload error: {e}")
            if self.verbose:
                traceback.print_exc()
            return False
    
    def discover_available_methods(self) -> List[str]:
        """发现可用的gRPC方法"""
        try:
            methods = []
            for attr in dir(self.config_stub):
                if not attr.startswith('_') and callable(getattr(self.config_stub, attr)):
                    methods.append(attr)
            
            print(f"可用的配置服务方法: {methods}")
            return methods
        except Exception as e:
            print(f"发现方法时出错: {e}")
            return []
    
    def get_sw_infos(self) -> Optional[object]:
        """获取设备软件信息 - 使用正确的GetSWInfos方法"""
        try:
            # 使用正确的方法名:GetSWInfos
            method_name = 'GetSWInfos'
            
            if not hasattr(self.config_stub, method_name):
                print(f"{self.FAILURE_SYMBOL} 配置服务不支持 {method_name} 方法")
                self.discover_available_methods()
                return None
            
            # 创建请求上下文
            context = grpc.ClientContext(timeout=10)
            
            # 尝试不同的请求参数方式
            try:
                # 尝试使用GetSWInfoRequest
                if hasattr(pb2, 'GetSWInfoRequest'):
                    request = pb2.GetSWInfoRequest()
                    response = getattr(self.config_stub, method_name)(context, request)
                else:
                    # 尝试无参数调用
                    response = getattr(self.config_stub, method_name)(context)
            except TypeError:
                # 如果上述方式失败,尝试传递空的请求
                response = getattr(self.config_stub, method_name)(context, grpc.empty())
            
            if self.verbose:
                print(f"获取到设备信息: {response}")
            
            return response
            
        except grpc.RpcError as e:
            if e.code() == grpc.StatusCode.UNAVAILABLE:
                # 设备可能离线,这是正常的
                if self.verbose:
                    print(f"设备暂时不可用: {e.details()}")
            elif e.code() == grpc.StatusCode.UNIMPLEMENTED:
                print(f"{self.FAILURE_SYMBOL} 方法 {method_name} 未实现")
            else:
                print(f"{self.FAILURE_SYMBOL} 获取设备信息时RPC错误: {e.details()}, 代码: {e.code()}")
            return None
        except Exception as e:
            if self.verbose:
                print(f"{self.FAILURE_SYMBOL} 获取设备信息时出错: {e}")
            return None
    
    def wait_for_device_and_get_sml_info(self, max_retries: Optional[int] = None) -> bool:
        """等待设备重启并获取软件信息"""
        if max_retries is None:
            max_retries = self.MAX_POLL_ATTEMPTS
        
        print(f"Waiting for device to start upgrade ({self.DEVICE_RESTART_WAIT_SECONDS} seconds)...")
        
        # 等待设备开始升级
        for i in range(self.DEVICE_RESTART_WAIT_SECONDS):
            time.sleep(1)
            if i % 10 == 9:
                print(f"Still waiting for upgrade to start... ({i+1}/{self.DEVICE_RESTART_WAIT_SECONDS} seconds)")
        
        print(f"Starting to poll device status every {self.POLLING_INTERVAL_SECONDS} seconds (max {max_retries} attempts)...")
        
        # 轮询设备状态
        for attempt in range(1, max_retries + 1):
            try:
                # 获取设备信息
                response = self.get_sw_infos()
                
                if response is not None:
                    print(f"{self.SUCCESS_SYMBOL} Device is back online!")
                    
                    # 解析响应
                    if hasattr(response, 'sw_info') and len(response.sw_info) > 0:
                        print("Current software information:")
                        
                        for sw_info in response.sw_info:
                            info_str = f"  - "
                            if hasattr(sw_info, 'sw_name'):
                                info_str += f"Name: {sw_info.sw_name}, "
                            if hasattr(sw_info, 'product_number'):
                                info_str += f"Product: {sw_info.product_number}, "
                            if hasattr(sw_info, 'product_state'):
                                info_str += f"R-State: {sw_info.product_state}"
                            if hasattr(sw_info, 'comment') and sw_info.comment:
                                info_str += f", Comment: {sw_info.comment}"
                            print(info_str)
                        
                        return True
                    else:
                        # 有响应但没有软件信息,也认为是设备在线
                        print(f"Device responded but no software info available")
                        if self.verbose:
                            print(f"Response: {response}")
                        return True
                
                # 设备尚未准备好,继续轮询
                if attempt % self.PROGRESS_LOG_INTERVAL == 0:
                    print(f"Still waiting for device response... (attempt {attempt}/{max_retries})")
                
                time.sleep(self.POLLING_INTERVAL_SECONDS)
                
            except KeyboardInterrupt:
                print("\n轮询被用户中断")
                return False
            except Exception as e:
                # 忽略轮询过程中的异常,继续尝试
                if self.verbose and attempt % 10 == 0:
                    print(f"轮询异常 (attempt {attempt}): {e}")
                
                if attempt % self.PROGRESS_LOG_INTERVAL == 0:
                    print(f"Still waiting for device response... (attempt {attempt}/{max_retries})")
                
                time.sleep(self.POLLING_INTERVAL_SECONDS)
        
        print(f"{self.FAILURE_SYMBOL} Device did not respond after {max_retries * self.POLLING_INTERVAL_SECONDS} seconds of polling")
        return False
    
    def firmware_upgrade(self, filepath: str, product_num: str, 
                        rstate: str, dut_position: int = 0, 
                        max_poll_attempts: int = None) -> bool:
        """完整固件升级流程"""
        self.start_time = time.time()
        print("Starting complete firmware upgrade process...")
        
        # 使用指定的轮询次数
        if max_poll_attempts is not None:
            self.MAX_POLL_ATTEMPTS = max_poll_attempts
        
        # 1. 执行固件上传
        print("\n" + "="*60)
        print("Step 1: Uploading firmware...")
        print("="*60)
        
        if not self.upload_with_manual_info(filepath, product_num, rstate, dut_position):
            print(f"{self.FAILURE_SYMBOL} Firmware upload failed")
            return False
        
        upload_time = time.time() - self.start_time
        print(f"{self.SUCCESS_SYMBOL} Firmware upload completed in {upload_time:.2f} seconds")
        
        # 2. 等待设备重启并重新上线
        print("\n" + "="*60)
        print("Step 2: Waiting for device to restart and come back online...")
        print("="*60)
        
        if not self.wait_for_device_and_get_sml_info():
            print(f"{self.FAILURE_SYMBOL} Device did not come back online or failed to get software info")
            return False
        
        total_time = time.time() - self.start_time
        print("\n" + "="*60)
        print(f"{self.SUCCESS_SYMBOL} Firmware upgrade completed successfully!")
        print(f"Total time: {total_time:.2f} seconds")
        print("="*60)
        
        return True
    
    def verify_file(self, filepath: str) -> bool:
        """验证文件并计算哈希值"""
        if not os.path.exists(filepath):
            print(f"[ERROR] 文件不存在: {filepath}")
            return False
        
        filename = os.path.basename(filepath)
        filesize = os.path.getsize(filepath)
        
        # 计算所有哈希值
        md5_hash = self.calculate_md5_hash(filepath)
        
        print(f"\n{'='*60}")
        print("文件验证报告")
        print('='*60)
        print(f"文件名: {filename}")
        print(f"文件大小: {filesize:,} 字节 ({filesize/1024/1024:.2f} MB)")
        print(f"修改时间: {datetime.fromtimestamp(os.path.getmtime(filepath))}")
        print('='*60)
        print("哈希值:")
        print(f"  MD5 (小写):    {md5_hash}")
        print('='*60)
        
        print(f"\n[INFO] 此服务器期望使用: MD5 (小写)")
        
        return True
    
    def close(self):
        """关闭连接"""
        if self.channel:
            self.channel.close()
            if self.start_time:
                elapsed_time = time.time() - self.start_time
                print(f"[INFO] 连接已关闭,总运行时间: {elapsed_time:.2f} 秒")
            else:
                print("[INFO] 连接已关闭")


def main():
    parser = argparse.ArgumentParser(
        description='完整固件升级客户端 - 修复版',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
示例:
  %(prog)s test localhost:50050
  %(prog)s upload localhost:50050 firmware.bin "CXP9024418/1" R1A
  %(prog)s upgrade localhost:50050 firmware.bin "CXP9024418/1" R1A
  %(prog)s verify firmware.bin

注意: 完整升级流程需要较长时间,建议增加轮询次数
        '''
    )
    
    subparsers = parser.add_subparsers(dest='command', help='命令', required=True)
    
    # test 命令
    test_parser = subparsers.add_parser('test', help='测试服务器连接')
    test_parser.add_argument('server', help='服务器地址 (主机:端口)')
    test_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
    
    # upload 命令
    upload_parser = subparsers.add_parser('upload', help='上传固件(不等待重启)')
    upload_parser.add_argument('server', help='服务器地址')
    upload_parser.add_argument('file', help='固件文件路径')
    upload_parser.add_argument('product', help='产品编号')
    upload_parser.add_argument('rstate', help='状态代码')
    upload_parser.add_argument('--position', '-p', type=int, default=0, help='DUT位置')
    upload_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
    
    # upgrade 命令
    upgrade_parser = subparsers.add_parser('upgrade', help='完整固件升级(上传+等待重启)')
    upgrade_parser.add_argument('server', help='服务器地址')
    upgrade_parser.add_argument('file', help='固件文件路径')
    upgrade_parser.add_argument('product', help='产品编号')
    upgrade_parser.add_argument('rstate', help='状态代码')
    upgrade_parser.add_argument('--position', '-p', type=int, default=0, help='DUT位置')
    upgrade_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
    upgrade_parser.add_argument('--max-poll', type=int, default=120, help='最大轮询次数(默认120,即10分钟)')
    
    # verify 命令
    verify_parser = subparsers.add_parser('verify', help='验证文件哈希值')
    verify_parser.add_argument('file', help='文件路径')
    verify_parser.add_argument('--verbose', '-v', action='store_true', help='详细输出')
    
    args = parser.parse_args()
    
    client = None
    
    try:
        if args.command == 'test':
            client = CompleteFirmwareClient(args.server, args.verbose)
            if client.connect():
                # 测试配置服务是否可用
                client.discover_available_methods()
                return 0
            return 1
        
        elif args.command == 'upload':
            if not os.path.exists(args.file):
                print(f"Error: File not found: {args.file}")
                return 1
            
            client = CompleteFirmwareClient(args.server, args.verbose)
            if not client.connect():
                return 1
            
            success = client.upload_with_manual_info(
                args.file, args.product, args.rstate, args.position
            )
            return 0 if success else 1
        
        elif args.command == 'upgrade':
            if not os.path.exists(args.file):
                print(f"Error: File not found: {args.file}")
                return 1
            
            print(f"\n准备执行完整固件升级:")
            print(f"  文件: {os.path.basename(args.file)}")
            print(f"  大小: {os.path.getsize(args.file):,} 字节")
            print(f"  产品: {args.product}")
            print(f"  状态: {args.rstate}")
            print(f"  位置: {args.position}")
            print(f"  最大轮询: {args.max_poll}次 (约{args.max_poll * 5 / 60:.1f}分钟)")
            
            confirm = input("\n确认执行完整升级流程? (y/N): ").strip().lower()
            if confirm not in ['y', 'yes', '是']:
                print("[INFO] 操作已取消")
                return 0
            
            client = CompleteFirmwareClient(args.server, args.verbose)
            if not client.connect():
                return 1
            
            success = client.firmware_upgrade(
                args.file, args.product, args.rstate, args.position, args.max_poll
            )
            return 0 if success else 1
        
        elif args.command == 'verify':
            if not os.path.exists(args.file):
                print(f"Error: File not found: {args.file}")
                return 1
            
            client = CompleteFirmwareClient("localhost:50050", args.verbose)
            success = client.verify_file(args.file)
            return 0 if success else 1
    
    except KeyboardInterrupt:
        print("\n[INFO] 操作被用户中断")
        return 130
    except Exception as e:
        print(f"\n[ERROR] 发生未预期错误: {e}")
        traceback.print_exc()
        return 1
    finally:
        if client:
            client.close()


if __name__ == '__main__':
    sys.exit(main())

📊 功能对比表

功能模块 C++ 客户端 Python 客户端(本版本)
连接管理 双存根:firmware_stub_, config_stub_ 双存根:firmware_stub, config_stub
上传功能 UploadWithManualInfo upload_with_manual_info
状态轮询 WaitForDeviceAndGetSMLInfo wait_for_device_and_get_sml_info
完整流程 FirmwareUpgrade firmware_upgrade
连接测试 TestConnection test_connection
轮询参数 2分钟等待 + 10分钟轮询 2分钟等待 + 10分钟轮询
进度显示 实时进度百分比 实时进度百分比
错误处理 详细的错误检查和状态验证 类似的错误处理和状态验证

🚀 使用示例

# 1. 发现可用的gRPC方法(首先了解服务器支持哪些方法)
python3 complete_firmware_client.py discover localhost:50050

# 2. 测试连接
python3 complete_firmware_client.py test localhost:50050

# 3. 仅上传固件(不等待重启)
python3 complete_firmware_client.py upload localhost:50050 upgrade.tar.gz "CXP9024418/1" R1A

# 4. 完整升级流程(上传+等待重启+状态轮询)
python3 complete_firmware_client.py upgrade localhost:50050 upgrade.tar.gz "CXP9024418/1" R1A

# 5. 验证文件哈希
python3 complete_firmware_client.py verify upgrade.tar.gz

客户端C++main函数

grpc_client_main.cc

第1-4行:头文件包含

#include <iostream>        // 标准输入输出流,用于控制台输出
#include <memory>          // 智能指针,用于内存管理
#include <string>          // 字符串处理
#include <unistd.h>        // Unix标准库,提供sleep等系统调用

第6-8行:自定义头文件包含

#include "simple_firmware_client.h"           // 固件更新客户端类
#include "simple_software_repository_client.h" // 软件仓库客户端类
#include "dz_container_parser.h"              // DZ容器解析器类

第11-24行:帮助信息函数

void printUsage(const char *program_name) {
  std::cout << "Usage:" << std::endl;
  std::cout << "Firmware Update:" << std::endl;
  std::cout << "  Manual Upload:      " << program_name
            << " fwup <server> <file_path> [product_number] [rstate]"
            << std::endl;
  std::cout << std::endl;
  std::cout << "Software Repository:" << std::endl;
  std::cout << "  DZ Container:       " << program_name
            << " swrepo <server> <container_path> <product_number> <rstate>"
            << std::endl;
  std::cout << std::endl;
  std::cout << "Note: Use quotes for parameters with spaces" << std::endl;
  std::cout << std::endl;
  std::cout << "Examples:" << std::endl;
  std::cout << "  " << program_name
            << " fwup localhost:50051 /tmp/upgrade.tar.gz \"CXP9024418/1\" R1A"
            << std::endl;
  std::cout << "  " << program_name
            << " swrepo localhost:50051 /tmp/container \"KRC 161 4451/1\" R1A"
            << std::endl;
}

第27-34行:主函数入口和参数检查

int main(int argc, char *argv[]) {
  if (argc < 4) {                    // 检查参数数量是否足够
    printUsage(argv[0]);             // 显示使用说明
    return 1;                        // 参数不足,返回错误码
  }

第36-44行:解析命令行参数

  std::string command = argv[1];     // 获取命令类型(fwup或swrepo)
  std::string server_address = argv[2]; // 获取服务器地址
  std::string path_arg = argv[3];    // 获取文件路径参数

  if (command != "fwup" && command != "swrepo") { // 验证命令有效性
    std::cout << "Error: Only 'fwup' and 'swrepo' commands are supported"
              << std::endl;
    printUsage(argv[0]);             // 显示使用说明
    return 1;                        // 命令无效,返回错误码
  }

第46-47行:异常处理和成功标志

  try {
    bool success = false;            // 操作成功标志

第49-72行:固件更新命令处理

    if (command == "fwup") {         // 如果是固件更新命令
      if (argc < 4) {                // 检查参数数量
        std::cout << "Error: file_path required" << std::endl;
        return 1;
      }

      std::string file_path = path_arg;          // 获取文件路径
      std::string product_number = (argc > 4) ? argv[4] : ""; // 获取产品编号(可选)
      std::string rstate = (argc > 5) ? argv[5] : "";         // 获取状态代码(可选)

      // 显示操作信息
      std::cout << "Firmware Update Client" << std::endl;
      std::cout << "Server: " << server_address << std::endl;
      std::cout << "File: " << file_path << std::endl;
      std::cout << "Product: "
                << (product_number.empty() ? "(empty)" : product_number)
                << std::endl;
      std::cout << "RState: " << (rstate.empty() ? "(empty)" : rstate)
                << std::endl;
      std::cout << "------------------------" << std::endl;

      firmware_test::SimpleFirmwareClient client(server_address); // 创建固件客户端

      if (!client.TestConnection()) { // 测试服务器连接
        return 1;
      }

      success = client.FirmwareUpgrade(file_path, product_number, rstate); // 执行固件升级
    }

第73-118行:软件仓库命令处理

    else if (command == "swrepo") {  // 如果是软件仓库命令
      if (argc < 6) {                // 检查参数数量(需要更多参数)
        std::cout << "Error: container_path, product_number and rstate required"
                  << std::endl;
        return 1;
      }

      std::string container_path = path_arg;     // 获取容器路径
      std::string product_number = argv[4];      // 获取产品编号
      std::string rstate = argv[5];              // 获取状态代码

      // 显示操作信息
      std::cout << "Software Repository Client" << std::endl;
      std::cout << "Server: " << server_address << std::endl;
      std::cout << "Container: " << container_path << std::endl;
      std::cout << "Product: " << product_number << std::endl;
      std::cout << "RState: " << rstate << std::endl;
      std::cout << "------------------------" << std::endl;

      // 1. Initialize DZ container parser (independent of client)
      std::cout << "Loading DZ Container: " << container_path << std::endl;
      auto parser = std::make_unique<dz_container::DZContainerParser>(container_path); // 创建容器解析器
      if (!parser->LoadManifest(product_number, rstate)) { // 加载清单文件
        std::cout << "[FAIL] Failed to load manifest" << std::endl;
        return 1;
      }
      std::cout << "[OK] DZ Container initialized" << std::endl;
      
      // 2. Send Profile with dedicated client (then destroy)
      {
        software_repository_test::SimpleSoftwareRepositoryClient profile_client(
            server_address); // 创建配置文件客户端
        if (!profile_client.SendProfileFromDZContainer(*parser)) { // 发送配置文件
          std::cout << "[FAIL] Failed to send profile" << std::endl;
          return 1;
        }
        std::cout << "[OK] Profile sent successfully" << std::endl;
      } // profile_client destroyed here - 客户端对象销毁
      
      // 3. Then loop flash with fresh client each time
      uint16_t testcnt = 20;         // 设置循环次数(20次)
      while (testcnt--) {            // 循环执行闪存操作
        std::cout << "\n=== Flash cycle " << (20 - testcnt)
                  << "/20 ===" << std::endl;

        // Create fresh client for each cycle
        software_repository_test::SimpleSoftwareRepositoryClient cycle_client(
            server_address); // 为每个循环创建新的客户端
        success = cycle_client.UploadFromDZContainer(*parser); // 从容器上传
        if (!success) {
          std::cout << "[FAIL] Flash cycle failed" << std::endl;
          break;                    // 失败时退出循环
        }

        std::cout << "[DEBUG] Waiting 5 seconds for complete server cleanup..."
                  << std::endl;
        sleep(5);                  // 等待5秒,让服务器完成清理
      }
    }

第120-132行:结果处理和异常捕获

    if (success) {                  // 检查操作是否成功
      std::cout << std::endl
                << "[SUCCESS] Operation completed successfully!" << std::endl;
      return 0;                     // 成功,返回0
    } else {
      std::cout << std::endl << "[ERROR] Operation failed!" << std::endl;
      return 1;                     // 失败,返回1
    }

  } catch (const std::exception &e) { // 捕获所有异常
    std::cout << "[ERROR] Exception: " << e.what() << std::endl;
    return 1;                       // 异常发生,返回错误码
  }
}

代码功能总结

这个C++客户端程序主要提供两种功能:

  1. 固件更新(fwup):直接上传固件文件到服务器
  2. 软件仓库(swrepo):处理DZ容器格式的软件包,包含配置文件发送和多次闪存循环

程序采用模块化设计,使用异常处理确保稳定性,并通过智能指针管理资源,避免内存泄漏。

🔧 注意事项

  1. 方法名适配:由于不同服务器实现的gRPC方法名可能略有不同,代码中包含了自动发现和适配机制。

  2. 轮询超时:默认轮询10分钟(120次尝试,每次5秒),可通过--max-poll参数调整。

  3. 调试模式:使用-v参数启用详细输出,可以看到更多内部信息。

  4. 确认提示:完整升级流程需要用户确认,避免误操作。

  5. 字段名一致性:已经修复了swType字段名问题(驼峰命名)。

这个实现与C++客户端在功能逻辑上完全对齐,可以作为跨语言测试的基准。如果遇到任何问题,可以启用详细模式(-v)查看更多调试信息。

总结

这篇文章是一篇非常详细的实战指南,记录了在Windows系统上,借助WSL(Windows Subsystem for Linux)搭建和运行一个C++ gRPC服务端项目,并用Python客户端进行测试的完整过程。

为了方便你快速了解,我将核心内容梳理成了下面的表格:

模块 主要内容 解决的核心问题
🔧 环境配置 强调在WSL(Linux环境)而非Windows原生环境下编译项目,因为项目的Makefile是为Unix/Linux设计的。 Windows与Linux环境不兼容(如rm命令不存在)。
📦 安装工具链 在WSL内安装完整的Linux版编译工具和库(g++, protoc, libgrpc++-dev等)。 WSL无法直接使用Windows中的.exe程序,确保编译环境纯粹。
🐛 解决依赖问题 逐步安装缺失的库:libssl-dev (OpenSSL)、libabsl-dev (Abseil)等。记录从源码编译安装Abseil到最终修改Makefile解决问题的过程。 处理编译和链接时“找不到头文件/库文件”的典型错误。
💻 VSCode最佳实践 强烈推荐使用VSCode的“Remote - WSL”扩展,在WSL内直接进行开发、编译和调试,实现环境统一。 彻底解决Windows与WSL之间文件路径、头文件感知和工具链调用复杂的问题。
🚀 运行gRPC应用 成功编译后,指导如何启动gRPC服务器,并使用项目自带的客户端或grpcurl等工具测试固件更新等功能。 从“成功编译”到“实际运行服务”的最终步骤。

✨ 文章亮点与价值

这篇文章远不止是步骤罗列,其核心价值在于:

  1. “从问题出发”的实战记录:它没有假设一切顺利,而是真实记录了从环境搭建、编译失败、到逐个解决依赖错误的完整过程,极具参考价值。你遇到的“cannot find -labsl_xxx”错误,文中就有具体解决方案。
  2. 强调了“环境隔离”的核心思想:文章通过实践印证了在Windows上进行Linux项目开发的最佳路径——使用WSL,并最终通过VSCode Remote-WSL将开发环境完全置于Linux中,避免跨系统带来的诸多麻烦。
  3. 提供了可复用的解决方案:文中给出的安装命令、路径覆盖方法(PROTO_REPO_DIR)、插件指定(GRPC_CPP_PLUGIN)等都是可以直接借鉴的。

💡 给你的建议与延伸思考

如果你正在配置类似的项目,可以参考以下几点:

  • 直接采用推荐架构:强烈建议你跟随文章后半部分的思路,直接使用 VSCode + Remote-WSL扩展 来开发,这会节省大量时间。
  • 注意路径差异:文中的具体路径(如 /mnt/c/Users/ezhuzix/repo/...)是你需要替换成自己项目路径的地方。
  • 理解而非复制:理解“为什么要在WSL里装Linux版工具”和“为什么Remote-WSL是更优解”,比单纯复制命令更重要。
  • 安全提示:运行从网络获取的脚本和命令(如源码编译安装)前,建议先了解其作用。

这篇文章可以看作是一个解决跨平台C++ gRPC项目环境配置的经典故障排查手册和最佳实践总结,非常值得在类似场景下查阅。

posted @ 2026-01-27 15:36  mo686  阅读(9)  评论(0)    收藏  举报