在 C++/Qt 上位机中嵌入 Python Runtime 与 pybind11,实现脚本化自动控制
文章目录
在 C++/Qt 上位机中嵌入 Python Runtime 与 pybind11,实现脚本化自动控制
在工业制造类上位机软件中,经常会遇到这类需求:
- 设备调试流程频繁变化;
- 不同产线、不同客户需要定制动作流程;
- 工程师希望不用重新编译 C++ 程序,就能修改测试逻辑;
- 需要把设备控制、数据采集、视觉检测、MES 通讯等能力开放给脚本调用。
一种常见方案是:
在 C++/Qt 上位机程序中嵌入 Python Runtime,通过 pybind11 把 C++ 设备控制接口暴露给 Python 脚本,使 Python 脚本可以调用 C++ 能力完成自动化任务。
本文介绍一种适用于工业上位机软件的脚本化架构设计,并给出核心代码示例。
一、整体架构
典型架构如下:
+--------------------------------------------------+
| Qt 上位机程序 |
| |
| +----------------+ +--------------------+ |
| | UI界面 | ---> | ScriptManager | |
| +----------------+ +--------------------+ |
| | |
| v |
| +--------------------+ |
| | Python Runtime | |
| +--------------------+ |
| | |
| v |
| +--------------------+ |
| | pybind11 API模块 | |
| +--------------------+ |
| | |
| v |
| +----------------+ +--------------------+ |
| | Motion控制 | | IO控制/扫码/MES | |
| +----------------+ +--------------------+ |
| |
+--------------------------------------------------+
核心思想是:
- Qt 程序负责 UI、线程管理、设备连接、状态显示;
- C++ 实现实际的工业控制逻辑,例如运动控制、IO、扫码、相机采集;
- pybind11 将部分 C++ 类和函数封装成 Python API;
- 用户编写 Python 脚本,调用这些 Python API;
- Qt 程序加载并执行 Python 脚本;
- Python 脚本中的每一个 API 调用最终映射到 C++ 函数执行。
二、技术选型
1. Python Runtime
Python Runtime 用于在 C++ 程序中嵌入 Python 解释器。
例如:
#include <pybind11/embed.h>
namespace py = pybind11;
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
py::scoped_interpreter guard{};
MainWindow w;
w.show();
return app.exec();
}
py::scoped_interpreter 会初始化 Python 解释器,并在对象析构时关闭解释器。
2. pybind11
pybind11 用于在 C++ 和 Python 之间建立绑定关系。
它既可以用于:
- C++ 调用 Python;
- Python 调用 C++;
- 嵌入式 Python 模块;
- 扩展式 Python 模块。
在上位机中,更常用的是:
C++ 程序内部嵌入 Python,并通过
PYBIND11_EMBEDDED_MODULE暴露 C++ API 给 Python 脚本。
三、CMake 配置示例
假设项目结构如下:
IndustrialApp/
├── CMakeLists.txt
├── main.cpp
├── MainWindow.cpp
├── ScriptManager.cpp
├── DeviceController.cpp
├── bindings/
│ └── PyApiBindings.cpp
└── scripts/
└── auto_test.py
CMake 示例:
cmake_minimum_required(VERSION 3.20)
project(IndustrialApp)
set(CMAKE_CXX_STANDARD 17)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
find_package(pybind11 REQUIRED)
add_executable(IndustrialApp
main.cpp
MainWindow.cpp
ScriptManager.cpp
DeviceController.cpp
bindings/PyApiBindings.cpp
)
target_link_libraries(IndustrialApp
PRIVATE
Qt6::Widgets
pybind11::embed
)
如果使用 Qt5:
find_package(Qt5 REQUIRED COMPONENTS Widgets)
target_link_libraries(IndustrialApp
PRIVATE
Qt5::Widgets
pybind11::embed
)
四、C++ 设备控制层设计
工业上位机通常会有一层统一的设备控制接口。
例如:
// DeviceController.h
#pragma once
#include <string>
#include <mutex>
class DeviceController
{
public:
bool connectDevice();
void disconnectDevice();
bool moveAxis(const std::string& axis, double position, double speed);
bool waitMotionDone(const std::string& axis, int timeoutMs);
bool setOutput(int channel, bool value);
bool getInput(int channel);
std::string scanBarcode();
void logInfo(const std::string& msg);
void logError(const std::string& msg);
private:
std::mutex mutex_;
};
实现示例:
// DeviceController.cpp
#include "DeviceController.h"
#include <iostream>
#include <thread>
#include <chrono>
bool DeviceController::connectDevice()
{
logInfo("Connecting device...");
return true;
}
void DeviceController::disconnectDevice()
{
logInfo("Disconnecting device...");
}
bool DeviceController::moveAxis(const std::string& axis, double position, double speed)
{
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "[C++] Move axis: " << axis
<< ", position: " << position
<< ", speed: " << speed << std::endl;
return true;
}
bool DeviceController::waitMotionDone(const std::string& axis, int timeoutMs)
{
std::cout << "[C++] Wait motion done: " << axis
<< ", timeout: " << timeoutMs << " ms" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
return true;
}
bool DeviceController::setOutput(int channel, bool value)
{
std::lock_guard<std::mutex> lock(mutex_);
std::cout << "[C++] Set output: " << channel
<< ", value: " << value << std::endl;
return true;
}
bool DeviceController::getInput(int channel)
{
std::cout << "[C++] Get input: " << channel << std::endl;
return true;
}
std::string DeviceController::scanBarcode()
{
std::cout << "[C++] Scan barcode" << std::endl;
return "SN123456789";
}
void DeviceController::logInfo(const std::string& msg)
{
std::cout << "[INFO] " << msg << std::endl;
}
void DeviceController::logError(const std::string& msg)
{
std::cerr << "[ERROR] " << msg << std::endl;
}
五、设计 Python API
脚本工程师不应该直接接触底层复杂的 C++ 对象。
可以设计一个面向工艺流程的 Python API,例如:
import machine
machine.log_info("开始自动测试")
machine.move_axis("X", 100.0, 50.0)
machine.wait_motion_done("X", 5000)
machine.set_output(1, True)
if machine.get_input(2):
machine.log_info("检测到到位信号")
sn = machine.scan_barcode()
machine.log_info("扫码结果: " + sn)
这个 machine 模块不是普通 Python 文件,而是由 C++ 使用 pybind11 嵌入注册出来的模块。
六、使用 pybind11 暴露 C++ 接口
1. 全局设备对象
为了让 Python API 能够调用实际设备,可以在 C++ 中维护一个全局或单例设备对象。
例如:
// AppContext.h
#pragma once
#include "DeviceController.h"
class AppContext
{
public:
static AppContext& instance()
{
static AppContext ctx;
return ctx;
}
DeviceController& device()
{
return device_;
}
private:
DeviceController device_;
};
2. pybind11 模块绑定
// bindings/PyApiBindings.cpp
#include <pybind11/embed.h>
#include "AppContext.h"
namespace py = pybind11;
PYBIND11_EMBEDDED_MODULE(machine, m)
{
m.doc() = "Industrial machine control API";
m.def("connect", []() {
return AppContext::instance().device().connectDevice();
});
m.def("disconnect", []() {
AppContext::instance().device().disconnectDevice();
});
m.def("move_axis", [](const std::string& axis, double pos, double speed) {
return AppContext::instance().device().moveAxis(axis, pos, speed);
});
m.def("wait_motion_done", [](const std::string& axis, int timeout_ms) {
return AppContext::instance().device().waitMotionDone(axis, timeout_ms);
});
m.def("set_output", [](int channel, bool value) {
return AppContext::instance().device().setOutput(channel, value);
});
m.def("get_input", [](int channel) {
return AppContext::instance().device().getInput(channel);
});
m.def("scan_barcode", []() {
return AppContext::instance().device().scanBarcode();
});
m.def("log_info", [](const std::string& msg) {
AppContext::instance().device().logInfo(msg);
});
m.def("log_error", [](const std::string& msg) {
AppContext::instance().device().logError(msg);
});
}
这样 Python 中就可以:
import machine
machine.move_axis("X", 100, 50)
而这行代码最终会调用:
DeviceController::moveAxis("X", 100, 50)
七、脚本管理器 ScriptManager
Qt 程序中通常会封装一个脚本执行管理器,负责:
- 初始化 Python 环境;
- 加载脚本文件;
- 执行脚本函数;
- 捕获异常;
- 向 UI 输出日志;
- 控制脚本启动、停止、暂停等。
示例:
// ScriptManager.h
#pragma once
#include <QObject>
#include <QString>
class ScriptManager : public QObject
{
Q_OBJECT
public:
explicit ScriptManager(QObject* parent = nullptr);
bool runScriptFile(const QString& filePath);
bool runScriptFunction(const QString& filePath, const QString& functionName);
signals:
void scriptStarted();
void scriptFinished();
void scriptError(QString message);
void scriptLog(QString message);
};
实现:
// ScriptManager.cpp
#include "ScriptManager.h"
#include <pybind11/embed.h>
#include <QFileInfo>
namespace py = pybind11;
ScriptManager::ScriptManager(QObject* parent)
: QObject(parent)
{
}
bool ScriptManager::runScriptFile(const QString& filePath)
{
emit scriptStarted();
try
{
py::gil_scoped_acquire gil;
py::object globals = py::dict();
py::object locals = py::dict();
py::eval_file(filePath.toStdString(), globals, locals);
emit scriptFinished();
return true;
}
catch (const py::error_already_set& e)
{
emit scriptError(QString::fromStdString(e.what()));
return false;
}
catch (const std::exception& e)
{
emit scriptError(QString::fromStdString(e.what()));
return false;
}
}
执行整个脚本:
scriptManager->runScriptFile("scripts/auto_test.py");
八、推荐的脚本结构设计
不建议所有逻辑直接写在脚本顶层。
推荐定义统一入口函数,例如 main()、setup()、run()、teardown()。
示例:
# scripts/auto_test.py
import machine
import time
def setup():
machine.log_info("初始化测试流程")
machine.connect()
def run():
machine.log_info("开始执行自动流程")
machine.move_axis("X", 100.0, 50.0)
machine.wait_motion_done("X", 5000)
machine.set_output(1, True)
time.sleep(0.2)
machine.set_output(1, False)
if machine.get_input(2):
machine.log_info("产品到位")
else:
machine.log_error("产品未到位")
return False
sn = machine.scan_barcode()
machine.log_info("SN = " + sn)
return True
def teardown():
machine.log_info("流程结束")
machine.disconnect()
def main():
setup()
result = False
try:
result = run()
finally:
teardown()
return result
然后 C++ 执行指定函数:
bool ScriptManager::runScriptFunction(const QString& filePath, const QString& functionName)
{
emit scriptStarted();
try
{
py::gil_scoped_acquire gil;
py::dict globals;
py::dict locals;
py::eval_file(filePath.toStdString(), globals, locals);
py::object func = locals[functionName.toStdString().c_str()];
py::object result = func();
bool ok = result.cast<bool>();
emit scriptFinished();
return ok;
}
catch (const py::error_already_set& e)
{
emit scriptError(QString::fromStdString(e.what()));
return false;
}
catch (const std::exception& e)
{
emit scriptError(QString::fromStdString(e.what()));
return false;
}
}
调用:
scriptManager->runScriptFunction("scripts/auto_test.py", "main");
九、Qt 中避免 UI 卡死:脚本线程
Python 脚本可能执行较长时间,如果直接在 UI 线程执行,会导致界面卡死。
推荐将脚本放到工作线程中执行。
示例:
QThread* thread = new QThread;
ScriptManager* manager = new ScriptManager;
manager->moveToThread(thread);
connect(thread, &QThread::started, [manager]() {
manager->runScriptFunction("scripts/auto_test.py", "main");
});
connect(manager, &ScriptManager::scriptFinished, thread, &QThread::quit);
connect(manager, &ScriptManager::scriptError, thread, &QThread::quit);
connect(thread, &QThread::finished, manager, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
注意:
- Python Runtime 是进程级别的;
- 多线程调用 Python 时必须处理 GIL;
- 同一时刻通常只允许一个脚本控制设备;
- 设备控制接口需要加锁或状态机保护。
十、如何让 Python 日志输出到 Qt 界面
可以在 C++ 中设计一个日志服务,然后暴露给 Python。
例如:
class LogService
{
public:
std::function<void(const std::string&)> onLog;
void info(const std::string& msg)
{
if (onLog)
onLog("[INFO] " + msg);
}
void error(const std::string& msg)
{
if (onLog)
onLog("[ERROR] " + msg);
}
};
然后绑定:
m.def("log_info", [](const std::string& msg) {
AppContext::instance().device().logInfo(msg);
});
实际项目中可以改为:
m.def("log_info", [](const std::string& msg) {
AppContext::instance().logService().info(msg);
});
Qt 侧通过 signal/slot 更新界面:
connect(scriptManager, &ScriptManager::scriptLog,
this, &MainWindow::appendLog);
十一、Python 脚本如何转换为 C++ 可执行命令?
这是很多人容易误解的地方。
严格来说:
Python 脚本并不是被转换成 C++ 代码,也不是被编译成 C++ 可执行文件。
实际过程是:
Python脚本
|
v
Python解释器读取并执行
|
v
执行到 import machine
|
v
加载由 pybind11 注册的 C++ 内嵌模块
|
v
执行 machine.move_axis("X", 100, 50)
|
v
pybind11 将 Python 参数转换为 C++ 参数
|
v
调用 C++ lambda 函数
|
v
调用 DeviceController::moveAxis()
|
v
驱动运动控制卡、PLC、IO模块、相机等硬件
例如 Python 脚本:
machine.move_axis("X", 100.0, 50.0)
运行时会经过以下映射:
Python str "X" -> std::string axis
Python float 100.0 -> double position
Python float 50.0 -> double speed
然后 pybind11 调用绑定函数:
m.def("move_axis", [](const std::string& axis, double pos, double speed) {
return AppContext::instance().device().moveAxis(axis, pos, speed);
});
最终执行:
DeviceController::moveAxis(axis, pos, speed);
因此,Python 脚本本质上是在描述自动化流程,而真正执行硬件动作的仍然是 C++ 代码。
可以理解为:
Python脚本 = 工艺流程描述层
pybind11 = 参数转换和函数分发层
C++代码 = 实际设备执行层
十二、命令模式设计:让脚本生成 C++ 命令对象
对于复杂工业软件,建议不要让 Python 直接调用所有设备接口,而是引入“命令模式”。
1. 定义 C++ 命令接口
class ICommand
{
public:
virtual ~ICommand() = default;
virtual bool execute() = 0;
};
运动命令:
class MoveAxisCommand : public ICommand
{
public:
MoveAxisCommand(DeviceController& device,
std::string axis,
double pos,
double speed)
: device_(device),
axis_(std::move(axis)),
pos_(pos),
speed_(speed)
{
}
bool execute() override
{
return device_.moveAxis(axis_, pos_, speed_);
}
private:
DeviceController& device_;
std::string axis_;
double pos_;
double speed_;
};
命令调度器:
class CommandExecutor
{
public:
bool executeMoveAxis(const std::string& axis, double pos, double speed)
{
MoveAxisCommand cmd(AppContext::instance().device(), axis, pos, speed);
return cmd.execute();
}
};
Python API 绑定:
m.def("move_axis", [](const std::string& axis, double pos, double speed) {
return AppContext::instance()
.commandExecutor()
.executeMoveAxis(axis, pos, speed);
});
这样 Python 调用:
machine.move_axis("X", 100, 50)
实际会被转换成:
MoveAxisCommand cmd(device, "X", 100, 50);
cmd.execute();
这种设计的好处是:
- 方便记录命令历史;
- 方便做权限检查;
- 方便做流程回放;
- 方便做暂停、恢复、停止;
- 方便接入状态机;
- 方便统一异常处理。
十三、脚本停止与安全控制
工业软件必须考虑安全性。
Python 脚本不能随意控制设备,建议增加:
- 急停检测;
- 设备状态检查;
- 运动互锁;
- 用户权限;
- 脚本超时;
- 黑白名单 API;
- 运行前语法检查;
- 日志追踪;
- 异常后安全复位。
例如在 C++ API 中增加状态检查:
m.def("move_axis", [](const std::string& axis, double pos, double speed) {
auto& ctx = AppContext::instance();
if (ctx.isEmergencyStop())
{
throw std::runtime_error("Emergency stop is active");
}
if (!ctx.device().isConnected())
{
throw std::runtime_error("Device is not connected");
}
return ctx.device().moveAxis(axis, pos, speed);
});
Python 中捕获异常:
try:
machine.move_axis("X", 100, 50)
except Exception as e:
machine.log_error("运动失败: " + str(e))
十四、完整执行流程示例
用户点击 Qt 界面的“执行脚本”按钮:
void MainWindow::onRunScriptClicked()
{
scriptManager->runScriptFunction("scripts/auto_test.py", "main");
}
C++ 加载 Python 脚本:
py::eval_file("scripts/auto_test.py", globals, locals);
Python 调用:
machine.move_axis("X", 100.0, 50.0)
pybind11 转发到 C++:
m.def("move_axis", [](const std::string& axis, double pos, double speed) {
return AppContext::instance().device().moveAxis(axis, pos, speed);
});
C++ 执行硬件动作:
DeviceController::moveAxis("X", 100.0, 50.0);
设备执行完成后返回结果:
C++ bool -> Python bool -> 脚本继续执行
十五、总结
通过 Python Runtime 和 pybind11,可以很方便地为 C++/Qt 工业上位机程序增加脚本自动化能力。
推荐的架构是:
Qt UI
|
ScriptManager
|
Python Runtime
|
pybind11 API
|
C++ Command / DeviceController
|
工业设备
Python 脚本主要负责:
- 描述工艺流程;
- 编排动作顺序;
- 判断流程分支;
- 调用设备 API;
- 处理异常和日志。
C++ 程序主要负责:
- 设备驱动;
- 运动控制;
- IO 控制;
- 线程安全;
- 权限管理;
- 安全互锁;
- 状态机;
- UI 显示。
需要注意的是,Python 脚本并不是转换成 C++ 代码,而是在 Python Runtime 中解释执行。当脚本调用 machine.xxx() 时,pybind11 会把 Python 函数调用映射到对应的 C++ 函数,从而实现脚本控制 C++ 上位机执行实际任务。
更多推荐



所有评论(0)