核心目标:能基于 lib60870 开源库独立开发 IEC 104 Server(从站/RTU)和 Client(主站/调度),掌握回调机制、数据处理、双网冗余和嵌入式移植。

前置知识:Part 2 的帧格式(APCI/ASDU 结构),Part 3 的通信机制(连接/总召/确认),Part 4 的数据类型(TI/品质位/COT)。


5.1 开源方案选型

5.1.1 主流开源库对比

语言 许可证 Server Client 101 特点
lib60870 C GPL-3.0 / 商业 功能最完整,嵌入式首选
lib60870.NET C# GPL-3.0 / 商业 .NET 平台,Windows 友好
py60870 Python (C 绑定) 快速原型 / 自动化测试
j60870 Java Apache-2.0 跨平台后端集成

推荐:嵌入式/高性能 → lib60870 ©;原型验证 → py60870;Java 后端 → j60870。

5.1.2 lib60870 项目结构速览

lib60870/
├── lib60870-C/              # C 语言核心库
│   ├── src/
│   │   ├── inc/             # 头文件
│   │   │   ├── cs104_slave.h   # Server API
│   │   │   ├── cs104_master.h  # Client API
│   │   │   ├── cs101_information_elements.h  # 信息元素定义
│   │   │   └── hal_socket.h    # 平台抽象层 (Socket)
│   │   └── common/          # 源码实现
│   ├── examples/
│   │   ├── cs104_server.c   # Server 示例
│   │   └── cs104_client.c   # Client 示例
│   ├── config/              # 构建配置
│   └── Makefile
├── lib60870.NET/            # C# 实现
└── py60870/                 # Python 绑定

5.2 基于 lib60870 开发 Server(从站/RTU)

5.2.1 环境搭建

Linux (x86_64)

# 克隆源码
git clone https://github.com/mz-automation/lib60870.git
cd lib60870/lib60870-C

# 创建构建目录
mkdir build && cd build
cmake ..
make -j$(nproc)

# 安装到系统
sudo make install

ARM 交叉编译

# 以 aarch64 为例
mkdir build_arm && cd build_arm
cmake .. \
  -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
  -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
  -DCMAKE_SYSTEM_NAME=Linux \
  -DCMAKE_SYSTEM_PROCESSOR=aarch64
make -j$(nproc)

5.2.2 最小 Server 示例

先从最简单的 Server 开始——启动监听,响应总召唤和时钟同步,周期上送遥测:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include "cs104_slave.h"

/* ── 全局句柄 ── */
static CS104_Slave slave = NULL;
static bool running = true;

void sigint_handler(int signal) {
    running = false;
}

/* ───────────────────────────────────────────
 * 回调: 时钟同步
 * ─────────────────────────────────────────── */
static bool clock_sync_handler(void *parameter,
                                IMasterConnection connection,
                                CS101_ASDU asdu,
                                CP56Time2a new_time)
{
    printf("[对时] 收到时钟同步: %02d-%02d-%02d %02d:%02d:%02d.%03d\n",
           new_time.year + 2000, new_time.month, new_time.day,
           new_time.hour, new_time.minute, new_time.second,
           new_time.millisecond);

    /* 返回 true = 接受对时, 返回 false = 拒绝对时 */
    return true;
}

/* ───────────────────────────────────────────
 * 回调: 总召唤 (GI)
 * ─────────────────────────────────────────── */
static bool interrogation_handler(void *parameter,
                                   IMasterConnection connection,
                                   CS101_ASDU asdu,
                                   uint8_t qoi)
{
    printf("[总召] QOI=%d, 主站请求全量数据\n", qoi);

    if (qoi == 20) { /* 站召唤 (全局) */
        CS101_AppLayerParameters al_params =
            IMasterConnection_getApplicationLayerParameters(connection);
        IMasterConnection_sendACT_CON(connection, asdu, false);

        /* 上送遥信数据 (TI=1, 单点遥信) */
        CS101_ASDU newAsdu = CS101_ASDU_create(
            al_params, false, CS101_COT_INTERROGATED_BY_STATION,
            0, 1, false, false);

        /* 离散地址模式 (SQ=0), 一次发送多个点 */
        for (int ioa = 0x0001; ioa <= 0x0010; ioa++) {
            /* 从数据源读取实际遥信值 */
            int siq_val = get_remote_signal(ioa);  // 用户实现的读取函数
            InformationObject io = (InformationObject)
                SinglePointInformation_create(NULL, ioa, siq_val & 0x01,
                    (siq_val & 0x80) ? IEC60870_QUALITY_INVALID : IEC60870_QUALITY_GOOD);
            CS101_ASDU_addInformationObject(newAsdu, io);
            InformationObject_destroy(io);
        }

        IMasterConnection_sendASDU(connection, newAsdu);
        CS101_ASDU_destroy(newAsdu);

        /* 上送遥测数据 (TI=13, 短浮点) */
        CS101_ASDU measAsdu = CS101_ASDU_create(
            al_params, false, CS101_COT_INTERROGATED_BY_STATION,
            0, 1, false, false);

        for (int ioa = 0x0400; ioa <= 0x040F; ioa++) {
            float value = get_analog_value(ioa);  // 用户实现的读取函数
            int qds_val  = get_analog_quality(ioa);
            InformationObject io = (InformationObject)
                MeasuredValueShort_create(NULL, ioa, value,
                    qds_val & 0x80 ? IEC60870_QUALITY_INVALID : IEC60870_QUALITY_GOOD);
            CS101_ASDU_addInformationObject(measAsdu, io);
            InformationObject_destroy(io);
        }

        IMasterConnection_sendASDU(connection, measAsdu);
        CS101_ASDU_destroy(measAsdu);

        /* 总召唤结束 */
        IMasterConnection_sendACT_TERM(connection, asdu);
    } else {
        /* 分组召唤 */
        IMasterConnection_sendACT_CON(connection, asdu, true); /* 负确认 */
    }

    return true;
}

/* ───────────────────────────────────────────
 * 回调: 遥控命令 (ASDU 处理)
 * ─────────────────────────────────────────── */
static bool asdu_handler(void *parameter,
                          IMasterConnection connection,
                          CS101_ASDU asdu)
{
    if (CS101_ASDU_getTypeID(asdu) == C_SC_NA_1) {
        printf("[遥控] 收到单点遥控命令\n");

        /* 遍历信息对象 */
        int objCount = CS101_ASDU_getNumberOfElements(asdu);
        for (int i = 0; i < objCount; i++) {
            InformationObject io = CS101_ASDU_getElement(asdu, i);
            int ioa = InformationObject_getObjectAddress(io);

            if (CS101_ASDU_getCOT(asdu) == CS101_COT_ACTIVATION) {
                int sco = SingleCommand_getSCO((SingleCommand)io);
                bool isSelect = ((sco >> 1) & 0x01);  // SE 位
                bool isClose  = (sco & 0x01);          // SCS 位

                if (isSelect) {
                    printf("[遥控] IOA=0x%04X: 选择 %s\n", ioa,
                           isClose ? "合闸" : "分闸");
                    IMasterConnection_sendACT_CON(connection, asdu, false);
                } else {
                    printf("[遥控] IOA=0x%04X: 执行 %s\n", ioa,
                           isClose ? "合闸" : "分闸");
                    /* ★ 在这里执行实际的遥控操作 */
                    execute_control(ioa, isClose);  // 用户实现
                    IMasterConnection_sendACT_CON(connection, asdu, false);
                }
            }
        }
    }
    return true;
}

/* ───────────────────────────────────────────
 * 周期上送线程
 * ─────────────────────────────────────────── */
static void *cyclic_send_thread(void *arg)
{
    while (running) {
        /* 每秒上送一次遥测变化量 */
        Thread_sleep(1000);

        /* 遍历所有连接,向每个主站发送周期数据 */
        IMasterConnection connection = CS104_Slave_getActiveConnections(slave);
        while (connection != NULL) {
            CS101_AppLayerParameters al_params =
                IMasterConnection_getApplicationLayerParameters(connection);

            CS101_ASDU asdu = CS101_ASDU_create(
                al_params, false, CS101_COT_PERIODIC, 0, 1, false, false);

            /* 发送第一个遥测点 (IOA=0x0401) */
            float value = get_analog_value(0x0401);
            int quality  = get_analog_quality(0x0401);
            InformationObject io = (InformationObject)
                MeasuredValueShort_create(NULL, 0x0401, value,
                    IEC60870_QUALITY_GOOD);
            CS101_ASDU_addInformationObject(asdu, io);
            InformationObject_destroy(io);

            IMasterConnection_sendASDU(connection, asdu);
            CS101_ASDU_destroy(asdu);

            connection = IMasterConnection_getNextActiveConnection(
                slave, connection);
        }
    }
    return NULL;
}

/* ───────────────────────────────────────────
 * 主函数: 初始化并启动 Server
 * ─────────────────────────────────────────── */
int main(void)
{
    signal(SIGINT, sigint_handler);

    /* ① 创建 Server 实例 */
    slave = CS104_Slave_create(100, 100);  /* 最大 100 个 TCP 连接 */

    /* ② 配置连接参数 */
    CS104_Slave_setLocalAddress(slave, "0.0.0.0");
    CS104_Slave_setLocalPort(slave, 2404);

    /* ③ 设置应用层参数 (k/w/IOA长度/COT长度/公共地址长度) */
    CS101_AppLayerParameters al_params =
        CS104_Slave_getAppLayerParameters(slave);
    al_params->sizeOfIOA   = 2;   /* IOA = 2 字节 */
    al_params->sizeOfCOT   = 2;   /* COT = 2 字节 */
    al_params->sizeOfCA    = 2;   /* 公共地址 = 2 字节 */
    al_params->maxOutstandingIPDUs = 12;  /* k = 12 */
    al_params->maxSupplRespAfterPDU  = 8;   /* w = 8  */
    CS104_Slave_setAppLayerParameters(slave, al_params);

    /* ④ 设置超时参数 (t1, t2, t3, 单位: ms) */
    CS104_Slave_setConnectionTimeout(slave, 15000);       /* t1 = 15s */
    CS104_Slave_setAckTimeout(slave, 10000);              /* t2 = 10s */
    CS104_Slave_setTestTimeout(slave, 20000);             /* t3 = 20s */

    /* ⑤ 注册回调函数 */
    CS104_Slave_setClockSyncHandler(slave, clock_sync_handler, NULL);
    CS104_Slave_setInterrogationHandler(slave, interrogation_handler, NULL);
    CS104_Slave_setASDUHandler(slave, asdu_handler, NULL);

    /* ⑥ 启动 Server */
    CS104_Slave_start(slave);

    if (CS104_Slave_isRunning(slave)) {
        printf("IEC 104 Server 启动成功, 监听端口 2404\n");

        /* ⑦ 启动周期上送线程 */
        Thread cyclicThread = Thread_create(cyclic_send_thread, NULL, true);
        Thread_start(cyclicThread);

        while (running) {
            Thread_sleep(100);
        }

        Thread_destroy(cyclicThread);
    }

    /* ⑧ 停止并销毁 */
    CS104_Slave_stop(slave);
    CS104_Slave_destroy(slave);

    printf("Server 已停止\n");
    return 0;
}

5.2.3 编译与运行

# 编译 Server
gcc -o iec104_server server.c \
    -I/path/to/lib60870/lib60870-C/src/inc \
    -L/path/to/lib60870/lib60870-C/build \
    -l60870 -lpthread -lm

# 运行
./iec104_server

# 输出:
# IEC 104 Server 启动成功, 监听端口 2404

5.2.4 Server 回调全景图

CS104_Slave_setClockSyncHandler()

CS104_Slave_setInterrogationHandler()

CS104_Slave_setASDUHandler()

CS104_Slave_setConnectionEventHandler()

CS104_Slave_setRawMessageHandler()

CS104_Slave 主循环

① clockSyncHandler
收到时钟同步命令
→ 更新本地 RTC

② interrogationHandler
收到总召唤命令
→ 上送全量数据

③ asduHandler
收到遥控/遥调/设点等 ASDU
→ 执行控制命令

④ connectionHandler
连接建立/断开事件
→ 记录日志/触发告警

⑤ rawMessageHandler
底层报文拦截
→ 调试/记录

5.2.5 IOA 配置与数据存储设计

在实际工程中,数据存储需要和 IOA 映射。以下是推荐的数据结构设计:

/* ── 遥信点表定义 ── */
typedef struct {
    uint16_t ioa;       /* 信息对象地址 */
    char     name[32];  /* 点名称 */
    uint8_t  type;      /* 0=单点, 1=双点 */
    uint8_t  value;     /* 当前值 (含品质位) */
    CP56Time2a last_change_time; /* 最后变位时间 (SOE用) */
} YX_Point;

/* ── 遥测点表定义 ── */
typedef struct {
    uint16_t ioa;       /* 信息对象地址 */
    char     name[32];  /* 点名称 */
    float    value;     /* 当前值 (物理量) */
    uint8_t  quality;   /* 品质描述符 QDS */
    float    deadband;  /* 死区值 (变化量上送用) */
    float    last_sent_value;  /* 上次上送的值 */
} YC_Point;

/* ── 遥控点表定义 ── */
typedef struct {
    uint16_t ioa;       /* 信息对象地址 */
    char     name[32];
    uint8_t  state;     /* 当前状态 (0=分, 1=合) */
    uint8_t  select_state; /* 0=未选中, 1=已选中 */
    int      select_timeout; /* 选择超时计时器 (ms) */
} YK_Point;

/* ── 全局点表 ── */
#define MAX_YX  256
#define MAX_YC  512
#define MAX_YK  64

static YX_Point yx_table[MAX_YX];
static YC_Point yc_table[MAX_YC];
static YK_Point yk_table[MAX_YK];
static int yx_count = 0, yc_count = 0, yk_count = 0;

/* ── 从点表中查找 ── */
static YC_Point *find_yc_by_ioa(uint16_t ioa) {
    for (int i = 0; i < yc_count; i++) {
        if (yc_table[i].ioa == ioa) return &yc_table[i];
    }
    return NULL;
}

5.2.6 SOE 突发上送实现

当检测到遥信变位时,从站应立即构造带时标的 SOE 帧上送:

/* SOE 突发上送: 遥信变位时调用 */
void send_soe_event(CS104_Slave slave, uint16_t ioa,
                     uint8_t siq_value, CP56Time2a timestamp)
{
    IMasterConnection connection =
        CS104_Slave_getActiveConnections(slave);

    while (connection != NULL) {
        CS101_AppLayerParameters al_params =
            IMasterConnection_getApplicationLayerParameters(connection);

        /* 构造 M_SP_TB_1 (TI=30, 单点遥信 + CP56Time2a) */
        CS101_ASDU asdu = CS101_ASDU_create(
            al_params, false,
            CS101_COT_SPONTANEOUS,   /* COT=3, 突发 */
            0, 1, false, false);
        CS101_ASDU_setTypeID(asdu, M_SP_TB_1);

        InformationObject io = (InformationObject)
            SinglePointWithCP56Time2a_create(
                NULL, ioa,
                siq_value & 0x01,    /* SPI */
                (siq_value & 0x80) ? IEC60870_QUALITY_INVALID
                                   : IEC60870_QUALITY_GOOD,
                &timestamp);

        CS101_ASDU_addInformationObject(asdu, io);
        InformationObject_destroy(io);

        IMasterConnection_sendASDU(connection, asdu);
        CS101_ASDU_destroy(asdu);

        connection = IMasterConnection_getNextActiveConnection(
            slave, connection);
    }
}

5.3 基于 lib60870 开发 Client(主站/调度)

5.3.1 最小 Client 示例——连接 + 总召唤

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cs104_master.h"

static bool running = true;

/* ── 回调: 收到 ASDU 数据 ── */
static bool asdu_received_handler(void *parameter, CS101_ASDU asdu)
{
    int ti  = CS101_ASDU_getTypeID(asdu);
    int cot = CS101_ASDU_getCOT(asdu);
    int n   = CS101_ASDU_getNumberOfElements(asdu);

    printf("[收到] TI=%d, COT=%d, 对象数=%d\n", ti, cot, n);

    /* 根据 TI 类型解析信息对象 */
    switch (ti) {
    case M_SP_NA_1: /* TI=1, 单点遥信 */
        for (int i = 0; i < n; i++) {
            InformationObject io = CS101_ASDU_getElement(asdu, i);
            int ioa = InformationObject_getObjectAddress(io);
            SinglePointInformation sp = (SinglePointInformation)io;
            printf("  遥信: IOA=0x%04X, SCS=%d, IV=%d\n",
                   ioa,
                   SinglePointInformation_getValue(sp),
                   SinglePointInformation_getQuality(sp) == IEC60870_QUALITY_INVALID);
        }
        break;

    case M_ME_NC_1: /* TI=13, 短浮点遥测 */
        for (int i = 0; i < n; i++) {
            InformationObject io = CS101_ASDU_getElement(asdu, i);
            int ioa = InformationObject_getObjectAddress(io);
            MeasuredValueShort mv = (MeasuredValueShort)io;
            printf("  遥测: IOA=0x%04X, 值=%.2f, IV=%d\n",
                   ioa,
                   MeasuredValueShort_getValue(mv),
                   MeasuredValueShort_getQuality(mv) == IEC60870_QUALITY_INVALID);
        }
        break;

    case M_SP_TB_1: /* TI=30, SOE */
        for (int i = 0; i < n; i++) {
            InformationObject io = CS101_ASDU_getElement(asdu, i);
            int ioa = InformationObject_getObjectAddress(io);
            SinglePointWithCP56Time2a soe = (SinglePointWithCP56Time2a)io;
            CP56Time2a ts = SinglePointWithCP56Time2a_getTimestamp(soe);
            printf("  SOE: IOA=0x%04X, 值=%d, "
                   "时间=%02d-%02d-%02d %02d:%02d:%02d.%03d\n",
                   ioa,
                   SinglePointWithCP56Time2a_getValue(soe),
                   ts.year + 2000, ts.month, ts.day,
                   ts.hour, ts.minute, ts.second, ts.millisecond);
        }
        break;
    }
    return true;
}

/* ── 回调: 连接状态变化 ── */
static void connection_handler(void *parameter,
                                CS104_Master master,
                                CS104_MasterConnectionEvent event)
{
    switch (event) {
    case CS104_CON_EVENT_ACTIVE:
        printf("[连接] 连接已建立\n");
        break;
    case CS104_CON_EVENT_INACTIVE:
        printf("[连接] 连接已断开\n");
        break;
    case CS104_CON_EVENT_STARTDT_CON:
        printf("[连接] STARTDT 已确认, 可传输数据\n");
        break;
    }
}

/* ── 主函数 ── */
int main(void)
{
    /* ① 创建 Master 实例 */
    CS104_Master master = CS104_Master_create(NULL, NULL);

    /* ② 配置应用层参数 (必须与 Server 一致) */
    CS101_AppLayerParameters al_params =
        CS104_Master_getAppLayerParameters(master);
    al_params->sizeOfIOA = 2;
    al_params->sizeOfCOT = 2;
    al_params->sizeOfCA  = 2;

    /* ③ 注册回调 */
    CS104_Master_setASDUReceivedHandler(master, asdu_received_handler, NULL);
    CS104_Master_setConnectionHandler(master, connection_handler, NULL);

    /* ④ 连接 Server */
    printf("正在连接 IEC 104 Server...\n");
    CS104_Master_addSlave(master, "192.168.1.100", 2404);

    /* 等待连接建立 + STARTDT */
    Thread_sleep(3000);

    /* ⑤ 检查连接状态 */
    if (CS104_Master_isRunning(master)) {
        printf("连接成功! 开始总召唤...\n");

        /* 发送总召唤命令 */
        CS104_Master_sendInterrogationCommand(master, CS101_QOI_STATION);

        /* 等待总召唤完成 (实际应根据 COT=10 标记判断) */
        Thread_sleep(5000);
    }

    /* ⑥ 断开 */
    CS104_Master_removeSlave(master);
    CS104_Master_destroy(master);

    return 0;
}

5.3.2 时钟同步实现

/*
 * 时钟同步: 将主站的当前时间发送给从站
 */
void send_clock_sync(CS104_Master master)
{
    /* 获取当前时间 */
    time_t now = time(NULL);
    struct tm *tm_now = localtime(&now);

    /* 构造 CP56Time2a 时间戳 */
    CP56Time2a cp56_time;
    CP56Time2a_createFromMsTimestamp(
        &cp56_time,
        (uint64_t)now * 1000);  /* 秒 → 毫秒 */

    /* 发送时钟同步命令 (TI=103, COT=6) */
    CS104_Master_sendClockSyncCommand(master, &cp56_time);

    printf("[对时] 已发送时钟同步: %04d-%02d-%02d %02d:%02d:%02d\n",
           tm_now->tm_year + 1900,
           tm_now->tm_mon + 1,
           tm_now->tm_mday,
           tm_now->tm_hour,
           tm_now->tm_min,
           tm_now->tm_sec);
}

5.3.3 遥控命令——Select-Execute 完整实现

/*
 * 遥控: 完整的 Select-Execute 两阶段流程
 *
 * 返回值:
 *   0 = 成功
 *  -1 = 选择失败
 *  -2 = 执行失败
 */
int send_control_command(CS104_Master master,
                          uint16_t ioa,
                          bool close)  /* true=合闸, false=分闸 */
{
    /* ═════════════════════════════════════
     * 阶段 1: 选择 (Select)
     * ═════════════════════════════════════ */
    printf("[遥控] IOA=0x%04X: 开始选择...\n", ioa);

    /* 构造选择命令 (SE=1) */
    InformationObject select_io = (InformationObject)
        SingleCommand_create(NULL, ioa,
            close ? 0x01 : 0x00,  /* SCS: 1=合, 0=分 */
            true,                   /* SE=1 (选择) */
            0);                     /* QU=0 */

    if (!CS104_Master_sendControlCommand(master,
            CS101_COT_ACTIVATION,   /* COT=6 */
            1,                      /* 公共地址 */
            select_io)) {
        printf("[遥控] ★ 选择命令发送失败\n");
        InformationObject_destroy(select_io);
        return -1;
    }

    InformationObject_destroy(select_io);

    /* 等待选择确认 (COT=7)
     * 在实际工程中,应通过回调异步等待,
     * 这里简化为同步等待 */
    Thread_sleep(500);

    /* ═════════════════════════════════════
     * 阶段 2: 执行 (Execute)
     * ═════════════════════════════════════ */
    printf("[遥控] IOA=0x%04X: 开始执行...\n", ioa);

    /* 构造执行命令 (SE=0) */
    InformationObject exec_io = (InformationObject)
        SingleCommand_create(NULL, ioa,
            close ? 0x01 : 0x00,  /* SCS */
            false,                  /* SE=0 (执行) */
            0);                     /* QU=0 */

    if (!CS104_Master_sendControlCommand(master,
            CS101_COT_ACTIVATION,   /* COT=6 */
            1,
            exec_io)) {
        printf("[遥控] ★ 执行命令发送失败\n");
        InformationObject_destroy(exec_io);
        return -2;
    }

    InformationObject_destroy(exec_io);

    printf("[遥控] IOA=0x%04X: 遥控完成 (等待 SOE 确认)\n", ioa);
    return 0;
}

/* ── 使用示例 ── */
void control_example(CS104_Master master)
{
    /* 合闸: 断路器 IOA=0x0801 */
    send_control_command(master, 0x0801, true);

    Thread_sleep(2000);

    /* 分闸: 断路器 IOA=0x0801 */
    send_control_command(master, 0x0801, false);
}

5.3.4 Client-Server 交互全景

硬件 IO IEC 104 Server IEC 104 Client 调度应用 硬件 IO IEC 104 Server IEC 104 Client 调度应用 ▎建连 + STARTDT ▎时钟同步 ▎总召唤 ▎突发 SOE ▎遥控 TCP 连接 + STARTDT 对时请求 TI=103, COT=6, CP56Time2a TI=103, COT=7 (确认) 更新 RTC 总召请求 TI=100, COT=6, QOI=20 COT=7 (确认) 遥信数据 (COT=20) 遥测数据 (COT=20) COT=10 (终止) 数据回调 断路器变位 TI=30, COT=3 (SOE) SOE 回调 遥控合闸 (IOA=0x0801) TI=45, COT=6 (选择) COT=7 (选择确认) TI=45, COT=6 (执行) 驱动合闸继电器 COT=7 (执行确认) 断路器已合位 TI=30, COT=3 (SOE)

5.3.5 遥测死区变化量上送

/*
 * 周期上送: 仅上送变化超过死区的遥测值
 */
void send_cyclic_deadband(CS104_Slave slave)
{
    static CP56Time2a last_send_time;
    CP56Time2a now;
    CP56Time2a_createFromMsTimestamp(&now, currentTimeMillis());

    IMasterConnection connection =
        CS104_Slave_getActiveConnections(slave);

    while (connection != NULL) {
        CS101_AppLayerParameters al_params =
            IMasterConnection_getApplicationLayerParameters(connection);

        bool has_data = false;
        CS101_ASDU asdu = CS101_ASDU_create(
            al_params, false, CS101_COT_PERIODIC, 0, 1, false, false);
        CS101_ASDU_setTypeID(asdu, M_ME_NC_1);

        /* 遍历遥测点表,筛选变化量 */
        for (int i = 0; i < yc_count; i++) {
            float current   = yc_table[i].value;
            float last_sent = yc_table[i].last_sent_value;
            float deadband  = yc_table[i].deadband;

            /* 死区判断 */
            if (fabs(current - last_sent) >= deadband) {
                yc_table[i].last_sent_value = current;

                InformationObject io = (InformationObject)
                    MeasuredValueShort_create(NULL,
                        yc_table[i].ioa, current,
                        (yc_table[i].quality & 0x80) ?
                            IEC60870_QUALITY_INVALID :
                            IEC60870_QUALITY_GOOD);
                CS101_ASDU_addInformationObject(asdu, io);
                InformationObject_destroy(io);
                has_data = true;

                /* 避免单帧过大 (每帧最多 50 个点) */
                if (CS101_ASDU_getNumberOfElements(asdu) >= 50) {
                    break;
                }
            }
        }

        if (has_data) {
            IMasterConnection_sendASDU(connection, asdu);
        }
        CS101_ASDU_destroy(asdu);

        connection = IMasterConnection_getNextActiveConnection(
            slave, connection);
    }

    last_send_time = now;
}

5.4 数据处理实战

5.4.1 构造和解析各类数据速查

/* ══════════════════════════════════════════
 * 快速参考: 常用信息对象的创建与解析
 * ══════════════════════════════════════════ */

/* ── 单点遥信 (TI=1, M_SP_NA_1) ── */
/* 创建 */
InformationObject sp = (InformationObject)
    SinglePointInformation_create(NULL, ioa,
        value,      /* 0=分, 1=合 */
        quality);   /* IEC60870_QUALITY_GOOD / INVALID / ... */
/* 解析 */
int sp_val     = SinglePointInformation_getValue((SinglePointInformation)sp);
int sp_quality = SinglePointInformation_getQuality((SinglePointInformation)sp);

/* ── 双点遥信 (TI=3, M_DP_NA_1) ── */
/* 创建 */
InformationObject dp = (InformationObject)
    DoublePointInformation_create(NULL, ioa,
        value,      /* 0=不确定, 1=分, 2=合, 3=中间 */
        quality);
/* 解析 */
int dp_val = DoublePointInformation_getValue((DoublePointInformation)dp);

/* ── 短浮点遥测 (TI=13, M_ME_NC_1) ── */
/* 创建 */
InformationObject mv = (InformationObject)
    MeasuredValueShort_create(NULL, ioa,
        230.5,      /* IEEE 754 单精度浮点 */
        quality);
/* 解析 */
float mv_val = MeasuredValueShort_getValue((MeasuredValueShort)mv);

/* ── 单点遥信 SOE (TI=30, M_SP_TB_1) ── */
/* 创建 */
CP56Time2a ts;
CP56Time2a_createFromMsTimestamp(&ts, currentTimeMillis());
InformationObject soe = (InformationObject)
    SinglePointWithCP56Time2a_create(NULL, ioa, value, quality, &ts);
/* 解析 */
CP56Time2a soe_ts = SinglePointWithCP56Time2a_getTimestamp(
    (SinglePointWithCP56Time2a)soe);

/* ── 单点遥控 (TI=45, C_SC_NA_1) ── */
/* 创建 */
InformationObject cmd = (InformationObject)
    SingleCommand_create(NULL, ioa,
        scs,   /* 0=分, 1=合 */
        se,    /* true=选择, false=执行 */
        qu);   /* 命令限定词, 通常为 0 */
/* 解析 */
int sco_byte = SingleCommand_getSCO((SingleCommand)cmd);
int cmd_scs  = sco_byte & 0x01;
int cmd_se   = (sco_byte >> 1) & 0x01;

/* ── 短浮点设点 (TI=50, C_SE_NC_1) ── */
/* 创建 */
InformationObject sp_cmd = (InformationObject)
    SetpointCommandShort_create(NULL, ioa,
        100.5,      /* 目标值 */
        false,      /* SE=false (执行) */
        qu);        /* 限定词 */
/* 解析 */
float sp_val = SetpointCommandShort_getValue((SetpointCommandShort)sp_cmd);

5.4.2 品质位统一处理模块

/*
 * 品质位处理: 统一判断数据是否有效可用
 */

/* 品质位定义 (与标准一致) */
#define QUALITY_INVALID     0x80  /* IV: 无效 */
#define QUALITY_NON_TOPICAL 0x40  /* NT: 非当前值 */
#define QUALITY_SUBSTITUTED 0x20  /* SB: 人工置数 */
#define QUALITY_BLOCKED     0x10  /* BL: 被闭锁 */
#define QUALITY_OVERFLOW    0x0F  /* OV: 溢出 (遥测专用) */

/* 判断遥测值是否可信 */
const char *get_measurement_quality_label(uint8_t qds) {
    if (qds & QUALITY_INVALID)     return "无效";
    if (qds & QUALITY_NON_TOPICAL) return "非当前值";
    if (qds & QUALITY_SUBSTITUTED) return "人工置数";
    if (qds & QUALITY_BLOCKED)     return "被闭锁";
    if (qds & QUALITY_OVERFLOW)    return "溢出";
    return "正常";
}

/* 判断遥信值是否可信 */
const char *get_signal_quality_label(uint8_t siq) {
    if (siq & QUALITY_INVALID)     return "无效";
    if (siq & QUALITY_NON_TOPICAL) return "非当前值";
    if (siq & QUALITY_SUBSTITUTED) return "人工置数";
    if (siq & QUALITY_BLOCKED)     return "被闭锁";
    return "正常";
}

/*
 * 数据过滤: 在显示或存储前过滤无效数据
 */
bool is_measurement_valid(uint8_t qds) {
    /* 仅 IV=0 且 BL=0 时数据有效 */
    return !(qds & (QUALITY_INVALID | QUALITY_BLOCKED));
}

bool is_signal_valid(uint8_t siq) {
    return !(siq & (QUALITY_INVALID | QUALITY_BLOCKED));
}

5.4.3 CP56Time2a 时间处理

/*
 * CP56Time2a 工具函数
 */

/* 将 struct tm 转换为 CP56Time2a (不依赖 lib60870 API) */
void tm_to_cp56time2a(struct tm *tm, int ms, CP56Time2a *out) {
    memset(out, 0, sizeof(CP56Time2a));
    out->millisecond = ms;
    out->second      = tm->tm_sec;
    out->minute      = tm->tm_min;
    out->hour        = tm->tm_hour;
    out->day         = tm->tm_mday;
    out->month       = tm->tm_mon + 1;
    out->year        = tm->tm_year - 100;  /* 1900+ → 2000+ */
}

/* 手动解析 CP56Time2a 原始 7 字节 (不依赖 API) */
void parse_cp56time2a_raw(const uint8_t *data, struct tm *out_tm, int *out_ms) {
    int ms_low  = data[0];
    int ms_high = data[1] & 0x7F;
    *out_ms     = ms_low | (ms_high << 8);

    out_tm->tm_sec  = 0;
    out_tm->tm_min  = data[2] & 0x3F;
    out_tm->tm_hour = data[3] & 0x1F;
    out_tm->tm_mday = data[4] & 0x1F;
    out_tm->tm_mon  = (data[5] & 0x0F) - 1;  /* 1~12 → 0~11 */
    out_tm->tm_year = (data[6] & 0x7F) + 100; /* 2000+ → 1900+ */

    /* 有效性检查: bit7 of data[2] */
    if (data[2] & 0x80) {
        *out_ms = -1;  /* 时间无效标记 */
    }
}

5.5 双网冗余实现

5.5.1 Server 端双网监听

/*
 * Server 双网冗余: 同时监听两个网段
 *   - 网段 A: 192.168.1.0/24 (主网络)
 *   - 网段 B: 192.168.2.0/24 (备网络)
 */
int start_dual_network_server(void)
{
    CS104_Slave slave = CS104_Slave_create(10, 10);

    CS101_AppLayerParameters al_params =
        CS104_Slave_getAppLayerParameters(slave);
    al_params->sizeOfIOA = 2;
    al_params->sizeOfCOT = 2;
    al_params->sizeOfCA  = 2;

    /* ★ 关键: 绑定到 0.0.0.0, 监听所有网卡 */
    CS104_Slave_setLocalAddress(slave, "0.0.0.0");
    CS104_Slave_setLocalPort(slave, 2404);

    /* 回调注册 (与单网相同) */
    CS104_Slave_setInterrogationHandler(slave, gi_handler, NULL);
    CS104_Slave_setASDUHandler(slave, asdu_handler, NULL);
    CS104_Slave_setClockSyncHandler(slave, clock_handler, NULL);

    CS104_Slave_start(slave);
    printf("双网 Server 启动: 0.0.0.0:2404\n");
    printf("  网段 A: 192.168.1.100:2404\n");
    printf("  网段 B: 192.168.2.100:2404\n");

    /* 主循环 */
    while (running) {
        /* 发送数据时遍历所有连接 */
        IMasterConnection conn = CS104_Slave_getActiveConnections(slave);
        while (conn != NULL) {
            const char *peer_ip = IMasterConnection_getPeerAddress(conn);
            printf("  活动连接: %s\n", peer_ip);
            /* 向该连接发送数据 ... */
            conn = IMasterConnection_getNextActiveConnection(slave, conn);
        }
        Thread_sleep(5000);
    }

    CS104_Slave_stop(slave);
    CS104_Slave_destroy(slave);
    return 0;
}

5.5.2 Client 端双连接 + 温备切换

/*
 * Client 双连接 温备模式:
 *   - 主连接: 192.168.1.100
 *   - 备连接: 192.168.2.100
 *   - 主连接故障 → 自动切换备连接
 */

typedef enum {
    REDUN_STATE_PRIMARY,   /* 主连接工作中 */
    REDUN_STATE_BACKUP,    /* 备连接工作中 */
    REDUN_STATE_FAILOVER,  /* 正在切换中 */
} RedundantState;

typedef struct {
    CS104_Master master;
    char         primary_ip[32];
    char         backup_ip[32];
    int          port;
    RedundantState state;
    bool         primary_ok;
    bool         backup_ok;
} DualNetworkClient;

static DualNetworkClient redun_client;

/* 连接状态变化回调 */
static void redundant_connection_handler(void *parameter,
    CS104_Master master, CS104_MasterConnectionEvent event)
{
    DualNetworkClient *cli = (DualNetworkClient *)parameter;

    switch (event) {
    case CS104_CON_EVENT_ACTIVE:
        printf("[冗余] 连接建立\n");
        if (cli->state == REDUN_STATE_FAILOVER) {
            cli->state = REDUN_STATE_BACKUP;
        }
        break;

    case CS104_CON_EVENT_INACTIVE:
        printf("[冗余] ★ 连接断开, 触发切换\n");

        if (cli->state == REDUN_STATE_PRIMARY) {
            /* 主连接断开 → 切换备连接 */
            cli->state = REDUN_STATE_FAILOVER;
            cli->primary_ok = false;

            printf("[冗余] 主连接断开, 切换到备连接: %s\n",
                   cli->backup_ip);
            CS104_Master_addSlave(cli->master,
                cli->backup_ip, cli->port);
        }
        break;
    }
}

/* 初始化双网 Client */
void init_dual_network_client(const char *primary_ip,
                               const char *backup_ip, int port)
{
    strncpy(redun_client.primary_ip, primary_ip, 32);
    strncpy(redun_client.backup_ip,  backup_ip,  32);
    redun_client.port       = port;
    redun_client.state      = REDUN_STATE_PRIMARY;
    redun_client.primary_ok = false;
    redun_client.backup_ok  = false;

    redun_client.master = CS104_Master_create(NULL, NULL);

    CS104_Master_setConnectionHandler(redun_client.master,
        redundant_connection_handler, &redun_client);

    /* 先连接主站 */
    printf("[冗余] 连接主站: %s:%d\n", primary_ip, port);
    CS104_Master_addSlave(redun_client.master, primary_ip, port);
}

5.5.3 冗余连接状态机

连接主站

STARTDT 确认

正常通信

主连接断开

连接超时

连接备站

STARTDT 确认

正常通信

主连接恢复

备连接也断开

切回主连接

人工切换

INIT

PRIMARY

PRIMARY_OK

FAILOVER

BACKUP

BACKUP_OK

RECOVERY


5.6 性能优化与嵌入式移植

5.6.1 大数据量场景优化

场景: 5000 个遥测点 + 2000 个遥信点

策略:
  ① GI 时分批上送 (每帧 50~100 个点)  → 避免单帧过大
  ② 周期上送启用死区过滤            → 减少无效传输
  ③ 增大 k 值 (12 → 60)             → 增大发送窗口
  ④ 适当增大 t1/t2                  → 减少超时误判

分批 GI 实现

/*
 * 分批总召唤: GI 响应时每帧最多 max_per_frame 个点
 */
#define GI_MAX_PER_FRAME 80

void send_gi_data_batched(IMasterConnection connection,
                           CS101_ASDU request_asdu,
                           YC_Point *points, int count)
{
    CS101_AppLayerParameters al_params =
        IMasterConnection_getApplicationLayerParameters(connection);

    int sent = 0;
    while (sent < count) {
        int batch_size = (count - sent > GI_MAX_PER_FRAME)
                         ? GI_MAX_PER_FRAME : (count - sent);

        CS101_ASDU asdu = CS101_ASDU_create(
            al_params, false, CS101_COT_INTERROGATED_BY_STATION,
            0, 1, false, false);

        /* ★ SQ=1: 顺序地址模式, 节省 (batch_size-1) × IOA长度 字节 */
        for (int i = sent; i < sent + batch_size; i++) {
            InformationObject io = (InformationObject)
                MeasuredValueShort_create(NULL,
                    points[i].ioa,
                    points[i].value,
                    IEC60870_QUALITY_GOOD);
            CS101_ASDU_addInformationObject(asdu, io);
            InformationObject_destroy(io);
        }

        IMasterConnection_sendASDU(connection, asdu);
        CS101_ASDU_destroy(asdu);

        sent += batch_size;

        /* 小幅延时, 避免网络拥塞 */
        Thread_sleep(5);
    }
}

5.6.2 内存管理优化

/*
 * 内存策略对比:
 * ┌──────────┬─────────────────┬─────────────────┐
 * │  策略     │  优点            │  缺点            │
 * ├──────────┼─────────────────┼─────────────────┤
 * │ 动态分配 │ 灵活, 按需使用    │ 碎片, malloc 开销 │
 * │ 内存池   │ 无碎片, 快速      │ 固定大小, 浪费   │
 * │ 栈分配   │ 最快, 自动回收    │ 大小受限         │
 * └──────────┴─────────────────┴─────────────────┘
 *
 * 推荐: 通信帧使用内存池, 点表使用动态分配
 */

/* 内存池示例: 预分配 I 帧缓冲区 */
#define MAX_I_FRAMES  64
#define MAX_FRAME_LEN 256

typedef struct {
    uint8_t data[MAX_FRAME_LEN];
    bool    in_use;
} FrameBuffer;

static FrameBuffer frame_pool[MAX_I_FRAMES];
static pthread_mutex_t pool_mutex = PTHREAD_MUTEX_INITIALIZER;

FrameBuffer *frame_alloc(void) {
    pthread_mutex_lock(&pool_mutex);
    for (int i = 0; i < MAX_I_FRAMES; i++) {
        if (!frame_pool[i].in_use) {
            frame_pool[i].in_use = true;
            memset(frame_pool[i].data, 0, MAX_FRAME_LEN);
            pthread_mutex_unlock(&pool_mutex);
            return &frame_pool[i];
        }
    }
    pthread_mutex_unlock(&pool_mutex);
    return NULL;  /* 池已满 */
}

void frame_free(FrameBuffer *fb) {
    pthread_mutex_lock(&pool_mutex);
    fb->in_use = false;
    pthread_mutex_unlock(&pool_mutex);
}

5.6.3 ARM Linux 交叉编译完整流程

# ═════════════════════════════════════════
# 目标: aarch64 (ARM64) Linux, 如树莓派 4B
# ═════════════════════════════════════════

# 1. 安装交叉编译工具链
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu

# 2. 编译 lib60870
cd lib60870/lib60870-C
mkdir build_arm64 && cd build_arm64

cmake .. \
  -DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc \
  -DCMAKE_CXX_COMPILER=aarch64-linux-gnu-g++ \
  -DCMAKE_SYSTEM_NAME=Linux \
  -DCMAKE_SYSTEM_PROCESSOR=aarch64 \
  -DCMAKE_BUILD_TYPE=Release \
  -DBUILD_TESTS=OFF \
  -DBUILD_EXAMPLES=OFF

make -j$(nproc)

# 3. 编译你的 Server 程序
aarch64-linux-gnu-gcc -o iec104_server_arm \
  my_server.c \
  -I/path/to/lib60870/lib60870-C/src/inc \
  -L/path/to/lib60870/lib60870-C/build_arm64 \
  -l60870 -lpthread -lm \
  -static   # ★ 静态链接, 避免缺少 .so

# 4. 查看生成的文件信息
file iec104_server_arm
# iec104_server_arm: ELF 64-bit LSB executable, ARM aarch64, ...

# 5. 部署到目标机
scp iec104_server_arm root@192.168.1.200:/opt/rtu/

# 6. 在目标机上运行
ssh root@192.168.1.200
chmod +x /opt/rtu/iec104_server_arm
/opt/rtu/iec104_server_arm

5.6.4 连接参数调优速查

/*
 * 不同场景的参数推荐配置
 */

typedef struct {
    int w;     /* 最大未确认 I 帧数 */
    int k;     /* 最大接收 I 帧数 */
    int t1;    /* 发送确认超时 (ms) */
    int t2;    /* 确认间隔超时 (ms) */
    int t3;    /* 测试帧间隔 (ms) */
} ConnectionTuning;

static ConnectionTuning tuning_table[] = {
    /* 场景              w    k    t1(ms)  t2(ms)   t3(ms)  */
    [0] = { 12,  8,  15000, 10000,  20000 },  /* 默认 */
    [1] = { 24, 16,  10000,  5000,  15000 },  /* 局域网低延迟 */
    [2] = { 24, 16,  30000, 20000,  60000 },  /* 广域网高延迟 */
    [3] = { 60, 32,  15000, 10000,  30000 },  /* 大量遥测点 */
    [4] = {  8,  8,  60000, 30000, 120000 },  /* 卫星/4G 通道 */
};

void apply_connection_tuning(CS104_Slave slave, int scenario)
{
    ConnectionTuning *t = &tuning_table[scenario];

    CS101_AppLayerParameters al_params =
        CS104_Slave_getAppLayerParameters(slave);
    al_params->maxOutstandingIPDUs = t->w;
    al_params->maxSupplRespAfterPDU = t->k;
    CS104_Slave_setAppLayerParameters(slave, al_params);

    CS104_Slave_setConnectionTimeout(slave, t->t1);
    CS104_Slave_setAckTimeout(slave, t->t2);
    CS104_Slave_setTestTimeout(slave, t->t3);
}

5.7 完整工程架构

调度层

网络层

协议层

数据层

驱动层

硬件层

硬件 IO
DI/DO/AI/AO

IO 驱动
SPI / I2C / Modbus

点表管理
YX / YC / YK / YT / YM
├ 值缓存
├ SOE 缓冲区
└ 品质位管理

IEC 104 Server
(lib60870)
├ APCI/ASDU 帧构造
├ 回调: GI/对时/遥控
└ 周期上送 + SOE 突发

双网冗余
├ 网段 A (主)
└ 网段 B (备)

调度主站
IEC 104 Client
├ SCADA 系统
└ 调度员工作站


5.8 开发检查清单

检查项 说明 状态
IOA 长度配置 Server 和 Client 的 sizeOfIOA 必须一致
COT 长度配置 2 字节 COT (sizeOfCOT=2) 是主流
公共地址配置 sizeOfCA=1 或 2, 双方一致
k/w 参数 w ≤ k, 非平衡配比不宜过大
回调线程安全 GI/ASDU 回调中不要做阻塞操作
遥控 Select 超时 选择后 15~30s 未执行应自动取消
品质位处理 IV=1 时忽略值, BL=1 时告警
SOE 缓冲区 避免变位事件丢失
连接断开重连 指数退避, 最大间隔不超过 60s
双网测试 拔网线测试主备切换是否正常
时钟同步 连接后 + 周期对时必须实现
死区配置 遥测死区合理, 避免数据风暴或漏报
日志记录 关键事件(连接/总召/遥控)必须有日志

5.9 小结

知识点 掌握程度 核心要点
lib60870 项目结构 理解 lib60870-C 核心库, 各语言绑定
Server 开发流程 熟练 创建→配置参数→注册回调→启动监听
GI 回调处理 掌握 四阶段: 确认→遥信→遥测→终止
ASDU 回调 (遥控处理) 掌握 遍历信息对象, 区分 Select/Execute
SOE 突发上送 掌握 TI=30, COT=3, 带 CP56Time2a
Client 开发流程 熟练 创建→配置→连接→总召→解析
遥控 Select-Execute 掌握 SE=1 选择 → SE=0 执行, 两阶段防误
品质位处理 掌握 IV=1 忽略, BL=1 告警, SB=1 标记
死区变化量上送 理解 变化量 ≥ 死区 → 上送
双网冗余 (Server) 掌握 bind 0.0.0.0, 遍历所有连接发送
双网冗余 (Client) 掌握 温备模式: 断连→切换→GI
ARM 交叉编译 掌握 CMake toolchain + 静态链接
连接参数调优 理解 5 种场景的 w/k/t1-t3 配置

下期预告

[Part 6:工程实战] 将覆盖从开发到投运的完整链路:

  • 系统集成流程(设备接入→点表配置→通信联调→对点测试→投运)
  • 数据点表设计与管理(Excel → 配置自动生成)
  • 网络架构与冗余设计
  • Wireshark 深度排障(7 种典型故障案例分析)
  • IEC 62351 安全加固
  • 典型工程案例(配电站接入、双网切换、61850↔104 网关)

参考资源

  • lib60870 官方仓库:https://github.com/mz-automation/lib60870
  • lib60870 API 文档:https://www.mz-automation.de/communication/lib60870/
  • IEC 60870-5-104 标准:第 9 节(互操作性配置)

推荐工具

  • CMake + GCC/Clang —— 编译构建
  • aarch64-linux-gnu-gcc —— ARM 交叉编译
  • Wireshark —— 验证 Server/Client 通信报文
  • Valgrind —— 内存泄漏检查(valgrind --leak-check=full ./iec104_server
  • strace —— 跟踪系统调用(排查 Socket 相关问题)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐