源码加更05_订阅表、Retain、PUBLISH 路由和业务事件

源码加更05_订阅表、Retain、PUBLISH 路由和业务事件

这一组源码加更只有一个目标:把 MqttBroker 的真实 ST 源码按工程阅读顺序讲完整。不是再补几段“看起来像源码”的片段,而是让读者能沿着源码对象理解这个 Broker 怎么组织、怎么运行、怎么排障。

适合谁收藏

  • 已经读过 MqttBroker 主线教程,想继续看真实源码实现的工程师。
  • 想学习 CodeSys ST 工程如何拆分 Broker、连接池、编解码、路由和 QoS 调度的人。
  • 想把 MQTT Broker 移植到 PLC、边缘控制器或教学工程里的开发者。

源码加更05_订阅表、Retain、PUBLISH 路由和业务事件

先给结论

这一篇看订阅表和路由器如何协同:订阅进入表,Retain 更新缓存,PUBLISH 按 Topic Filter 匹配 fanout,最后把消息送进连接发送队列。

SUBSCRIBE 不是保存一个字符串,PUBLISH 也不是收到就群发。Broker 的核心价值在于把订阅关系、通配符匹配、Retain 回放和业务事件组织成可控路由。

这篇覆盖 13 个源码文件,合计约 819 行 ST 代码。为了保持公开教程可读性,正文先讲源码阅读路径,再给完整源码。读代码时建议不要从第一个代码块一路机械读到底,而是按本篇的“读代码顺序”来抓主线。

从工程问题到代码职责

层次本篇重点你读源码时要抓住的判断
工程入口程序如何启动、对象如何被实例化先确认谁是入口,谁只是被调度的对象
数据边界容量、状态、错误、缓冲区和表结构先知道边界,后面排障才不会乱猜
协作关系各 FB、函数和结构体如何互相传递数据不按文件夹读,按数据流和状态流读
验证路径在线观察应该看哪些变量代码最终要能落到现场排障,而不是只停在源码阅读

本篇源码覆盖表

序号源码对象行数
1FB_MqttBroker.M_HandlePublish.st62
2FB_MqttBroker.M_HandleSubscribe.st94
3FB_MqttBroker.M_HandleUnsubscribe.st41
4FB_MqttBroker.M_RoutePublishNow.st75
5FB_MqttBrokerRouter.M_AddSubscription.st66
6FB_MqttBrokerRouter.M_ClearSlot.st41
7FB_MqttBrokerRouter.M_FindNextRetain.st77
8FB_MqttBrokerRouter.M_FindNextRoute.st63
9FB_MqttBrokerRouter.M_RemoveSubscription.st47
10FB_MqttBrokerRouter.M_UpdateRetain.st86
11FB_MqttBrokerRouter.st21
12F_MqttMinQoS.st21
13F_MqttTopicMatch.st125

推荐阅读顺序

  • 先看 Router 主体和订阅增删。
  • 再看 Retain 查询与更新。
  • 最后看 Broker 顶层如何处理 Publish/Subscribe/Unsubscribe 业务事件。

验证和排障边界

  • 测试精确主题、+# 三类订阅。
  • 测试 Retain 首次订阅回放,以及取消订阅后不再接收。

本篇完整开源代码

下面代码来自对应 .st 源文件的连续完整内容。为方便公开阅读,只保留源码对象名,不放本机工程路径。

完整代码 01: FB_MqttBroker.M_HandlePublish.st

/// =======================================================================
/// 名称      : M_HandlePublish
/// 功能      : 处理一条入站或 Will PUBLISH
/// 说明      : 统一完成入站事务、PUBACK、Retain 更新和订阅路由投递。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_HandlePublish : BOOL
VAR_INPUT
    uiSourceSlot : UINT; // 发布来源槽位编号,Will 或系统内部发布也使用原异常槽位[1..cnMaxClientSlots]
    stPublish    : ST_MqttBrokerPublishFrame; // 需要处理的发布帧
END_VAR

// === IMPLEMENTATION ===
IF NOT stPublish.xValid THEN
    M_HandlePublish := FALSE;
    RETURN;
END_IF

IF (uiSourceSlot >= 1) AND (uiSourceSlot <= GVL_MqttBroker.cnMaxClientSlots) THEN
    IF NOT M_CheckPublishAcl(uiSlot := uiSourceSlot, sTopic := stPublish.sTopic) THEN
        stMetrics.udiAclRejected := stMetrics.udiAclRejected + 1;
        M_LogDiag(uiSlot := uiSourceSlot, eError := E_MqttBrokerError.uiInvalidTopic, sMessage := 'Publish topic rejected by ACL');
        M_HandlePublish := FALSE;
        RETURN;
    END_IF
END_IF

stMetrics.udiPublishReceived := stMetrics.udiPublishReceived + 1;

IF stPublish.eQoS = E_MqttQoS.byQoS1 THEN
    IF fbRxScheduler.M_RecordPublish(stPublish := stPublish, udiNowMs := udiNowMs) THEN
        IF (uiSourceSlot >= 1) AND (uiSourceSlot <= GVL_MqttBroker.cnMaxClientSlots) THEN
            aConnections[uiSourceSlot].M_EnqueueProtocolAck(
                ePacketType := E_MqttPacketType.byPubAck,
                uiPacketId := stPublish.uiPacketId,
                byReturnCode := 0);
            fbRxScheduler.M_CompletePublish(uiSlot := uiSourceSlot, uiPacketId := stPublish.uiPacketId);
            stMetrics.udiPubAckSent := stMetrics.udiPubAckSent + 1;
        END_IF
    END_IF
END_IF

IF stPublish.eQoS = E_MqttQoS.byQoS2 THEN
    IF fbRxScheduler.M_RecordPublish(stPublish := stPublish, udiNowMs := udiNowMs) THEN
        IF (uiSourceSlot >= 1) AND (uiSourceSlot <= GVL_MqttBroker.cnMaxClientSlots) THEN
            aConnections[uiSourceSlot].M_EnqueueProtocolAck(
                ePacketType := E_MqttPacketType.byPubRec,
                uiPacketId := stPublish.uiPacketId,
                byReturnCode := 0);
            stMetrics.udiPubRecSent := stMetrics.udiPubRecSent + 1;
        END_IF
    END_IF
    M_HandlePublish := TRUE;
    RETURN;
END_IF

M_RoutePublishNow(stPublish := stPublish);

M_HandlePublish := TRUE;

完整代码 02: FB_MqttBroker.M_HandleSubscribe.st

/// =======================================================================
/// 名称      : M_HandleSubscribe
/// 功能      : 处理连接槽位输出的多 Topic SUBSCRIBE
/// 说明      : 每个 Topic Filter 独立写订阅表、生成 SUBACK 返回码,并触发 Retain 补发。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_HandleSubscribe : BOOL
VAR_INPUT
    uiSlot       : UINT; // 发起 SUBSCRIBE 的客户端槽位编号[1..cnMaxClientSlots]
END_VAR
VAR
    uiItemIndex  : UINT; // SUBSCRIBE 多 Topic 条目扫描索引[1..cnMaxTopicItemsPerPacket]
    uiRetainReplayCount : UINT; // 本次 SUBSCRIBE 已补发的 Retain 消息数量[条]
    aReturnCodes : ARRAY[1..GVL_MqttBroker.cnMaxTopicItemsPerPacket] OF BYTE; // SUBACK 多 Topic 返回码数组
    uiReturnCount : UINT; // SUBACK 返回码数量[条]
END_VAR

// === IMPLEMENTATION ===
IF (uiSlot < 1) OR (uiSlot > GVL_MqttBroker.cnMaxClientSlots) THEN
    M_HandleSubscribe := FALSE;
    RETURN;
END_IF

uiReturnCount := 0;
uiRetainReplayCount := 0;

FOR uiItemIndex := 1 TO aConnections[uiSlot].uiSubItemCount DO
    IF uiItemIndex > GVL_MqttBroker.cnMaxTopicItemsPerPacket THEN
        EXIT;
    END_IF

    uiReturnCount := uiReturnCount + 1;

    IF aConnections[uiSlot].aSubItems[uiItemIndex].xUsed
        AND M_CheckSubscribeAcl(uiSlot := uiSlot, sTopicFilter := aConnections[uiSlot].aSubItems[uiItemIndex].sTopicFilter)
        AND fbRouter.M_AddSubscription(
            uiSlot := uiSlot,
            sClientId := aConnectionStates[uiSlot].sClientId,
            sTopicFilter := aConnections[uiSlot].aSubItems[uiItemIndex].sTopicFilter,
            eMaxQoS := aConnections[uiSlot].aSubItems[uiItemIndex].eQoS) THEN
        aReturnCodes[uiReturnCount] := TO_BYTE(aConnections[uiSlot].aSubItems[uiItemIndex].eQoS);

        IF fbRouter.M_FindNextRetain(
            stDelivery := stDelivery,
            uiTargetSlot := uiSlot,
            sTopicFilter := aConnections[uiSlot].aSubItems[uiItemIndex].sTopicFilter,
            eMaxQoS := aConnections[uiSlot].aSubItems[uiItemIndex].eQoS,
            xRestart := TRUE,
            xFound => xRetainFound) THEN
            WHILE xRetainFound AND (uiRetainReplayCount < GVL_MqttBroker.cnMaxRetainReplayPerScan) DO
                IF aConnections[uiSlot].M_EnqueueDelivery(stDelivery := stDelivery) THEN
                    stMetrics.udiPublishDelivered := stMetrics.udiPublishDelivered + 1;
                    sLastRetainTopic := stDelivery.sTopic;
                    uiLastRetainPayloadLen := stDelivery.uiPayloadLen;
                    uiRetainReplayCount := uiRetainReplayCount + 1;

                    IF stDelivery.eQoS <> E_MqttQoS.byQoS0 THEN
                        // Retain 补发本质上也是 Broker 到订阅者方向的出站 PUBLISH。
                        // 当补发 QoS>0 时,必须登记出站事务,否则订阅者后续 PUBACK/PUBREC 无法正确收口。
                        fbTxScheduler.M_RegisterPublish(stPublish := stDelivery, udiNowMs := udiNowMs);
                    END_IF
                END_IF

                fbRouter.M_FindNextRetain(
                    stDelivery := stDelivery,
                    uiTargetSlot := uiSlot,
                    sTopicFilter := aConnections[uiSlot].aSubItems[uiItemIndex].sTopicFilter,
                    eMaxQoS := aConnections[uiSlot].aSubItems[uiItemIndex].eQoS,
                    xRestart := FALSE,
                    xFound => xRetainFound);
            END_WHILE

            IF xRetainFound THEN
                // 默认预算等于 Retain 表容量,因此正常配置下不会漏发。
                // 如果工程师为了扫描周期把 cnMaxRetainReplayPerScan 调小,本诊断能明确提示本次订阅仍有 Retain 未在同一扫描周期补发。
                M_LogDiag(uiSlot := uiSlot, eError := E_MqttBrokerError.uiQueueFull, sMessage := 'Retain replay budget exhausted');
            END_IF
        END_IF
    ELSE
        aReturnCodes[uiReturnCount] := 16#80;
        stMetrics.udiAclRejected := stMetrics.udiAclRejected + 1;
    END_IF
END_FOR

aConnections[uiSlot].M_EnqueueProtocolAckList(
    aReturnCodes := aReturnCodes,
    ePacketType := E_MqttPacketType.bySubAck,
    uiPacketId := aConnections[uiSlot].uiSubPacketId,
    uiReturnCount := uiReturnCount);

M_HandleSubscribe := TRUE;

完整代码 03: FB_MqttBroker.M_HandleUnsubscribe.st

/// =======================================================================
/// 名称      : M_HandleUnsubscribe
/// 功能      : 处理连接槽位输出的多 Topic UNSUBSCRIBE
/// 说明      : 每个 Topic Filter 独立从订阅表删除,最后返回一个 UNSUBACK。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_HandleUnsubscribe : BOOL
VAR_INPUT
    uiSlot      : UINT; // 发起 UNSUBSCRIBE 的客户端槽位编号[1..cnMaxClientSlots]
END_VAR
VAR
    uiItemIndex : UINT; // UNSUBSCRIBE 多 Topic 条目扫描索引[1..cnMaxTopicItemsPerPacket]
END_VAR

// === IMPLEMENTATION ===
IF (uiSlot < 1) OR (uiSlot > GVL_MqttBroker.cnMaxClientSlots) THEN
    M_HandleUnsubscribe := FALSE;
    RETURN;
END_IF

FOR uiItemIndex := 1 TO aConnections[uiSlot].uiUnsubItemCount DO
    IF uiItemIndex > GVL_MqttBroker.cnMaxTopicItemsPerPacket THEN
        EXIT;
    END_IF

    IF aConnections[uiSlot].aUnsubItems[uiItemIndex].xUsed THEN
        fbRouter.M_RemoveSubscription(
            uiSlot := uiSlot,
            sTopicFilter := aConnections[uiSlot].aUnsubItems[uiItemIndex].sTopicFilter);
    END_IF
END_FOR

aConnections[uiSlot].M_EnqueueProtocolAck(
    ePacketType := E_MqttPacketType.byUnsubAck,
    uiPacketId := aConnections[uiSlot].uiUnsubPacketId,
    byReturnCode := 0);

M_HandleUnsubscribe := TRUE;

完整代码 04: FB_MqttBroker.M_RoutePublishNow.st

/// =======================================================================
/// 名称      : M_RoutePublishNow
/// 功能      : 对已确认可路由的发布帧执行 Retain 与订阅路由
/// 说明      : QoS0/QoS1 直接调用;QoS2 仅在首次 PUBREL 时调用,保证 exactly once。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_RoutePublishNow : BOOL
VAR_INPUT
    stPublish    : ST_MqttBrokerPublishFrame; // 已确认允许执行路由的发布帧
END_VAR
VAR
    uiRouteCount : UINT; // 本次发布已经生成的路由投递数量
    stRouteFrame : ST_MqttBrokerPublishFrame; // 路由器返回的单条投递任务
END_VAR

// === IMPLEMENTATION ===
IF NOT stPublish.xValid THEN
    M_RoutePublishNow := FALSE;
    RETURN;
END_IF

IF stPublish.xRetain THEN
    IF fbRouter.M_UpdateRetain(stPublish := stPublish, udiNowMs := udiNowMs) THEN
        sLastRetainTopic := stPublish.sTopic;
        uiLastRetainPayloadLen := stPublish.uiPayloadLen;
        uiRetainCount := fbRouter.uiRetainCount;
        IF stPublish.uiPayloadLen = 0 THEN
            stMetrics.udiRetainCleared := stMetrics.udiRetainCleared + 1;
        ELSE
            stMetrics.udiRetainUpdated := stMetrics.udiRetainUpdated + 1;
        END_IF
    END_IF
END_IF

uiRouteCount := 0;

IF fbRouter.M_FindNextRoute(
    stDelivery := stRouteFrame,
    stPublish := stPublish,
    xRestart := TRUE,
    xFound => xRouteFound) THEN
    WHILE xRouteFound AND (uiRouteCount < GVL_MqttBroker.cnMaxRouteFanoutPerScan) DO
        IF (stRouteFrame.uiTargetSlot >= 1) AND (stRouteFrame.uiTargetSlot <= GVL_MqttBroker.cnMaxClientSlots) THEN
            IF aConnections[stRouteFrame.uiTargetSlot].M_EnqueueDelivery(stDelivery := stRouteFrame) THEN
                stMetrics.udiPublishDelivered := stMetrics.udiPublishDelivered + 1;
                uiRouteCount := uiRouteCount + 1;

                IF stRouteFrame.eQoS <> E_MqttQoS.byQoS0 THEN
                    // M_EnqueueDelivery 会为首次 QoS>0 投递分配“目标连接内”的 PacketId。
                    // 事务表必须登记这个已分配后的 PacketId,后续订阅者 PUBACK 才能正确清理事务。
                    fbTxScheduler.M_RegisterPublish(stPublish := stRouteFrame, udiNowMs := udiNowMs);
                END_IF
            ELSE
                stMetrics.udiPublishDropped := stMetrics.udiPublishDropped + 1;
            END_IF
        END_IF

        fbRouter.M_FindNextRoute(
            stDelivery := stRouteFrame,
            stPublish := stPublish,
            xRestart := FALSE,
            xFound => xRouteFound);
    END_WHILE

    IF xRouteFound THEN
        // 路由扇出预算耗尽时保留诊断,不把它伪装成“没有订阅者”。
        // 当前轻量实现不缓存剩余路由任务,默认 cnMaxRouteFanoutPerScan 与客户端规模一致,正常 5~8 客户端不会丢投。
        M_LogDiag(uiSlot := stPublish.uiSourceSlot, eError := E_MqttBrokerError.uiQueueFull, sMessage := 'Route fanout budget exhausted');
    END_IF
END_IF

M_RoutePublishNow := TRUE;

完整代码 05: FB_MqttBrokerRouter.M_AddSubscription.st

/// =======================================================================
/// 名称      : M_AddSubscription
/// 功能      : 新增或更新订阅表项
/// 说明      : 同一槽位同一 Topic Filter 重复订阅时更新 QoS,而不是新增重复项。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_AddSubscription : BOOL
VAR_INPUT
    uiSlot       : UINT; // 订阅所属客户端槽位编号[1..cnMaxClientSlots]
    sClientId    : STRING; // 订阅所属 ClientID 快照
    sTopicFilter : STRING; // 客户端请求的 Topic Filter
    eMaxQoS      : E_MqttQoS; // 客户端请求的最大 QoS
END_VAR
VAR
    uiIndex      : UINT; // 订阅表扫描索引
    uiFreeIndex  : UINT; // 首个空闲订阅表槽位索引
END_VAR

// === IMPLEMENTATION ===
IF (uiSlot < 1) OR (uiSlot > GVL_MqttBroker.cnMaxClientSlots) THEN
    eLastError := E_MqttBrokerError.uiInvalidState;
    M_AddSubscription := FALSE;
    RETURN;
END_IF

IF NOT F_MqttIsValidTopicFilter(sFilter := sTopicFilter) THEN
    eLastError := E_MqttBrokerError.uiInvalidTopic;
    M_AddSubscription := FALSE;
    RETURN;
END_IF

uiFreeIndex := 0;

FOR uiIndex := 1 TO GVL_MqttBroker.cnMaxSubscriptions DO
    IF aSubscriptions[uiIndex].xUsed THEN
        IF (aSubscriptions[uiIndex].uiSlot = uiSlot) AND (aSubscriptions[uiIndex].sTopicFilter = sTopicFilter) THEN
            aSubscriptions[uiIndex].eMaxQoS := eMaxQoS;
            aSubscriptions[uiIndex].xActive := TRUE;
            eLastError := E_MqttBrokerError.uiNoError;
            M_AddSubscription := TRUE;
            RETURN;
        END_IF
    ELSIF uiFreeIndex = 0 THEN
        uiFreeIndex := uiIndex;
    END_IF
END_FOR

IF uiFreeIndex = 0 THEN
    eLastError := E_MqttBrokerError.uiSubscriptionFull;
    M_AddSubscription := FALSE;
    RETURN;
END_IF

aSubscriptions[uiFreeIndex].xUsed := TRUE;
aSubscriptions[uiFreeIndex].xActive := TRUE;
aSubscriptions[uiFreeIndex].uiSlot := uiSlot;
aSubscriptions[uiFreeIndex].sClientId := sClientId;
aSubscriptions[uiFreeIndex].sTopicFilter := sTopicFilter;
aSubscriptions[uiFreeIndex].uiFilterLen := TO_UINT(LEN(sTopicFilter));
aSubscriptions[uiFreeIndex].eMaxQoS := eMaxQoS;
uiSubscriptionCount := uiSubscriptionCount + 1;
eLastError := E_MqttBrokerError.uiNoError;
M_AddSubscription := TRUE;

完整代码 06: FB_MqttBrokerRouter.M_ClearSlot.st

/// =======================================================================
/// 名称      : M_ClearSlot
/// 功能      : 清理指定连接槽位的订阅资源
/// 说明      : Clean Session 或连接释放时调用,避免断线客户端继续收到路由任务。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_ClearSlot : BOOL
VAR_INPUT
    uiSlot : UINT; // 需要清理的客户端槽位编号[1..cnMaxClientSlots]
END_VAR
VAR
    uiIndex : UINT; // 订阅表扫描索引
END_VAR

// === IMPLEMENTATION ===
IF (uiSlot < 1) OR (uiSlot > GVL_MqttBroker.cnMaxClientSlots) THEN
    eLastError := E_MqttBrokerError.uiInvalidState;
    M_ClearSlot := FALSE;
    RETURN;
END_IF

FOR uiIndex := 1 TO GVL_MqttBroker.cnMaxSubscriptions DO
    IF aSubscriptions[uiIndex].xUsed AND (aSubscriptions[uiIndex].uiSlot = uiSlot) THEN
        aSubscriptions[uiIndex].xUsed := FALSE;
        aSubscriptions[uiIndex].xActive := FALSE;
        aSubscriptions[uiIndex].uiSlot := 0;
        aSubscriptions[uiIndex].sClientId := '';
        aSubscriptions[uiIndex].sTopicFilter := '';
        aSubscriptions[uiIndex].uiFilterLen := 0;
        aSubscriptions[uiIndex].eMaxQoS := E_MqttQoS.byQoS0;
        IF uiSubscriptionCount > 0 THEN
            uiSubscriptionCount := uiSubscriptionCount - 1;
        END_IF
    END_IF
END_FOR

eLastError := E_MqttBrokerError.uiNoError;
M_ClearSlot := TRUE;

完整代码 07: FB_MqttBrokerRouter.M_FindNextRetain.st

/// =======================================================================
/// 名称      : M_FindNextRetain
/// 功能      : 查找新订阅命中的下一条 Retain 消息
/// 说明      : SUBSCRIBE 成功后循环调用,生成需要补发给订阅者的 Retain 投递任务。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_FindNextRetain : BOOL
VAR_INPUT
    uiTargetSlot   : UINT; // 新订阅客户端槽位编号[1..cnMaxClientSlots]
    sTopicFilter   : STRING; // 新订阅的 Topic Filter
    eMaxQoS        : E_MqttQoS; // 新订阅请求的最大 QoS
    xRestart       : BOOL; // TRUE 表示从 Retain 表起点重新扫描
END_VAR
VAR_IN_OUT
    stDelivery     : ST_MqttBrokerPublishFrame; // 返回给调度层的 Retain 投递任务
END_VAR
VAR_OUTPUT
    xFound         : BOOL; // TRUE 表示本次找到一条命中 Retain 消息
END_VAR
VAR
    uiIndex        : UINT; // Retain 表扫描索引
END_VAR

// === IMPLEMENTATION ===
xFound := FALSE;

IF xRestart THEN
    uiScanIndex := 1;
END_IF

IF (uiTargetSlot < 1) OR (uiTargetSlot > GVL_MqttBroker.cnMaxClientSlots) THEN
    eLastError := E_MqttBrokerError.uiInvalidState;
    M_FindNextRetain := FALSE;
    RETURN;
END_IF

IF NOT F_MqttIsValidTopicFilter(sFilter := sTopicFilter) THEN
    eLastError := E_MqttBrokerError.uiInvalidTopic;
    M_FindNextRetain := FALSE;
    RETURN;
END_IF

IF uiScanIndex < 1 THEN
    uiScanIndex := 1;
END_IF

FOR uiIndex := uiScanIndex TO GVL_MqttBroker.cnMaxRetainedMessages DO
    uiScanIndex := uiIndex + 1;

    // 与普通路由一样,Retain 匹配也必须先确认表项有效,再调用 Topic 匹配函数。
    // 这能避免运行时非短路求值导致空 Retain 主题参与匹配。
    IF aRetained[uiIndex].xUsed THEN
        IF F_MqttTopicMatch(sFilter := sTopicFilter, sTopic := aRetained[uiIndex].sTopic) THEN
            stDelivery.xValid := TRUE;
            stDelivery.uiSourceSlot := 0;
            stDelivery.uiTargetSlot := uiTargetSlot;
            stDelivery.uiPacketId := 0;
            stDelivery.eQoS := F_MqttMinQoS(ePublishQoS := aRetained[uiIndex].eQoS, eMaxQoS := eMaxQoS);
            stDelivery.xDup := FALSE;
            stDelivery.xRetain := TRUE;
            stDelivery.uiTopicLen := aRetained[uiIndex].uiTopicLen;
            stDelivery.uiPayloadLen := aRetained[uiIndex].uiPayloadLen;
            stDelivery.sTopic := aRetained[uiIndex].sTopic;
            stDelivery.sPayload := aRetained[uiIndex].sPayload;
            xFound := TRUE;
            eLastError := E_MqttBrokerError.uiNoError;
            M_FindNextRetain := TRUE;
            RETURN;
        END_IF
    END_IF
END_FOR

eLastError := E_MqttBrokerError.uiNoError;
M_FindNextRetain := TRUE;

完整代码 08: FB_MqttBrokerRouter.M_FindNextRoute.st

/// =======================================================================
/// 名称      : M_FindNextRoute
/// 功能      : 查找发布消息的下一条订阅匹配结果
/// 说明      : 外部循环调用本方法生成投递任务,每次最多返回一条,避免一次扫描阻塞扫描周期。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_FindNextRoute : BOOL
VAR_INPUT
    stPublish      : ST_MqttBrokerPublishFrame; // 需要路由的发布帧
    xRestart       : BOOL; // TRUE 表示从订阅表起点重新扫描
END_VAR
VAR_IN_OUT
    stDelivery     : ST_MqttBrokerPublishFrame; // 返回给调度层的投递任务
END_VAR
VAR_OUTPUT
    xFound         : BOOL; // TRUE 表示本次找到一条匹配订阅
END_VAR
VAR
    uiIndex        : UINT; // 订阅表扫描索引
END_VAR

// === IMPLEMENTATION ===
xFound := FALSE;
stDelivery := stPublish;

IF xRestart THEN
    uiScanIndex := 1;
END_IF

IF NOT stPublish.xValid THEN
    M_FindNextRoute := FALSE;
    RETURN;
END_IF

IF uiScanIndex < 1 THEN
    uiScanIndex := 1;
END_IF

FOR uiIndex := uiScanIndex TO GVL_MqttBroker.cnMaxSubscriptions DO
    uiScanIndex := uiIndex + 1;

    // IEC ST 不要求 AND 必须短路求值,Topic 匹配这类带字符串下标的函数必须放到最内层。
    // 这样未使用或未激活的订阅不会进入 F_MqttTopicMatch,避免空字符串/旧数据干扰路由。
    // MQTT 3.1.1 没有 noLocal 订阅选项,发布者如果自己也订阅了同一主题,也应该收到 Broker 回投。
    IF aSubscriptions[uiIndex].xUsed THEN
        IF aSubscriptions[uiIndex].xActive THEN
            IF F_MqttTopicMatch(sFilter := aSubscriptions[uiIndex].sTopicFilter, sTopic := stPublish.sTopic) THEN
                stDelivery.uiTargetSlot := aSubscriptions[uiIndex].uiSlot;
                stDelivery.uiPacketId := 0;
                stDelivery.eQoS := F_MqttMinQoS(ePublishQoS := stPublish.eQoS, eMaxQoS := aSubscriptions[uiIndex].eMaxQoS);
                stDelivery.xDup := FALSE;
                xFound := TRUE;
                M_FindNextRoute := TRUE;
                RETURN;
            END_IF
        END_IF
    END_IF
END_FOR

M_FindNextRoute := TRUE;

完整代码 09: FB_MqttBrokerRouter.M_RemoveSubscription.st

/// =======================================================================
/// 名称      : M_RemoveSubscription
/// 功能      : 删除指定槽位的订阅表项
/// 说明      : UNSUBSCRIBE 成功解析后调用,按槽位和 Topic Filter 精确删除。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_RemoveSubscription : BOOL
VAR_INPUT
    uiSlot       : UINT; // 取消订阅所属客户端槽位编号[1..cnMaxClientSlots]
    sTopicFilter : STRING; // 需要删除的 Topic Filter
END_VAR
VAR
    uiIndex      : UINT; // 订阅表扫描索引
END_VAR

// === IMPLEMENTATION ===
IF (uiSlot < 1) OR (uiSlot > GVL_MqttBroker.cnMaxClientSlots) THEN
    eLastError := E_MqttBrokerError.uiInvalidState;
    M_RemoveSubscription := FALSE;
    RETURN;
END_IF

FOR uiIndex := 1 TO GVL_MqttBroker.cnMaxSubscriptions DO
    IF aSubscriptions[uiIndex].xUsed
        AND (aSubscriptions[uiIndex].uiSlot = uiSlot)
        AND (aSubscriptions[uiIndex].sTopicFilter = sTopicFilter) THEN
        aSubscriptions[uiIndex].xUsed := FALSE;
        aSubscriptions[uiIndex].xActive := FALSE;
        aSubscriptions[uiIndex].uiSlot := 0;
        aSubscriptions[uiIndex].sClientId := '';
        aSubscriptions[uiIndex].sTopicFilter := '';
        aSubscriptions[uiIndex].uiFilterLen := 0;
        aSubscriptions[uiIndex].eMaxQoS := E_MqttQoS.byQoS0;
        IF uiSubscriptionCount > 0 THEN
            uiSubscriptionCount := uiSubscriptionCount - 1;
        END_IF
        eLastError := E_MqttBrokerError.uiNoError;
        M_RemoveSubscription := TRUE;
        RETURN;
    END_IF
END_FOR

eLastError := E_MqttBrokerError.uiNoError;
M_RemoveSubscription := TRUE;

完整代码 10: FB_MqttBrokerRouter.M_UpdateRetain.st

/// =======================================================================
/// 名称      : M_UpdateRetain
/// 功能      : 更新或清除 Retain 保留消息
/// 说明      : Retain PUBLISH 载荷为空时清除对应 Topic,否则新增或覆盖保存。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_UpdateRetain : BOOL
VAR_INPUT
    stPublish      : ST_MqttBrokerPublishFrame; // 收到的 Retain PUBLISH 标准发布帧
    udiNowMs       : ULINT; // 当前系统时间戳[ms]
END_VAR
VAR
    uiIndex        : UINT; // Retain 表扫描索引
    uiFreeIndex    : UINT; // 首个空闲 Retain 表槽位索引
END_VAR

// === IMPLEMENTATION ===
IF NOT stPublish.xRetain THEN
    M_UpdateRetain := TRUE;
    RETURN;
END_IF

IF NOT F_MqttIsValidTopicName(sTopic := stPublish.sTopic) THEN
    eLastError := E_MqttBrokerError.uiInvalidTopic;
    M_UpdateRetain := FALSE;
    RETURN;
END_IF

uiFreeIndex := 0;

FOR uiIndex := 1 TO GVL_MqttBroker.cnMaxRetainedMessages DO
    IF aRetained[uiIndex].xUsed THEN
        IF aRetained[uiIndex].sTopic = stPublish.sTopic THEN
            IF stPublish.uiPayloadLen = 0 THEN
                aRetained[uiIndex].xUsed := FALSE;
                aRetained[uiIndex].sTopic := '';
                aRetained[uiIndex].sPayload := '';
                aRetained[uiIndex].uiTopicLen := 0;
                aRetained[uiIndex].uiPayloadLen := 0;
                aRetained[uiIndex].eQoS := E_MqttQoS.byQoS0;
                aRetained[uiIndex].udiUpdatedAtMs := udiNowMs;
                IF uiRetainCount > 0 THEN
                    uiRetainCount := uiRetainCount - 1;
                END_IF
            ELSE
                aRetained[uiIndex].eQoS := stPublish.eQoS;
                aRetained[uiIndex].uiTopicLen := stPublish.uiTopicLen;
                aRetained[uiIndex].uiPayloadLen := stPublish.uiPayloadLen;
                aRetained[uiIndex].udiUpdatedAtMs := udiNowMs;
                aRetained[uiIndex].sTopic := stPublish.sTopic;
                aRetained[uiIndex].sPayload := stPublish.sPayload;
            END_IF
            eLastError := E_MqttBrokerError.uiNoError;
            M_UpdateRetain := TRUE;
            RETURN;
        END_IF
    ELSIF uiFreeIndex = 0 THEN
        uiFreeIndex := uiIndex;
    END_IF
END_FOR

IF stPublish.uiPayloadLen = 0 THEN
    eLastError := E_MqttBrokerError.uiNoError;
    M_UpdateRetain := TRUE;
    RETURN;
END_IF

IF uiFreeIndex = 0 THEN
    eLastError := E_MqttBrokerError.uiRetainFull;
    M_UpdateRetain := FALSE;
    RETURN;
END_IF

aRetained[uiFreeIndex].xUsed := TRUE;
aRetained[uiFreeIndex].eQoS := stPublish.eQoS;
aRetained[uiFreeIndex].uiTopicLen := stPublish.uiTopicLen;
aRetained[uiFreeIndex].uiPayloadLen := stPublish.uiPayloadLen;
aRetained[uiFreeIndex].udiUpdatedAtMs := udiNowMs;
aRetained[uiFreeIndex].sTopic := stPublish.sTopic;
aRetained[uiFreeIndex].sPayload := stPublish.sPayload;
uiRetainCount := uiRetainCount + 1;
eLastError := E_MqttBrokerError.uiNoError;
M_UpdateRetain := TRUE;

完整代码 11: FB_MqttBrokerRouter.st

/// =======================================================================
/// 名称      : FB_MqttBrokerRouter
/// 功能      : MQTT Broker 主题路由与 Retain 管理
/// 说明      : 维护全局订阅表和 Retain 表,只生成投递任务,不直接执行 TCP 发送。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
FUNCTION_BLOCK FB_MqttBrokerRouter
VAR_OUTPUT
    uiSubscriptionCount : UINT; // 当前全局有效订阅数量
    uiRetainCount       : UINT; // 当前 Retain 表有效消息数量
    eLastError          : E_MqttBrokerError; // 路由器最近一次错误码
END_VAR
VAR
    aSubscriptions : ARRAY[1..GVL_MqttBroker.cnMaxSubscriptions] OF ST_MqttBrokerSubscription; // 全局订阅表
    aRetained      : ARRAY[1..GVL_MqttBroker.cnMaxRetainedMessages] OF ST_MqttBrokerRetainedMessage; // 全局 Retain 表
    uiScanIndex    : UINT; // 路由扫描时使用的订阅表索引
END_VAR
// === IMPLEMENTATION ===

完整代码 12: F_MqttMinQoS.st

/// =======================================================================
/// 名称      : F_MqttMinQoS
/// 功能      : 选择发布 QoS 与订阅最大 QoS 中较低等级
/// 说明      : MQTT Broker 投递时不能超过订阅者请求的最大 QoS。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
FUNCTION F_MqttMinQoS : E_MqttQoS
VAR_INPUT
    ePublishQoS : E_MqttQoS; // 发布者原始 PUBLISH QoS 等级
    eMaxQoS     : E_MqttQoS; // 订阅者在 SUBSCRIBE 中请求的最大 QoS 等级
END_VAR

// === IMPLEMENTATION ===
IF TO_BYTE(ePublishQoS) <= TO_BYTE(eMaxQoS) THEN
    F_MqttMinQoS := ePublishQoS;
ELSE
    F_MqttMinQoS := eMaxQoS;
END_IF

完整代码 13: F_MqttTopicMatch.st

/// =======================================================================
/// 名称      : F_MqttTopicMatch
/// 功能      : MQTT Topic Filter 与 Topic Name 匹配
/// 说明      : 支持 + 单层通配符和 # 多层通配符,用于 Broker 路由扫描订阅表。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-08
/// 版本      : V1.1
/// =======================================================================
{attribute 'hide_all_locals'}
FUNCTION F_MqttTopicMatch : BOOL
VAR_INPUT
    sFilter : STRING; // 订阅表中的 Topic Filter,可包含 + 或 #
    sTopic  : STRING; // PUBLISH 报文中的 Topic Name,不允许包含通配符
END_VAR
VAR
    uiFilterLen  : UINT; // Topic Filter 长度[byte]
    uiTopicLen   : UINT; // Topic Name 长度[byte]
    uiFilterPos  : UINT; // 当前检查的 Topic Filter 字符位置,CodeSys/CODESYS STRING 下标从 0 开始[byte]
    uiTopicPos   : UINT; // 当前检查的 Topic Name 字符位置,CodeSys/CODESYS STRING 下标从 0 开始[byte]
    byFilterChar : BYTE; // 当前 Topic Filter 字符
    byTopicChar  : BYTE; // 当前 Topic Name 字符
END_VAR

// === IMPLEMENTATION ===
IF NOT F_MqttIsValidTopicFilter(sFilter := sFilter) THEN
    F_MqttTopicMatch := FALSE;
    RETURN;
END_IF

IF NOT F_MqttIsValidTopicName(sTopic := sTopic) THEN
    F_MqttTopicMatch := FALSE;
    RETURN;
END_IF

uiFilterLen := TO_UINT(LEN(sFilter));
uiTopicLen := TO_UINT(LEN(sTopic));
uiFilterPos := 0;
uiTopicPos := 0;

// 关键坑位:
// CodeSys/CODESYS 的 STRING 字符访问按 0 基下标工作,和很多工程师直觉中的 1 基字符串不同。
// CONNECT 协议名、SUBSCRIBE Topic Filter、PUBLISH Topic/Payload 都会经过这类字符串函数。
// 如果这里按 1 基访问,现象通常不是立即编译失败,而是订阅路由错位、发布后客户端被协议错误断开。
WHILE (uiFilterPos < uiFilterLen) AND (uiTopicPos < uiTopicLen) DO
    byFilterChar := sFilter[uiFilterPos];
    byTopicChar := sTopic[uiTopicPos];

    CASE byFilterChar OF
        16#23:
            // # 匹配当前层级以及后续所有层级;合法性已由 F_MqttIsValidTopicFilter 保证。
            F_MqttTopicMatch := TRUE;
            RETURN;

        16#2B:
            // + 匹配一个完整层级:跳过 Topic 中直到下一个 / 之前的所有字符。
            WHILE uiTopicPos < uiTopicLen DO
                IF sTopic[uiTopicPos] = 16#2F THEN
                    EXIT;
                END_IF
                uiTopicPos := uiTopicPos + 1;
            END_WHILE

            uiFilterPos := uiFilterPos + 1;

            IF uiFilterPos < uiFilterLen THEN
                IF sFilter[uiFilterPos] = 16#2F THEN
                    IF uiTopicPos < uiTopicLen THEN
                        IF sTopic[uiTopicPos] = 16#2F THEN
                            uiFilterPos := uiFilterPos + 1;
                            uiTopicPos := uiTopicPos + 1;
                        ELSE
                            F_MqttTopicMatch := FALSE;
                            RETURN;
                        END_IF
                    ELSE
                        F_MqttTopicMatch := FALSE;
                        RETURN;
                    END_IF
                END_IF
            END_IF

        16#2F:
            IF byTopicChar <> 16#2F THEN
                F_MqttTopicMatch := FALSE;
                RETURN;
            END_IF
            uiFilterPos := uiFilterPos + 1;
            uiTopicPos := uiTopicPos + 1;
    ELSE
            IF byFilterChar <> byTopicChar THEN
                F_MqttTopicMatch := FALSE;
                RETURN;
            END_IF
            uiFilterPos := uiFilterPos + 1;
            uiTopicPos := uiTopicPos + 1;
    END_CASE
END_WHILE

IF (uiFilterPos = uiFilterLen) AND (uiTopicPos = uiTopicLen) THEN
    F_MqttTopicMatch := TRUE;
ELSE
    F_MqttTopicMatch := FALSE;

    // IEC ST 的 AND 在部分运行时不保证像高级语言一样短路求值。
    // 因此带 STRING 下标访问的边界判断必须拆成嵌套 IF,不能写成一个长布尔表达式。
    IF uiFilterPos < uiFilterLen THEN
        IF sFilter[uiFilterPos] = 16#23 THEN
            F_MqttTopicMatch := TRUE;
        ELSIF sFilter[uiFilterPos] = 16#2B THEN
            IF (uiFilterPos + 1) = uiFilterLen THEN
                // + 可以匹配一个空层级,例如 sport/+ 应匹配 sport/。
                F_MqttTopicMatch := TRUE;
            END_IF
        ELSIF uiTopicPos = uiTopicLen THEN
            IF (uiFilterPos + 1) < uiFilterLen THEN
                IF sFilter[uiFilterPos] = 16#2F THEN
                    IF sFilter[uiFilterPos + 1] = 16#23 THEN
                        // MQTT 通配规则允许 sport/# 匹配 sport;这里处理 Topic 已结束但 Filter 还剩 /# 的场景。
                        F_MqttTopicMatch := TRUE;
                    END_IF
                END_IF
            END_IF
        END_IF
    END_IF
END_IF

这一篇你最该记住的几句话

  • Broker 源码不要按“文件夹顺序”读,要按“入口、状态、数据、报文、路由、事务”读。
  • CodeSys ST 工程最容易失控的不是语法,而是对象职责边界混乱。
  • 只要你能把本篇源码对象和在线变量对应起来,后续排查连接、订阅、发布和 QoS 问题就不会乱。

系列导航

  • 第 1 篇:源码加更01_Broker 工程入口、容量边界和数据模型
  • 第 2 篇:源码加更02_FB_MqttBroker 顶层调度、连接池和权限边界
  • 第 3 篇:源码加更03_单连接槽位、TCP 字节流和发送队列
  • 第 4 篇:源码加更04_MQTT 编解码器和字节工具函数
  • 第 5 篇:源码加更05_订阅表、Retain、PUBLISH 路由和业务事件
  • 第 6 篇:源码加更06_QoS 事务调度、重试和生产级闭环
代码下载地址: https://pan.quark.cn/s/a4b39357ea24 在计算机视觉技术中,数据集扮演着训练评估模型的核心角色。Labelme作为一个广受欢迎的开源工具,能够支持用户以交互方式对图像进行标注,而COCO(Common Objects in Context)则是一种被广泛采纳的数据集标准格式,适用于包括物体检测、图像分割在内的多种任务。本文将详细阐述如何将Labelme生成的标注数据转换为COCO数据集的标准格式。 Labelme标注的图像在输出为JSON格式时,会包含以下核心内容: 1. `version`: 指明JSON文件的版本信息。 2. `flags`: 目前未定义或保持为空,预留用于未来的功能扩展。 3. `shapes`: 列形式存储对象的形状信息,每个形状项包含`label`(对象类别名称),`points`(构成对象边缘的多边形顶点),以及`shape_type`(通常为“polygon”)。 4. `imagePath``imageData`: 提供原始图像的存储路径二进制数据,便于后续图像的还原。 5. `imageHeight``imageWidth`: 明确标注图像的垂直水平尺寸。 COCO数据集的标准格式中定义了三种主要的标注类型: 1. Object instances(目标实例):主要用于执行物体检测任务。 2. Object keypoints(目标上的关键点):适用于人体姿态估计相关应用。 3. Image captions(看图说话):用于生成图像的文本描述。 COCO的JSON结构中包含以下基本组成部分: 1. `images`:记录图像的基本属性,包括`height`(高度)、`...
内容概要:本文围绕基于Basisformer模型的时间序列锂离子电池SOC(State of Charge,荷电状态)预测展开研究,利用PyTorch深度学习框架构建并训练模型,旨在提升锂电池SOC估计的准确性与鲁棒性。该方法融合Transformer架构的核心机制,通过引入基函数(Basis)分解策略,有效捕捉电池充放电过程中长时序、非线性动态特征,增强模型对复杂工况的适应能力。研究不仅详细阐述了Basisformer的网络结构设计、注意力机制优化与训练流程,还提供了完整的Python代码实现方案,涵盖数据预处理、模型搭建、损失函数定义、训练验证及结果可视化等环节,便于科研人员快速复现、调优并拓展至其他电池状态预测任务。; 适合人群:具备一定深度学习与Python编程基础,熟悉PyTorch框架,从事电池管理系统(BMS)、新能源汽车、储能系统、智能传感等领域的高校研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于动力电池与储能系统的实时SOC估算模块,提升系统安全性与能量利用效率;②作为学术研究的基础模型,用于复现、改进基于Transformer的时间序列预测方法在电化学系统中的应用;③为数据驱动的电池健康状态(SOH)、剩余使用寿命(RUL)联合估计提供可扩展的技术框架。; 阅读建议:建议读者结合所提供的代码与公开电池数据集(如NASA、CALCE等)进行动手实践,深入理解模型的输入输出结构与时序建模逻辑,同时可尝试引入温度、老化周期等多维特征,或融合物理模型构建混合预测架构,以进一步提升预测精度与泛化能力。
内容概要:本文系统阐述了基于动态规划算法优化插电式混合动力电动汽车(PHEV)能源管理的技术方案,结合Matlab与Simulink工具实现完整的仿真建模与代码开发。通过动态规划这一全局优化方法,在已知驾驶循环条件下,精确求解发动机、电机及电池之间的最优能量分配策略,以实现燃油消耗与排放的最小化目标,解决PHEV多能源路径规划中的复杂决策问题。文中提供了详尽的仿真模型构建流程与算法实现步骤,涵盖车辆动力学建模、能量管理架构设计、状态空间定义、代价函数构造、最优控制律求解及结果可视化分析等关键环节,全面揭示PHEV能量管理系统的内在机制与优化逻辑。; 适合人群:具备一定Matlab/Simulink编程基础,从事新能源汽车、智能控制、电力电子、自动化或交通运输工程等相关领域的研究生、科研人员及工程技术人员,尤其适合专注于车辆能量管理策略、节能控制算法研究的专业人士。; 使用场景及目标:①深入掌握动态规划在混合动力汽车能量管理中的理论基础与工程实现方法;②学习如何在Matlab/Simulink环境中搭建PHEV整车仿真平台并实施多目标优化仿真;③为学术研究、学位论文撰写或实际工程项目提供可复用的算法框架、模型模板与技术支持,支撑后续对等效燃油消耗最小化策略(ECMS)、模型预测控制(MPC)、实时优化算法等的对比研究与性能评估。; 阅读建议:建议读者结合所提供的完整代码与Simulink模型文件,逐模块调试运行,重点理解状态变量离散化处理、前后向递推求解过程、惩罚项设置以及边界条件处理等核心技术细节,同时可进一步拓展应用于不同工况场景、不同车型结构或与其他优化算法(如庞特里亚金极小值原理PMP)的对比验证,从而深化对PHEV能量管理实时性与全局性平衡问题的理解。
内容概要:本文围绕基于多虚拟同步发电机(VSG)的独立微网系统,开展多目标二次控制策略的MATLAB/Simulink建模与仿真研究。通过构建包含多个VSG单元的独立微网系统,设计并实现了能够同时实现频率与电压的无静差恢复、有功/无功功率精确分配以及环流有效抑制的综合控制目标的二次控制方法。研究重点在于控制策略的整体架构设计、关键控制模块的数学建模及其在Simulink环境中的精细化实现,通过大量仿真实验验证了所提控制策略在不同工况下的有效性、动态响应性能及系统鲁棒性。; 适合人群:具备电力系统分析、自动控制理论及现代电力电子技术等专业知识背景,熟悉MATLAB/Simulink仿真工具,从事新能源发电、微电网运行与控制、分布式能源系统集成等相关领域的科研人员、工程技术人员及高校研究生。; 使用场景及目标:① 深入掌握多VSG独立微网系统的建模方法与稳定性分析要点;② 理解并复现兼顾静态精度与动态品质的多目标二次协同控制算法;③ 为新型微网控制保护装置的研发及先进控制策略的工程化应用提供可靠的仿真验证平台技术储备。; 阅读建议:学习者应在巩固电力系统基础理论的前提下,重点关注控制算法的设计逻辑、各控制环节间的耦合关系以及Simulink模块的搭建技巧,建议通过调整系统参数、设置不同的负载投切与故障扰动工况进行反复仿真,以深刻理解控制策略的内在机理与适应能力。
【通用视觉框架】基于Qt+Halcon开发的仿Visionmaster的通用视觉框架软件,全套源码,开箱即用 1.1 背景 ​ 本项目软件开发意图为实现对Halcon、Opencv算子及其它视觉软件的便捷使用,由于HalconOpencv使用相比VisionPro较为麻烦,故此本软件仿照海康VisionMaster的流程图式操作,实现对Halcon、Opencv及其它视觉软件的二次开发。 2.1 软件概述 本软件使用Qt框架进行开发,实现对视觉流程的自由搭配,市场上对标海康威视的VisionMaster; 本软件使用插件化开发框架,可使用提供的二次开发库自行添新功能算子新模块(将生成的插件放置到对应目录下即可); 2.2 功能概述: 视觉流程图式编程:实现对视觉/数据处理算子的自由编程,从而实现各类复杂的视觉需求 项目读取保存:将编程的视觉项目进行保存或者读取 图像显示:主界面中可以显示及监控视觉算子的图像处理情况 日志消息显示:显示软件运行过程中出现的日志消息 多语言:可进行多种语言切换 2.3 开发平台 主开发语言:Qt(C++) C++语言标椎:C++17 开发环境:Window/Linux 编程平台:Qt Creator 编译器: |版本 | MSVC | Qt 6.4.0 MSVC2019 64bit | | Mingw | Qt 6.4.0 MinGW 64-bit | 视觉工具:Halcon19.11 Progress X64 资源介绍请查阅:https://blog.csdn.net/m0_37302966/article/details/146980317 多视觉框架资源:https://blog.csdn.net/m0_37302966/article/details/146583453
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值