openPOWERLINK c读取数据并送到mqtt

要实现 openPOWERLINK C 语言读取 PLC 数据 + MQTT 发送,核心逻辑是:用 C 语言调用 openPOWERLINK 协议栈读取 PLC 的 PDO/SDO 数据,再通过 MQTT 客户端库(如 Paho MQTT C)将数据发布到 MQTT Broker。以下是完整可落地的方案,包含环境搭建、代码开发、调试步骤,适配 Windows/Linux 系统。

一、核心前提(必满足)

1. 硬件要求
  • 目标 PLC:支持 Powerlink 协议(贝加莱 X20、倍福 CX 等),已配置为「从站(CN)」,完成 PDO/SDO 映射(记录变量的「OD 索引 + 子索引 + 数据类型」);
  • 通信连接:C 程序运行设备(Powerlink 主站)与 PLC 直连(CAT5e 以上网线)或接入 Powerlink 兼容交换机;
  • 网卡:推荐支持硬件时间戳的网卡(如 Intel I210),保障 Powerlink 实时性。
2. 软件依赖
依赖项 作用 下载 / 安装方式
openPOWERLINK 协议栈 Powerlink 主站通信核心(C 语言) 官网:https://openpowerlink.sourceforge.io/ 或 Git 克隆:git clone https://git.code.sf.net/p/openpowerlink/code
Paho MQTT C 库 MQTT 客户端(C 语言,轻量稳定) Git 克隆:git clone https://github.com/eclipse/paho.mqtt.c.git
编译工具 编译协议栈、MQTT 库、应用程序 Windows:MinGW-w64;Linux:gcc/make;CMake(跨平台构建工具)
MQTT Broker 接收 MQTT 消息(如 EMQ X、Mosquitto、阿里云 IoT 等) 本地部署:Mosquitto(sudo apt install mosquitto);云端:阿里云 IoT / 华为云 IoT
辅助工具 调试 MQTT 消息 MQTTX(客户端工具,验证消息是否发布成功)

二、第一步:搭建基础环境(编译依赖库)

需先编译 openPOWERLINK 协议栈Paho MQTT C 库 ,生成静态库 / 动态库,供应用程序调用。以下以 Linux(Ubuntu 20.04) 为例(Windows 流程类似,仅编译命令调整)。

1. 安装系统依赖

bash

运行

复制代码
sudo apt update && sudo apt install -y gcc make cmake libpcap-dev pkg-config
  • libpcap-dev:openPOWERLINK 依赖(网络抓包);
  • pkg-config:辅助查找 MQTT 库路径。
2. 编译 Paho MQTT C 库

bash

运行

复制代码
# 1. 克隆源码
git clone https://github.com/eclipse/paho.mqtt.c.git
cd paho.mqtt.c
# 2. 创建编译目录
mkdir build && cd build
# 3. CMake配置(生成静态库+动态库)
cmake .. -DCMAKE_BUILD_TYPE=Release -DPAHO_BUILD_STATIC=ON -DPAHO_BUILD_SHARED=ON
# 4. 编译并安装(安装到系统目录/usr/local)
make -j4 && sudo make install
# 5. 刷新库缓存(让系统识别新安装的MQTT库)
sudo ldconfig

编译成功后,生成:

  • 静态库:/usr/local/lib/libpaho-mqtt3c.a(C 语言同步客户端);
  • 动态库:/usr/local/lib/libpaho-mqtt3c.so
  • 头文件:/usr/local/include/mqtt/(包含MQTTClient.h等)。

bash

运行

复制代码
# 1. 克隆源码(若已下载可跳过)
git clone https://git.code.sf.net/p/openpowerlink/code openpowerlink-code
cd openpowerlink-code
# 2. 创建编译目录
mkdir build && cd build
# 3. CMake配置(启用Powerlink主站模式+静态库)
cmake .. -DCMAKE_BUILD_TYPE=Release \
-DOPLK_BUILD_STACK=ON \
-DOPLK_BUILD_APPS=OFF \
-DOPLK_TARGET_PLATFORM=linux \
-DOPLK_NETIF=linux \
-DOPLK_BUILD_STATIC_LIB=ON \
-DOPLK_MN_MODE=ON  # 运行为主站(读取PLC数据)
# 4. 编译(生成静态库)
make -j4

编译成功后,核心文件在 build/lib/Release/

  • 静态库:libpowerlink.a
  • 头文件:源码根目录include/(如oplk/oplk.hoplk/sdo.hoplk/pdo.h)。

代码分为 3 个模块:Powerlink初始化与数据读取MQTT连接与消息发布主逻辑调度。需将 openPOWERLINK 的头文件目录和库文件路径配置到编译命令中。

c

运行

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>

// 1. openPOWERLINK头文件(需替换为你的openPOWERLINK源码include路径)
#include "../../openpowerlink-code/include/oplk/oplk.h"
#include "../../openpowerlink-code/include/oplk/sdo.h"
#include "../../openpowerlink-code/include/oplk/pdo.h"

// 2. Paho MQTT头文件(系统安装路径)
#include <mqtt/MQTTClient.h>

// -------------------------- 配置参数(必须根据实际场景修改)--------------------------
// Powerlink配置
#define PLC_NODE_ID        0x01        // 目标PLC的Node ID(1-239,唯一)
#define MAIN_STATION_ID    0xEF        // Powerlink主站Node ID(默认239=0xEF)
#define NET_IF_NAME        "eth0"      // 主站网卡名称(Linux:ifconfig查询;Windows:NPF GUID)
// 变量配置(从PLC的EDS文件/厂商工具获取)
#define VAR_OD_INDEX       0x0E253A4E  // 变量的OD索引(十六进制)
#define VAR_OD_SUBINDEX    0x002E      // 变量的子索引(十六进制)
#define VAR_DATA_LEN       2           // 变量长度(字节):UINT16=2,INT32=4,FLOAT=4
#define VAR_DATA_TYPE      "UINT16"    // 变量数据类型(用于JSON格式化)

// MQTT配置
#define MQTT_BROKER        "tcp://localhost:1883"  // MQTT Broker地址(本地Mosquitto默认)
#define MQTT_CLIENT_ID     "powerlink_plc_reader"   // MQTT客户端ID(唯一)
#define MQTT_TOPIC         "plc/powerlink/data"     // 发布消息的主题
#define MQTT_QOS           1                        // QoS等级(0=最多一次,1=至少一次,2=恰好一次)
#define MQTT_USERNAME      ""                       // MQTT用户名(无则留空)
#define MQTT_PASSWORD      ""                       // MQTT密码(无则留空)
#define PUBLISH_INTERVAL   1000                     // 发布间隔(毫秒,PDO实时读取可设为10)
// --------------------------------------------------------------------------------

// 全局变量(控制程序退出)
static volatile int g_run_program = 1;

// 信号处理函数(捕获Ctrl+C,优雅退出)
void signal_handler(int sig) {
    if (sig == SIGINT) {
        printf("\n收到退出信号,正在关闭程序...\n");
        g_run_program = 0;
    }
}

// -------------------------- MQTT相关函数 --------------------------
/**
 * @brief 初始化MQTT客户端,建立与Broker的连接
 * @return MQTTClient句柄(NULL表示失败)
 */
MQTTClient init_mqtt_client() {
    MQTTClient client;
    MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
    int ret;

    // 1. 创建MQTT客户端
    ret = MQTTClient_create(&client, MQTT_BROKER, MQTT_CLIENT_ID,
                           MQTTCLIENT_PERSISTENCE_NONE, NULL);
    if (ret != MQTTCLIENT_SUCCESS) {
        printf("MQTT客户端创建失败,错误码: %d\n", ret);
        return NULL;
    }

    // 2. 配置连接参数
    conn_opts.keepAliveInterval = 60;  // 心跳间隔(秒)
    conn_opts.cleansession = 1;        // 清除会话(断开后重新连接不恢复历史消息)
    if (strlen(MQTT_USERNAME) > 0) {
        conn_opts.username = MQTT_USERNAME;
        conn_opts.password = MQTT_PASSWORD;
    }

    // 3. 连接MQTT Broker
    printf("正在连接MQTT Broker: %s...\n", MQTT_BROKER);
    ret = MQTTClient_connect(client, &conn_opts);
    if (ret != MQTTCLIENT_SUCCESS) {
        printf("MQTT连接失败,错误码: %d\n", ret);
        MQTTClient_destroy(&client);
        return NULL;
    }
    printf("MQTT连接成功!\n");
    return client;
}

/**
 * @brief 发布消息到MQTT Broker
 * @param client MQTTClient句柄
 * @param payload 消息内容(字符串)
 * @return 0=成功,非0=失败
 */
int publish_mqtt_message(MQTTClient client, const char* payload) {
    MQTTClient_message pub_msg = MQTTClient_message_initializer;
    int ret;

    pub_msg.payload = (void*)payload;
    pub_msg.payloadlen = strlen(payload);
    pub_msg.qos = MQTT_QOS;
    pub_msg.retained = 0;  // 不保留消息(新订阅者看不到历史消息)

    ret = MQTTClient_publishMessage(client, MQTT_TOPIC, &pub_msg, NULL);
    if (ret != MQTTCLIENT_SUCCESS) {
        printf("MQTT消息发布失败,错误码: %d\n", ret);
        return ret;
    }
    printf("[MQTT发布] 主题: %s, 消息: %s\n", MQTT_TOPIC, payload);
    return 0;
}

// -------------------------- Powerlink相关函数 --------------------------
/**
 * @brief 初始化Powerlink主站
 * @return 0=成功,非0=失败(参考openPOWERLINK错误码)
 */
int init_powerlink() {
    tOplkError ret;

    // 1. 初始化Powerlink栈(主站模式)
    printf("正在初始化Powerlink主站...\n");
    ret = oplk_init(NET_IF_NAME, MAIN_STATION_ID, OPLK_MN_MODE);
    if (ret != kErrorOk) {
        printf("Powerlink初始化失败,错误码: %d\n", ret);
        return ret;
    }

    // 2. 启动Powerlink栈
    printf("启动Powerlink栈...\n");
    ret = oplk_start();
    if (ret != kErrorOk) {
        printf("Powerlink栈启动失败,错误码: %d\n", ret);
        oplk_shutdown();
        return ret;
    }

    // 3. 等待主站与PLC从站建立连接(3秒超时)
    printf("等待连接PLC(Node ID: %d)...\n", PLC_NODE_ID);
    sleep(3);

    // 验证从站是否在线
    tOplkNodeInfo nodeInfo;
    ret = oplk_enumNodes(&nodeInfo, NULL);
    if (ret == kErrorOk && nodeInfo.nodeId == PLC_NODE_ID && nodeInfo.state == kNmtStateOperational) {
        printf("Powerlink连接成功!PLC已进入运行状态。\n");
        return 0;
    } else {
        printf("Powerlink连接PLC失败,PLC可能未在线或配置错误。\n");
        oplk_stop();
        oplk_shutdown();
        return -1;
    }
}

/**
 * @brief 通过SDO读取PLC数据(非实时,适用于参数读取)
 * @param data_buf 接收数据的缓冲区
 * @param buf_len 缓冲区长度(需与VAR_DATA_LEN一致)
 * @return 0=成功,非0=失败
 */
int read_powerlink_sdo(uint8_t* data_buf, uint32_t buf_len) {
    tOplkError ret;

    if (buf_len != VAR_DATA_LEN) {
        printf("SDO读取:缓冲区长度不匹配!\n");
        return -1;
    }

    // 调用openPOWERLINK的SDO读取接口(阻塞模式)
    ret = oplk_sdoClientDownload(
        PLC_NODE_ID,          // 目标PLC的Node ID
        VAR_OD_INDEX,         // OD索引
        VAR_OD_SUBINDEX,      // 子索引
        data_buf,             // 接收缓冲区
        buf_len,              // 数据长度
        kSdoClientBlocking    // 阻塞模式(直到读取完成)
    );

    if (ret != kErrorOk) {
        printf("SDO读取失败,错误码: %d\n", ret);
        return ret;
    }
    return 0;
}

/**
 * @brief PDO接收回调函数(实时数据触发,由Powerlink栈异步调用)
 * @param nodeId 发送数据的PLC Node ID
 * @param pData 接收的数据缓冲区
 * @param dataLen 数据长度
 */
void plc_pdo_receive_callback(uint16_t nodeId, uint8_t* pData, uint32_t dataLen) {
    if (nodeId != PLC_NODE_ID || dataLen != VAR_DATA_LEN) {
        return;  // 忽略非目标PLC或长度不匹配的数据
    }

    // 解析数据(根据VAR_DATA_TYPE调整)
    uint16_t value = 0;
    if (strcmp(VAR_DATA_TYPE, "UINT16") == 0) {
        value = (pData[0] << 8) | pData[1];  // 大端模式(需与PLC一致,不一致则交换字节)
    } else if (strcmp(VAR_DATA_TYPE, "INT16") == 0) {
        value = (int16_t)((pData[0] << 8) | pData[1]);
    }

    // 格式化MQTT消息(JSON格式,便于上位机解析)
    char mqtt_payload[128];
    snprintf(mqtt_payload, sizeof(mqtt_payload), 
             "{\"node_id\":%d,\"od_index\":\"0x%08X\",\"subindex\":\"0x%02X\",\"data_type\":\"%s\",\"value\":%d,\"timestamp\":%ld}",
             PLC_NODE_ID, VAR_OD_INDEX, VAR_OD_SUBINDEX, VAR_DATA_TYPE, value, time(NULL));

    // 发布MQTT消息(注意:回调函数中避免阻塞,若MQTT发布失败可记录日志)
    MQTTClient client = *(MQTTClient*)oplk_getUserContext();  // 从Powerlink上下文获取MQTT句柄
    if (client != NULL) {
        publish_mqtt_message(client, mqtt_payload);
    }
}

// -------------------------- 主函数 --------------------------
int main() {
    MQTTClient mqtt_client = NULL;
    uint8_t sdo_data[VAR_DATA_LEN] = {0};
    int ret = 0;

    // 1. 注册信号处理(捕获Ctrl+C)
    signal(SIGINT, signal_handler);

    // 2. 初始化MQTT客户端
    mqtt_client = init_mqtt_client();
    if (mqtt_client == NULL) {
        ret = -1;
        goto exit_program;
    }

    // 3. 初始化Powerlink主站
    // 将MQTT句柄存入Powerlink用户上下文(供PDO回调函数使用)
    oplk_setUserContext(&mqtt_client);
    ret = init_powerlink();
    if (ret != 0) {
        goto exit_program;
    }

    // 4. 注册PDO接收回调(实时读取用)
    ret = oplk_registerPdoRxCallback(plc_pdo_receive_callback);
    if (ret != kErrorOk) {
        printf("PDO回调注册失败,错误码: %d\n", ret);
        goto exit_program;
    }
    printf("PDO回调注册成功,开始接收实时数据...\n");

    // 5. 主循环(根据配置选择SDO或PDO读取)
    while (g_run_program) {
        // 方式1:SDO读取(非实时,按间隔发布)
        /*
        ret = read_powerlink_sdo(sdo_data, VAR_DATA_LEN);
        if (ret == 0) {
            uint16_t sdo_value = (sdo_data[0] << 8) | sdo_data[1];
            char payload[128];
            snprintf(payload, sizeof(payload), 
                     "{\"type\":\"SDO\",\"node_id\":%d,\"value\":%d,\"timestamp\":%ld}",
                     PLC_NODE_ID, sdo_value, time(NULL));
            publish_mqtt_message(mqtt_client, payload);
        }
        usleep(PUBLISH_INTERVAL * 1000);  // 毫秒转微秒
        */

        // 方式2:PDO读取(实时,回调函数自动发布,主循环仅保持程序运行)
        usleep(10000);  // 10ms延时,降低CPU占用
    }

exit_program:
    // 6. 资源释放(顺序:先关闭MQTT,再关闭Powerlink)
    if (mqtt_client != NULL) {
        MQTTClient_disconnect(mqtt_client, 1000);  // 1秒内断开连接
        MQTTClient_destroy(&mqtt_client);
        printf("MQTT客户端已关闭\n");
    }

    oplk_stop();
    oplk_shutdown();
    printf("Powerlink栈已关闭\n");

    return ret;
}

四、第三步:编译与运行

1. 编译命令(Linux)

需指定 openPOWERLINK 的头文件路径、库文件路径,以及链接 MQTT 库和 Powerlink 库。假设:

  • openPOWERLINK 源码目录:../openpowerlink-code
  • 应用程序代码:powerlink_mqtt.c
  • 生成可执行文件:powerlink_mqtt

编译命令:

bash

运行

复制代码
gcc powerlink_mqtt.c -o powerlink_mqtt \
-I../openpowerlink-code/include \  # openPOWERLINK头文件路径
-L../openpowerlink-code/build/lib/Release \  # openPOWERLINK库文件路径
-lpowerlink \  # 链接openPOWERLINK静态库
-lpaho-mqtt3c \  # 链接Paho MQTT C库
-lpcap \  # 链接libpcap库(Powerlink依赖)
-pthread  # 线程支持(Powerlink和MQTT均需)
2. 运行前准备
  1. 启动 MQTT Broker(本地 Mosquitto): bash

    运行

    复制代码
    sudo systemctl start mosquitto  # 启动服务
    sudo systemctl enable mosquitto  # 设置开机自启
  2. 验证 MQTT Broker 是否可用:用 MQTTX 连接tcp://localhost:1883,订阅主题plc/powerlink/data

  3. 确认 PLC 配置:PLC 已设为 Powerlink 从站(Node ID 与代码一致),变量已映射到 TxPDO(PDO 方式)或启用 SDO 访问(SDO 方式)。

3. 运行程序

bash

运行

复制代码
# 赋予执行权限
chmod +x powerlink_mqtt
# 运行程序
./powerlink_mqtt
4. 预期输出

plaintext

复制代码
正在连接MQTT Broker: tcp://localhost:1883...
MQTT连接成功!
正在初始化Powerlink主站...
启动Powerlink栈...
等待连接PLC(Node ID: 1)...
Powerlink连接成功!PLC已进入运行状态。
PDO回调注册成功,开始接收实时数据...
[MQTT发布] 主题: plc/powerlink/data, 消息: {"node_id":1,"od_index":"0x0E253A4E","subindex":"0x002E","data_type":"UINT16","value":1234,"timestamp":1730000000}
[MQTT发布] 主题: plc/powerlink/data, 消息: {"node_id":1,"od_index":"0x0E253A4E","subindex":"0x002E","data_type":"UINT16","value":1236,"timestamp":1730000001}
...

五、关键配置与避坑指南

1. 核心参数修改(必须适配你的场景)
  • PLC_NODE_ID:PLC 的 Powerlink Node ID(从 PLC 配置工具中获取,唯一);
  • VAR_OD_INDEX/VAR_OD_SUBINDEX:变量的索引和子索引(从 EDS 文件中提取);
  • VAR_DATA_LEN/VAR_DATA_TYPE:变量长度和类型(如FLOAT对应 4 字节,需修改数据解析逻辑);
  • MQTT_BROKER:若用云端 Broker(如阿里云 IoT),格式为tcp://xxx.xxx.xxx.xxx:1883,需填写用户名 / 密码。
2. 数据解析注意事项
  • 字节序:PLC 的 PDO/SDO 数据默认多为「大端模式」(高位字节在前),代码中UINT16解析用(pData[0] << 8) | pData[1];若 PLC 是小端模式,需改为(pData[1] << 8) | pData[0]

  • 浮点数解析:若变量是FLOAT(4 字节),用memcpy转换:

    c

    运行

    复制代码
    float float_value;
    memcpy(&float_value, pData, 4);
3. 常见错误排查
错误现象 排查方向
Powerlink 初始化失败 1. 网卡名称错误(NET_IF_NAME);2. 网卡被占用(如 Wireshark);3. openPOWERLINK 库编译错误;4. 未安装libpcap-dev
无法连接 PLC 1. Node ID 冲突;2. PLC 未设为从站;3. PLC 未重启生效配置;4. 网线 / 交换机不兼容
SDO 读取失败 1. 索引 / 子索引错误;2. 变量未启用 SDO 访问;3. 数据长度不匹配
PDO 无回调数据 1. 变量未映射到 PLC 的 TxPDO;2. PDO 映射的索引 / 子索引与代码不一致;3. 实时同步失败(换 Powerlink 交换机)
MQTT 连接失败 1. Broker 地址 / 端口错误;2. 用户名 / 密码错误;3. 防火墙拦截 1883 端口(云端需开放端口)
4. 实时性优化
  • 优先用 PDO 读取(周期可达 1ms 级),SDO 仅用于非实时参数;
  • 减少 MQTT 发布频率(如 10ms / 次),避免网络拥堵;
  • 选择支持硬件时间戳的网卡,编译 openPOWERLINK 时启用-DOPLK_USE_HW_TIMESTAMP=ON

六、Windows 系统适配说明

  1. 编译依赖:安装 MinGW-w64、WinPcap(Powerlink 依赖)、CMake;
  2. 编译 openPOWERLINK:将OPLK_TARGET_PLATFORM改为windowsOPLK_NETIF改为winpcap
  3. 编译 Paho MQTT:用 CMake 生成 MinGW Makefiles,编译后安装;
  4. 网卡名称:Windows 下NET_IF_NAME需填网卡的 NPF GUID(通过 Wireshark 查询);
  5. 编译命令:调整头文件 / 库文件路径为 Windows 格式(如-I"C:\openpowerlink-code\include")。

总结

通过「openPOWERLINK C 协议栈 + Paho MQTT C 库」,可实现 PLC 数据的实时读取与 MQTT 发布,适用于工业物联网(IIoT)场景。核心是:正确配置 Powerlink 主从站参数、确保索引 / 子索引匹配、处理好数据解析与 MQTT 连接稳定性

若需对接云端 IoT 平台(如阿里云、华为云),只需修改 MQTT Broker 地址和认证信息;若需读取多个变量,可扩展 PDO 映射表和数据解析逻辑。

相关推荐
smile_Iris1 小时前
Day 26 常见的降维算法
开发语言·算法·kotlin
刻刻帝的海角1 小时前
响应式数据可视化 Dashboard
开发语言·前端·javascript
王铁柱子哟-1 小时前
如何在 VS Code 中调试带参数和环境变量的 Python 程序
开发语言·python
weixin_307779131 小时前
Jenkins Bootstrap 5 API插件:现代化Jenkins界面的开发利器
开发语言·前端·网络·bootstrap·jenkins
青衫码上行1 小时前
【JavaWeb学习 | 第17篇】JSP内置对象
java·开发语言·前端·学习·jsp
今天吃饺子1 小时前
数据清洗APP重大更新!我用MATLAB写了一个数据清洗APP
开发语言·matlab
coderxiaohan1 小时前
【C++】map和set的使用
开发语言·c++
散峰而望1 小时前
Python 所需软件配置
开发语言·python
ChrisitineTX1 小时前
警惕数据“陷阱”:Python 如何自动发现并清洗 Excel 中的异常值?
开发语言·python·excel