这一篇把 MQTT 最核心的 PUBLISH 报文拆开讲,重点不是概念,而是首字节怎么编码、Topic 和 Packet ID 怎么排、QoS0 / QoS1 / QoS2 在协议和代码层到底差在哪。
适合谁收藏
- 正在做 CODESYS / PLC / MQTT 项目的人
- 想把 MQTT 从报文真正看到 ST 代码的人
- 正在排查 QoS1 / QoS2 超时、掉线、重连问题的人
很多人对 MQTT 的理解,停在一句话:
MQTT 不就是发布订阅吗?
这句话当然没错。 但一到代码里,这句话基本没什么用。
因为真正把客户端难住的,不是“知道有发布订阅”,而是下面这些问题:
PUBLISH报文首字节到底怎么编码- Topic、Packet Identifier、Payload 到底怎么排
- QoS0、QoS1、QoS2 到底不是一个按钮,而是三种完全不同的协议语义
- 为什么 QoS0 很容易跑通,QoS1 / QoS2 一上来就开始暴露问题
这一篇我们就把这件事一次讲透。
先给结论:
PUBLISH 是 MQTT 最核心、也最容易把客户端“真实水平”暴露出来的一类报文。
一、先看 PUBLISH 在状态机里处于什么位置
先把这件事放回主状态机看,不然很容易只盯着报文。
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 | 是否重发 |
| QoS | 0 / 1 / 2 |
| Retain | 是否保留消息 |
| Remaining Length | 后续字节总长度 |
2. 可变报头
| 字段 | 说明 |
|---|---|
| Topic Name | 发布主题 |
| Packet Identifier | QoS1 / QoS2 必须带,QoS0 不带 |
| Properties | MQTT 5.0 才有 |
3. 载荷
| 字段 | 说明 |
|---|---|
| Payload | 真正的业务数据 |
三、PUBLISH 第一个字节最容易让人混
PUBLISH 的首字节不是固定值。 它由 4 个部分拼出来:
| 位段 | 含义 |
|---|---|
| 高四位 | 报文类型,固定是 PUBLISH |
| bit3 | DUP |
| bit2-bit1 | QoS |
| bit0 | Retain |
源码里真实写法是:
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 件事:
- 先把报文类型位放进去
- 如果是重发,DUP 位置 1
- 把 QoS 左移到 bit2-bit1
- 如果要保留消息,Retain 位置 1
四、先拆一个最小 PUBLISH 十六进制
先看一个最小 QoS0 发布:
30 0B 00 04 74 65 73 74 68 69这个例子可以理解为:
- 主题:
test - 载荷:
hi
拆开看:
| 字节 | 含义 |
|---|---|
30 | PUBLISH,DUP=0,QoS=0,Retain=0 |
0B | Remaining Length=11 |
00 04 | Topic 长度=4 |
74 65 73 74 | test |
68 69 | hi |
注意这里 没有 Packet Identifier。 因为它是 QoS0。
五、QoS1 和 QoS2 为什么会多出 Packet Identifier
因为 QoS1 / QoS2 后面还要走确认链。 服务端回 ACK 的时候,必须知道自己是在确认哪一条消息。
所以:
- QoS0:没有后续确认,不需要 Packet ID
- QoS1 / QoS2:必须有 Packet ID
源码里也能看到这个分支:
IF ePublishQoS = E_MqttQoS.byQoS0 THEN
uiVarHeaderLen := 2 + uiTopicLen;
ELSE
uiVarHeaderLen := 2 + uiTopicLen + 2;
END_IF后面那 + 2,就是 Packet Identifier 的两个字节。
六、QoS1 的报文链到底是什么
QoS1 不是一个单包动作,而是一条两段链。
sequenceDiagram
participant PLC as PLC Client
participant Broker as Broker
PLC->>Broker: PUBLISH(QoS1)
Broker-->>PLC: PUBACK对应状态机是:
iConnected -> iPublish -> iPubAck -> iConnected也就是说,QoS1 的重点不只是把 PUBLISH 发出去,而是:
- 要记住这条消息的 Packet ID
- 要等到匹配的
PUBACK - 要在收到 ACK 后把在途记录清掉
七、QoS2 的报文链为什么更重
QoS2 要保证“只一次”,所以它要拆成两段确认。
sequenceDiagram
participant PLC as PLC Client
participant Broker as Broker
PLC->>Broker: PUBLISH(QoS2)
Broker-->>PLC: PUBREC
PLC->>Broker: PUBREL
Broker-->>PLC: PUBCOMP对应状态机是:
iConnected -> iPublish -> iPubRec -> iPubRel -> iPubComp -> iConnected所以 QoS2 的复杂度,不是“多两个报文”这么简单。 它意味着客户端必须真的有状态。
八、M_BuildPublishPacket 的核心工作流
这个方法如果压缩一下,其实可以看成下面这张图:
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 记录]这里最容易被忽略的一层,是“服务端能力边界检查”。
比如源码里有这段:
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 时,会把订阅过滤器和发布主题混了。
订阅时:
+#
这些是合法过滤符。
发布时:
主题名不能带通配符。
源码里直接做了拦截:
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。
看源码:
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 记录:
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 很容易炸
对比一下就很直观。
| 维度 | QoS0 | QoS1 | QoS2 |
|---|---|---|---|
| Packet ID | 不需要 | 需要 | 需要 |
| ACK | 无 | PUBACK | PUBREC/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 |
| 等待 ACK | iPubAck / iPubRec / iPubComp |
| 管理在途消息 | M_InflightAdd |
十四、这一篇你最该记住的 5 句话
- PUBLISH 首字节不是固定值,而是报文类型、DUP、QoS、Retain 拼出来的。
- QoS0 没有 Packet ID,QoS1 / QoS2 必须有。
- QoS1 和 QoS2 的难点不是构包,而是后面的确认链。
- 真正把高可靠发布撑起来的,是 inflight 记录和状态机。
- 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 和服务端限制校验。
/// =======================================================================
/// 名称 : 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 报文里字符串字段最容易被忽略的地方。
/// =======================================================================
/// 名称 : 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 字符串必须带长度前缀。
/// =======================================================================
/// 名称 : 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 最容易掉线超时
评论区预留
这里先保留评论和回复结构,不接入第三方服务。后续统一决定登录、匿名、审核、反垃圾和静态站兼容策略。