ControlRookie
返回文章

第3篇_PUBLISH 报文怎么写?QoS0、QoS1、QoS2 到底差在哪

这一篇把 MQTT 最核心的 PUBLISH 报文拆开讲,重点不是概念,而是首字节怎么编码、Topic 和 Packet ID 怎么排、QoS0 / QoS1 / QoS2 在协议和代码层到底差在哪。

这一篇把 MQTT 最核心的 PUBLISH 报文拆开讲,重点不是概念,而是首字节怎么编码、Topic 和 Packet ID 怎么排、QoS0 / QoS1 / QoS2 在协议和代码层到底差在哪。

适合谁收藏

  • 正在做 CODESYS / PLC / MQTT 项目的人
  • 想把 MQTT 从报文真正看到 ST 代码的人
  • 正在排查 QoS1 / QoS2 超时、掉线、重连问题的人

很多人对 MQTT 的理解,停在一句话:

MQTT 不就是发布订阅吗?

这句话当然没错。 但一到代码里,这句话基本没什么用。

因为真正把客户端难住的,不是“知道有发布订阅”,而是下面这些问题:

  1. PUBLISH 报文首字节到底怎么编码
  2. Topic、Packet Identifier、Payload 到底怎么排
  3. QoS0、QoS1、QoS2 到底不是一个按钮,而是三种完全不同的协议语义
  4. 为什么 QoS0 很容易跑通,QoS1 / QoS2 一上来就开始暴露问题

这一篇我们就把这件事一次讲透。

先给结论:

PUBLISH 是 MQTT 最核心、也最容易把客户端“真实水平”暴露出来的一类报文。

一、先看 PUBLISH 在状态机里处于什么位置

先把这件事放回主状态机看,不然很容易只盯着报文。

Mermaid
flowchart LR
    A[iConnected] --> B[iPublish]
    B --> C[QoS0: 直接回 iConnected]
    B --> D[QoS1: 进入 iPubAck]
    B --> E[QoS2: 进入 iPubRec]

这一张图已经把差别说得很明白了:

  • QoS0:发完就结束
  • QoS1:发完还要等 PUBACK
  • QoS2:发完还要继续走 PUBREC -> PUBREL -> PUBCOMP

所以你可以先记一句大白话:

QoS0 是“我发了”。

二、PUBLISH 报文结构先看表

1. 固定报头

字段说明
报文类型0011,也就是 PUBLISH
DUP是否重发
QoS0 / 1 / 2
Retain是否保留消息
Remaining Length后续字节总长度

2. 可变报头

字段说明
Topic Name发布主题
Packet IdentifierQoS1 / QoS2 必须带,QoS0 不带
PropertiesMQTT 5.0 才有

3. 载荷

字段说明
Payload真正的业务数据

三、PUBLISH 第一个字节最容易让人混

PUBLISH 的首字节不是固定值。 它由 4 个部分拼出来:

位段含义
高四位报文类型,固定是 PUBLISH
bit3DUP
bit2-bit1QoS
bit0Retain

源码里真实写法是:

iecst
aTxBuf[uiPos] := E_MqttPacketType.byPublish OR
    (BOOL_TO_BYTE(bDup) * 16#08) OR
    TO_BYTE(SHL(ePublishQoS AND 16#03, 1)) OR
    (BOOL_TO_BYTE(bPublishRetainLocal) * 16#01);

这段要是真看懂了,后面很多问题都会顺。

它等于在干 4 件事:

  1. 先把报文类型位放进去
  2. 如果是重发,DUP 位置 1
  3. 把 QoS 左移到 bit2-bit1
  4. 如果要保留消息,Retain 位置 1

四、先拆一个最小 PUBLISH 十六进制

先看一个最小 QoS0 发布:

text
30 0B 00 04 74 65 73 74 68 69

这个例子可以理解为:

  • 主题:test
  • 载荷:hi

拆开看:

字节含义
30PUBLISH,DUP=0,QoS=0,Retain=0
0BRemaining Length=11
00 04Topic 长度=4
74 65 73 74test
68 69hi

注意这里 没有 Packet Identifier。 因为它是 QoS0。


五、QoS1 和 QoS2 为什么会多出 Packet Identifier

因为 QoS1 / QoS2 后面还要走确认链。 服务端回 ACK 的时候,必须知道自己是在确认哪一条消息。

所以:

  • QoS0:没有后续确认,不需要 Packet ID
  • QoS1 / QoS2:必须有 Packet ID

源码里也能看到这个分支:

iecst
IF ePublishQoS = E_MqttQoS.byQoS0 THEN
    uiVarHeaderLen := 2 + uiTopicLen;
ELSE
    uiVarHeaderLen := 2 + uiTopicLen + 2;
END_IF

后面那 + 2,就是 Packet Identifier 的两个字节。


六、QoS1 的报文链到底是什么

QoS1 不是一个单包动作,而是一条两段链。

Mermaid
sequenceDiagram
    participant PLC as PLC Client
    participant Broker as Broker

    PLC->>Broker: PUBLISH(QoS1)
    Broker-->>PLC: PUBACK

对应状态机是:

text
iConnected -> iPublish -> iPubAck -> iConnected

也就是说,QoS1 的重点不只是把 PUBLISH 发出去,而是:

  • 要记住这条消息的 Packet ID
  • 要等到匹配的 PUBACK
  • 要在收到 ACK 后把在途记录清掉

七、QoS2 的报文链为什么更重

QoS2 要保证“只一次”,所以它要拆成两段确认。

Mermaid
sequenceDiagram
    participant PLC as PLC Client
    participant Broker as Broker

    PLC->>Broker: PUBLISH(QoS2)
    Broker-->>PLC: PUBREC
    PLC->>Broker: PUBREL
    Broker-->>PLC: PUBCOMP

对应状态机是:

text
iConnected -> iPublish -> iPubRec -> iPubRel -> iPubComp -> iConnected

所以 QoS2 的复杂度,不是“多两个报文”这么简单。 它意味着客户端必须真的有状态。


八、M_BuildPublishPacket 的核心工作流

这个方法如果压缩一下,其实可以看成下面这张图:

Mermaid
flowchart TD
    A[基本参数校验] --> B[Topic UTF-8 与通配符校验]
    B --> C[检查 QoS Retain 与服务端能力边界]
    C --> D[计算固定报头 可变报头 Remaining Length]
    D --> E[编码首字节 Topic Packet ID Properties]
    E --> F[追加 Payload]
    F --> G[如果是 QoS1/2 建立 inflight 记录]

这里最容易被忽略的一层,是“服务端能力边界检查”。

比如源码里有这段:

iecst
IF eVersion = E_MqttVersion.byMqttVersion50 THEN
    IF TO_BYTE(ePublishQoS) > byServerMaxQoS THEN
        M_SetError(
            uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
            sMessage := 'Publish QoS exceeds server limit');
        M_BuildPublishPacket := FALSE;
        RETURN;
    END_IF

    IF bPublishRetainLocal AND (NOT bServerRetainAvailable) THEN
        M_SetError(
            uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
            sMessage := 'Retain not supported by server');
        M_BuildPublishPacket := FALSE;
        RETURN;
    END_IF
END_IF

这就是上一篇讲过的那个点: CONNACK 里协商出来的能力,不是摆设,后面真的会影响发包逻辑。


九、为什么发布 Topic 绝对不能带通配符

很多人刚写 MQTT 时,会把订阅过滤器和发布主题混了。

订阅时:

  • +
  • #

这些是合法过滤符。

发布时:

主题名不能带通配符。

源码里直接做了拦截:

iecst
FOR i := 1 TO TO_DINT(uiTopicLen) DO
    IF (sPublishTopic[i - 1] = 16#2B) OR (sPublishTopic[i - 1] = 16#23) THEN
        bHasWildcard := TRUE;
        EXIT;
    END_IF
END_FOR

IF bHasWildcard THEN
    M_SetError(
        uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
        sMessage := 'Publish topic must not contain wildcards');
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

这段规则不是实现者自创的,而是协议本身就这么规定。


十、MQTT 5.0 下的 PUBLISH 又多了什么

如果是 MQTT 5.0,这个库当前会在发布报文里支持 Topic Alias。

看源码:

iecst
IF eVersion = E_MqttVersion.byMqttVersion50 THEN
    uiPropertyDataLen := 0;
    uiTopicAlias := 0;
    IF uiServerTopicAliasMax > 0 THEN
        uiTopicAlias := uiNextTopicAlias;
        IF uiTopicAlias = 0 THEN
            uiTopicAlias := 1;
        END_IF
        uiPropertyDataLen := uiPropertyDataLen + 3;
    END_IF
END_IF

这段的意思很简单:

  • 如果 Broker 在 CONNACK 里告诉你“我支持 Topic Alias”
  • 那客户端后面发 PUBLISH 时,就可以开始带这个属性

这对长主题、高频发布场景是有意义的。 因为它能减少后续报文长度。


十一、真正把 QoS1 / QoS2 撑起来的,不是构包,而是 inflight

PUBLISH 真正难的地方,不在于拼个包。 而在于你发出去之后,怎么保证后面的 ACK 链条还能对得上。

所以 M_BuildPublishPacket 最后一个关键动作,是建立 inflight 记录:

iecst
uiInflightIndex := M_InflightAdd(
    uiPacketId := uiExpectedPacketId,
    eQoS := ePublishQoS,
    sTopic := sPublishTopic,
    sPayload := sPublishPayload,
    uiPayloadLen := uiPayloadLen,
    bRetain := bPublishRetainLocal);

你可以把 inflight 先理解成:

一张“我还有哪些 QoS 消息没有真正走完”的台账。

它至少要记住:

  • Packet ID
  • QoS
  • Topic
  • Payload
  • Retain
  • 最后发送时间
  • 重试次数
  • 当前在途状态

十二、为什么 QoS0 很容易通,QoS1 / QoS2 很容易炸

对比一下就很直观。

维度QoS0QoS1QoS2
Packet ID不需要需要需要
ACK无PUBACKPUBREC/PUBREL/PUBCOMP
inflight不需要需要需要
超时重发不需要需要需要
去重不需要一般不强调必须重点处理

也就是说,QoS0 只是“能发”。 QoS1 / QoS2 则要求客户端有完整的“发送后管理能力”。

这也是为什么很多项目一开始觉得自己 MQTT 写得挺顺:

  • QoS0 全通
  • 一换 QoS1 / QoS2 就开始掉线、超时、重连

不是网络突然变差了。 而是你的状态机、ACK 处理、inflight 生命周期没闭环。


十三、把 PUBLISH 的四层链路再串一遍

1. 标准层

  • 客户端发 PUBLISH
  • QoS 不同,后续确认链不同

2. 报文层

  • 首字节编码 DUP / QoS / Retain
  • Topic 在前
  • QoS1 / QoS2 带 Packet ID
  • 5.0 还可能带属性

3. 状态机层

  • QoS0:iPublish -> iConnected
  • QoS1:iPublish -> iPubAck -> iConnected
  • QoS2:iPublish -> iPubRec -> iPubRel -> iPubComp -> iConnected

4. ST 代码层

动作方法 / 状态
构造发布报文M_BuildPublishPacket
等待 ACKiPubAck / iPubRec / iPubComp
管理在途消息M_InflightAdd

十四、这一篇你最该记住的 5 句话

  1. PUBLISH 首字节不是固定值,而是报文类型、DUP、QoS、Retain 拼出来的。
  2. QoS0 没有 Packet ID,QoS1 / QoS2 必须有。
  3. QoS1 和 QoS2 的难点不是构包,而是后面的确认链。
  4. 真正把高可靠发布撑起来的,是 inflight 记录和状态机。
  5. QoS0 跑得通,不代表你的 MQTT 客户端真的成熟。

十五、下篇预告

下一篇我们直接进入最值钱的一块:

为什么 QoS1 / QoS2 最容易把状态机搞崩

重点会讲:

  • PUBACK / PUBREC / PUBREL / PUBCOMP
  • inflight 超时
  • 重发
  • DUP 位
  • 为什么高频场景最容易把问题打出来

如果说这一篇是“看懂 PUBLISH 长什么样”, 那下一篇就是“看懂它为什么能稳,或者为什么会炸”。


完整 ST 代码

复制使用说明

  • 这部分给出的是与本篇主题直接对应的完整 ST 代码,不是零碎片段。
  • 如果你只是想先跑通,优先整段复制,不要只摘几行变量或几条赋值语句。
  • 如果是 METHOD,请确认它仍然属于 FB_MqttClient;如果是 PROGRAM,请确认相关 DUT、GVL、FB 已一并导入。

代码阅读重点

  • 先按 报文结构 -> 状态机入口 -> 关键变量 -> 返回结果 的顺序看。
  • 再把正文里的十六进制拆解和这里的字节写入、字节解析语句一行行对上。
  • 最后回到在线调试,重点盯 uiTxLength、uiRxLength、eState、xWaitingForAck 这类状态量。

完整代码 1:M_BuildPublishPacket

  • 对应源码路径:10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient/处理发送报文/M_BuildPublishPacket.st
  • 复制使用说明:这是 PUBLISH 的主方法,QoS0、QoS1、QoS2 都从这里起步。
  • 阅读重点:先盯报文头里 DUP/QoS/Retain 的拼接,再看 Packet ID 和 Inflight 入队,最后看 MQTT 5.0 Topic Alias 和服务端限制校验。
iecst
/// =======================================================================
/// 名称      : M_BuildPublishPacket
/// 功能      : 构建 PUBLISH 发送报文
/// 说明      : 根据主题、载荷、QoS 和协议版本组装发布报文。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-05
/// 版本      : V1.1
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_BuildPublishPacket : BOOL
VAR
    uiPos               : UINT := 0;
    uiVarHeaderLen      : UINT;
    uiPayloadLen        : UINT;
    uiRemainingLen      : UINT;
    uiPropsLen          : UINT;
    uiPropertyDataLen   : UINT;
    uiTopicAlias        : UINT;
    uiInflightIndex     : UINT;
    uiPublishPacketId   : UINT;
    i                   : DINT;
    sPublishTopic       : STRING(GVL_Mqtt.cnMaxTopicLen);
    sPublishPayload     : STRING(GVL_Mqtt.cnMaxPayloadSize);
    ePublishQoS         : E_MqttQoS;
    bPublishRetainLocal : BOOL;
    bHasWildcard        : BOOL;
END_VAR

// === IMPLEMENTATION ===
IF SIZEOF(aTxBuf) < 256 THEN
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

IF (ePubQoS > E_MqttQoS.byQoS0) AND (eVersion = E_MqttVersion.byMqttVersion50) AND (uiSendQuota = 0) THEN
    M_SetError(
        uiErrorCode := TO_UINT(E_ReasonCode.uiErrReceiveMaxExceeded),
        sMessage := 'Send quota exhausted');
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

FOR i := LOWER_BOUND(aTxBuf, 1) TO UPPER_BOUND(aTxBuf, 1) DO
    aTxBuf[i] := 0;
END_FOR

sPublishTopic := sPubTopic;
sPublishPayload := sPubPayload;
ePublishQoS := ePubQoS;
bPublishRetainLocal := bPubRetain;
uiPublishPacketId := 0;

IF (uiRetryInflightIndex > 0) AND (uiRetryInflightIndex <= GVL_Mqtt.cnMaxInflight) THEN
    IF aInflight[uiRetryInflightIndex].bUsed THEN
        sPublishTopic := aInflight[uiRetryInflightIndex].sTopic;
        sPublishPayload := aInflight[uiRetryInflightIndex].sPayload;
        ePublishQoS := aInflight[uiRetryInflightIndex].eQoS;
        bPublishRetainLocal := aInflight[uiRetryInflightIndex].bRetain;
        uiPublishPacketId := aInflight[uiRetryInflightIndex].uiPacketId;
        bDup := aInflight[uiRetryInflightIndex].bDup;
    END_IF
END_IF

IF sPublishTopic = '' THEN
    M_SetError(
        uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
        sMessage := 'Publish topic is required');
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

IF TO_UINT(LEN(sPublishTopic)) > GVL_Mqtt.cnMaxTopicLen THEN
    M_SetError(
        uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
        sMessage := 'Publish topic exceeds maximum length');
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

IF NOT M_IsValidUtf8String(sValue := sPublishTopic) THEN
    M_SetError(
        uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
        sMessage := 'Publish topic is not valid UTF-8');
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

bHasWildcard := FALSE;
FOR i := 1 TO TO_DINT(LEN(sPublishTopic)) DO
    IF (sPublishTopic[i - 1] = 16#2B) OR (sPublishTopic[i - 1] = 16#23) THEN
        bHasWildcard := TRUE;
        EXIT;
    END_IF
END_FOR

IF bHasWildcard THEN
    M_SetError(
        uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
        sMessage := 'Publish topic must not contain wildcards');
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

IF TO_UINT(LEN(sPublishPayload)) > GVL_Mqtt.cnMaxPayloadSize THEN
    M_SetError(
        uiErrorCode := TO_UINT(E_ReasonCode.uiErrPacketTooLarge),
        sMessage := 'Publish payload exceeds configured maximum');
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

IF eVersion = E_MqttVersion.byMqttVersion50 THEN
    IF TO_BYTE(ePublishQoS) > byServerMaxQoS THEN
        M_SetError(
            uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
            sMessage := 'Publish QoS exceeds server limit');
        M_BuildPublishPacket := FALSE;
        RETURN;
    END_IF

    IF bPublishRetainLocal AND (NOT bServerRetainAvailable) THEN
        M_SetError(
            uiErrorCode := TO_UINT(E_ReasonCode.uiErrInvalidParameter),
            sMessage := 'Retain not supported by server');
        M_BuildPublishPacket := FALSE;
        RETURN;
    END_IF
END_IF

IF ePublishQoS = E_MqttQoS.byQoS0 THEN
    uiVarHeaderLen := 2 + TO_UINT(LEN(sPublishTopic));
ELSE
    uiVarHeaderLen := 2 + TO_UINT(LEN(sPublishTopic)) + 2;
END_IF

uiPayloadLen := TO_UINT(LEN(sPublishPayload));
uiRemainingLen := uiVarHeaderLen + uiPayloadLen;

IF eVersion = E_MqttVersion.byMqttVersion50 THEN
    uiPropertyDataLen := 0;
    uiTopicAlias := 0;
    IF uiServerTopicAliasMax > 0 THEN
        uiTopicAlias := uiNextTopicAlias;
        IF uiTopicAlias = 0 THEN
            uiTopicAlias := 1;
        END_IF
        uiPropertyDataLen := uiPropertyDataLen + 3;
    END_IF
    uiPropsLen := 1 + uiPropertyDataLen;
    uiRemainingLen := uiRemainingLen + uiPropsLen;

    IF (udServerMaxPacketSize > 0) AND (TO_UDINT(uiRemainingLen + 5) > udServerMaxPacketSize) THEN
        M_SetError(
            uiErrorCode := TO_UINT(E_ReasonCode.uiErrPacketTooLarge),
            sMessage := 'Publish packet exceeds server maximum packet size');
        M_BuildPublishPacket := FALSE;
        RETURN;
    END_IF
END_IF

IF uiRemainingLen + 5 > SIZEOF(aTxBuf) THEN
    M_BuildPublishPacket := FALSE;
    RETURN;
END_IF

IF ePublishQoS = E_MqttQoS.byQoS0 THEN
    bDup := FALSE;
END_IF

aTxBuf[uiPos] := E_MqttPacketType.byPublish OR
    (BOOL_TO_BYTE(bDup) * 16#08) OR
    TO_BYTE(SHL(ePublishQoS AND 16#03, 1)) OR
    (BOOL_TO_BYTE(bPublishRetainLocal) * 16#01);
uiPos := uiPos + 1;

uiPos := uiPos + M_EncodeRemainingLength(udiLength := uiRemainingLen, pBuffer := ADR(aTxBuf[uiPos]));
uiPos := uiPos + M_AppendString(sStr := sPublishTopic, pBuffer := ADR(aTxBuf[uiPos]));

IF ePublishQoS > E_MqttQoS.byQoS0 THEN
    IF uiPublishPacketId = 0 THEN
        uiExpectedPacketId := M_GetNextPacketId();
    ELSE
        uiExpectedPacketId := uiPublishPacketId;
    END_IF
    IF uiExpectedPacketId = 0 THEN
        M_SetError(
            uiErrorCode := TO_UINT(E_ReasonCode.uiErrReceiveMaxExceeded),
            sMessage := 'No Packet ID available');
        M_BuildPublishPacket := FALSE;
        RETURN;
    END_IF
    uiQoS2PacketId := uiExpectedPacketId;
    aTxBuf[uiPos] := UINT_TO_BYTE(SHR(uiExpectedPacketId, 8));
    uiPos := uiPos + 1;
    aTxBuf[uiPos] := UINT_TO_BYTE(uiExpectedPacketId AND 16#FF);
    uiPos := uiPos + 1;
END_IF

IF eVersion = E_MqttVersion.byMqttVersion50 THEN
    uiPos := uiPos + M_EncodeRemainingLength(udiLength := uiPropertyDataLen, pBuffer := ADR(aTxBuf[uiPos]));
    IF uiTopicAlias > 0 THEN
        aTxBuf[uiPos] := GVL_Mqtt.cnPropTopicAlias;
        uiPos := uiPos + 1;
        aTxBuf[uiPos] := UINT_TO_BYTE(SHR(uiTopicAlias, 8));
        uiPos := uiPos + 1;
        aTxBuf[uiPos] := UINT_TO_BYTE(uiTopicAlias AND 16#FF);
        uiPos := uiPos + 1;
        uiNextTopicAlias := uiTopicAlias + 1;
        IF (uiNextTopicAlias = 0) OR (uiNextTopicAlias > uiServerTopicAliasMax) THEN
            uiNextTopicAlias := 1;
        END_IF
    END_IF
END_IF

uiPos := uiPos + M_AppendPayload(sPayload := sPublishPayload, pBuffer := ADR(aTxBuf[uiPos]));
uiTxLength := uiPos;

IF ePublishQoS > E_MqttQoS.byQoS0 THEN
    IF uiPublishPacketId = 0 THEN
        uiInflightIndex := M_InflightAdd(
            uiPacketId := uiExpectedPacketId,
            eQoS := ePublishQoS,
            sTopic := sPublishTopic,
            sPayload := sPublishPayload,
            uiPayloadLen := uiPayloadLen,
            bRetain := bPublishRetainLocal);
        IF uiInflightIndex = 0 THEN
            M_SetError(
                uiErrorCode := TO_UINT(E_ReasonCode.uiErrReceiveMaxExceeded),
                sMessage := 'Inflight queue is full');
            M_BuildPublishPacket := FALSE;
            RETURN;
        END_IF
    ELSIF (uiRetryInflightIndex > 0) AND (uiRetryInflightIndex <= GVL_Mqtt.cnMaxInflight) THEN
        aInflight[uiRetryInflightIndex].tLastSend := TIME();
    END_IF
END_IF

M_BuildPublishPacket := TRUE;

完整代码 2:M_AppendString

  • 对应源码路径:10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient/辅助功能/M_AppendString.st
  • 复制使用说明:这个方法专门负责把 MQTT 字符串写进缓冲区,复制时建议和 PUBLISH、SUBSCRIBE、CONNECT 相关方法一起保留。
  • 阅读重点:重点看前 2 字节长度前缀,这就是 MQTT 报文里字符串字段最容易被忽略的地方。
iecst
/// =======================================================================
/// 名称      : M_AppendString
/// 功能      : 追加 UTF-8 字符串到发送缓冲区
/// 说明      : 先写入 2 字节长度前缀,再写入字符串内容并返回总字节数。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-05
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_AppendString : UINT
VAR_INPUT
    sStr        : STRING;
    pBuffer     : POINTER TO BYTE;
END_VAR
VAR
    uiLen       : UINT;
    i           : DINT;
    pWrite      : POINTER TO BYTE;
END_VAR

// === IMPLEMENTATION ===
uiLen := TO_UINT(LEN(sStr));

// 长度前缀(MSB + LSB)
pWrite := pBuffer;
pWrite^ := UINT_TO_BYTE(SHR(uiLen, 8));
pWrite := pWrite + 1;
pWrite^ := UINT_TO_BYTE(uiLen AND 16#FF);

// 字符串内容
pWrite := pBuffer + 2;
IF uiLen > 0 THEN
    FOR i := 1 TO TO_DINT(uiLen) DO
        pWrite^ := TO_BYTE(sStr[i - 1]);
        pWrite := pWrite + 1;
    END_FOR
END_IF

M_AppendString := uiLen + 2;

完整代码 3:M_AppendPayload

  • 对应源码路径:10 MQTT/MqttClient_V1_0/Device/Application/MQTT/POUs/MqttClient NBS/FB_MqttClient/辅助功能/M_AppendPayload.st
  • 复制使用说明:这个方法负责把业务载荷逐字节落到发送缓冲区,是 PUBLISH 组包的最后一跳。
  • 阅读重点:重点看它和 M_AppendString 的差别:Payload 不带长度前缀,而 Topic/ClientID 这类 UTF-8 字符串必须带长度前缀。
iecst
/// =======================================================================
/// 名称      : M_AppendPayload
/// 功能      : 追加消息载荷到发送缓冲区
/// 说明      : 按字节拷贝字符串载荷并返回写入字节数。
/// 编程人员  : ControlRookie
/// 时间      : 2026-05-05
/// 版本      : V1.0
/// =======================================================================
{attribute 'hide_all_locals'}
METHOD M_AppendPayload : UINT
VAR_INPUT
    sPayload    : STRING;
    pBuffer     : POINTER TO BYTE;
END_VAR
VAR
    uiLen       : UINT;
    i           : DINT;
    pWrite      : POINTER TO BYTE;
END_VAR

// === IMPLEMENTATION ===
uiLen := TO_UINT(LEN(sPayload));

pWrite := pBuffer;
IF uiLen > 0 THEN
    FOR i := 1 TO TO_DINT(uiLen) DO
        pWrite^ := TO_BYTE(sPayload[i - 1]);
        pWrite := pWrite + 1;
    END_FOR
END_IF

M_AppendPayload := uiLen;

系列导航

  • 系列定位:第 3 篇
  • 上一篇:第2篇 CONNECT / CONNACK
  • 下一篇:第4篇 为什么 QoS1 / QoS2 最容易掉线超时
评论和回复区

评论区预留

这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。

↑ ↓