03-2 · 协议实现详解(下):三菱 MC 与横向对比
本文分析三菱 MC(Melsec) 协议族的实现(
protocol/melsec),并在结尾给出 Modbus / S7 / MC 三大协议的横向对比。MC 部分包含:支持帧与系列、报文构造类、软元件地址解析、批量读写构造,并附真实源码片段(带行号)。Modbus 与 S7 见姊妹篇 协议实现详解(上):Modbus 与 S7。
本文目录
第一部分:MC(Melsec)
3.1 McPLC API 与支持的帧
说明:本库中没有名为
McClient的类(源码中未见);对等门面是McPLC(protocol/melsec/service/McPLC.java:72,795 行),继承链McPLC → McNetwork → TcpClientBasic。
3.1.1 支持的帧
protocol/melsec/enums/EMcFrameType.java:40-56:
40→public enum EMcFrameType {
45→ FRAME_4E("4E", 0x0054, 0x00D4), // 请求副帧头 0x0054,响应副帧头 0x00D4
50→ FRAME_3E("3E", 0x0050, 0x00D0), // 请求 0x0050,响应 0x00D0
55→ FRAME_1E("1E", 0x00, 0x00), // 副帧头就是指令类型,这里先写个临时的数据
| 帧 | 副帧头(请求/响应) | 头部长度 | 适用系列 | 数据长度字段 |
|---|---|---|---|---|
| 4E | 0x0054 / 0x00D4 |
15 | iQ-R 扩展帧 | ✅ dataLength |
| 3E | 0x0050 / 0x00D0 |
11 | QnA / Q/L / FX5U | ✅ dataLength |
| 1E | 指令码本身(1 字节) | — | A 系列 / FX3U+以太网模块 | ❌ 无 |
3.1.2 支持的系列
protocol/melsec/enums/EMcSeries.java 定义了 4 个系列,每个带 11 个协议容量参数:
| 系列 | 帧类型 | 软元件码字节 | 起始编号字节 | 批量读字点数 | 批量读位点数 | 随机读字 | 随机写字 | 随机写位 | 多块数 |
|---|---|---|---|---|---|---|---|---|---|
A |
1E | 2 | 4 | 10 | 160 | 0 | 10 | 80 | 0 |
QnA |
3E | 1 | 3 | 480 | 3584 | 96 | 960 | 94 | 120 |
Q_L |
3E | 1 | 3 | 960 | 7168 | 192 | 1920 | 188 | 120 |
IQ_R |
3E | 2 | 4 | 960 | 7168 | 96 | 960 | 94 | 60 |
注意:
IQ_R的注释写着 "TODO: 不是很确定IQ-R是否为3E帧类型"(EMcSeries.java中 IQ_R 上方注释)——作者自己标注了不确定。
3.1.3 API 形态
McPLC 的 API 与 S7PLC 高度对称:
| 类别 | 方法(行号) |
|---|---|
| 多地址 | readMultiAddress(McMultiAddressRead) (103) / writeMultiAddress(McMultiAddressWrite) (441) |
| 位/字节 | readBoolean (115/146) / readByte (158) / readBytes (131) |
| 整数 | readInt16 (170) / readUInt16 (210) / readInt32 (251) / readUInt32 (292) / readInt64 (333) |
| 浮点 | readFloat32 (345) / readFloat64 (386) |
| 字符串 | readString(address, length) (426) |
| 写 | writeBoolean/writeByte/writeInt16/.../writeString (452~789) |
⚠️ 能力缺口:
readFloat64(String... addresses)与readFloat64(List<String>)被注释掉了(McPLC.java:397,407)——多地址读 double 不支持。
3.2 报文构造类
3.2.1 类关系
McMessageReq (IObjectByteArray) McMessageAck (IObjectByteArray)
├── McHeaderReq header ├── McHeaderAck header
└── McData data └── McData data
↑ 所有请求数据体的基类(空实现) ├── McAckData(正常响应)
├── McError1EData(1E 错误)
└── McError4E3EData(3E/4E 错误)
McHeaderReq (抽象语义)
├── McHeader1EReq (1 + route + 2)
├── McHeader3EReq (2 + route + 2 + 2)
└── McHeader4EReq extends McHeader3EReq (2 + 2 + 2 + route + 2 + 2)
3.2.2 McHeaderReq —— 帧类型工厂
protocol/melsec/model/McHeaderReq.java:81-92:
81→ public static McHeaderReq createByFrameType(EMcFrameType frameType, McAccessRoute accessRoute, int timer) {
82→ switch (frameType) {
83→ case FRAME_1E:
84→ return new McHeader1EReq(accessRoute, timer);
85→ case FRAME_3E:
86→ return new McHeader3EReq(accessRoute, timer);
87→ case FRAME_4E:
88→ return new McHeader4EReq(accessRoute, timer);
89→ default:
90→ throw new McCommException("unknown frame type");
91→ }
92→ }
这是简单工厂模式,byteArrayLength() / toByteArray() 在基类里直接抛 UnsupportedOperationException(McHeaderReq.java:71-79)——强制子类实现。
3.2.3 三种头部的字节布局
1E(McHeader1EReq.java:53-66):
54→ public int byteArrayLength() {
55→ return 1 + this.accessRoute.byteArrayLength() + 2;
56→ }
59→ public byte[] toByteArray() {
60→ int length = 1 + this.accessRoute.byteArrayLength() + 2;
61→ return ByteWriteBuff.newInstance(length, true) // ← true = 小端!
62→ .putByte(this.subHeader) // 1 字节(就是指令码)
63→ .putBytes(this.accessRoute.toByteArray())
64→ .putShort(this.monitoringTimer)
65→ .getData();
66→ }
3E(McHeader3EReq.java:63-77):
69→ public byte[] toByteArray() {
70→ int length = 2 + this.accessRoute.byteArrayLength() + 2 + 2;
71→ return ByteWriteBuff.newInstance(length, true)
72→ .putShort(this.subHeader) // 0x0050
73→ .putBytes(this.accessRoute.toByteArray())
74→ .putShort(this.dataLength) // 数据长度
75→ .putShort(this.monitoringTimer)
76→ .getData();
77→ }
4E(McHeader4EReq.java:74-85)—— 在 3E 基础上加了 serialNumber 和 fixedNumber:
75→ public byte[] toByteArray() {
76→ int length = 6 + this.accessRoute.byteArrayLength() + 2 + 2;
77→ return ByteWriteBuff.newInstance(length, true)
78→ .putShort(this.subHeader)
79→ .putShort(this.serialNumber) // 序列号
80→ .putShort(this.fixedNumber) // 固定值编号
81→ .putBytes(this.accessRoute.toByteArray())
82→ .putShort(this.dataLength)
83→ .putShort(this.monitoringTimer)
84→ .getData();
85→ }
⚠️ 全库都是小端:
ByteWriteBuff.newInstance(length, true)的第二个参数true表示 littleEndian。因为三菱 MC 协议是小端字节序(与西门子 S7 的大端相反)。这是三个协议里唯一全程小端的。
3.2.4 McMessageReq.selfCheck() —— 长度回填
protocol/melsec/model/McMessageReq.java:79-85:
79→ public void selfCheck() {
80→ if (this.header.getFrameType() == EMcFrameType.FRAME_1E) {
81→ return; // 1E 帧没有长度字段
82→ }
83→ McHeader3EReq header3EReq = (McHeader3EReq) this.header;
84→ header3EReq.dataLength = 2 + this.data.byteArrayLength(); // +2 = monitoringTimer
85→ }
注意这里把
McHeaderReq强转成McHeader3EReq—— 因为McHeader4EReq extends McHeader3EReq,所以 4E 也能转。这是继承关系设计得好的一个体现。
3.2.5 McMessageAck.fromBytes() —— 按结束码分发
protocol/melsec/model/McMessageAck.java:100-112:
100→ public static McMessageAck fromBytes(final byte[] data, final int offset, EMcFrameType frameType) {
101→ McMessageAck res = new McMessageAck();
102→ res.header = McHeaderAck.fromBytes(data, offset, frameType);
103→ int off = offset + res.header.byteArrayLength();
104→ if (res.header.getEndCode() == 0) {
105→ res.data = McAckData.fromBytes(data, off); // 正常
106→ } else if (frameType == EMcFrameType.FRAME_1E) {
107→ res.data = McError1EData.fromBytes(data, off); // 1E 错误
108→ } else {
109→ res.data = McError4E3EData.fromBytes(data, off); // 3E/4E 错误
110→ }
111→ return res;
112→ }
错误码解析在 McNetwork.extractError()(McNetwork.java:249-294),硬编码了 0xC050 ~ 0xC0B5 共 13 个常见码的中文说明,其余返回"请查询三菱用户手册"。
3.2.6 McData / McDeviceContent
McData(model/McData.java:36-46)是一个空基类——只提供 byteArrayLength()=0 和 toByteArray()=new byte[0]。所有具体请求数据体都继承它。
McDeviceContent extends McDeviceAddress(model/McDeviceContent.java:42)= 地址 + 数据内容:
66→ @Override
67→ public int byteArrayLengthWithoutPointsCount(EMcSeries series) {
68→ return this.data.length + super.byteArrayLengthWithoutPointsCount(series);
69→ }
76→ @Override
77→ public byte[] toByteArrayWithoutPointsCount(EMcSeries series) {
78→ int length = this.data.length + super.byteArrayLengthWithoutPointsCount(series);
79→ ByteWriteBuff buff = ByteWriteBuff.newInstance(length, true);
80→ buff.putBytes(super.toByteArrayWithoutPointsCount(series));
81→ buff.putBytes(this.data);
82→ return buff.getData();
83→ }
"With / Without PointsCount" 双形态是 MC 报文的关键区分:
| 形态 | 字节布局 | 用于 |
|---|---|---|
WithPointsCount |
点数(2) + 软元件码 + 起始编号 | 批量读/写(batch) |
WithoutPointsCount |
软元件码 + 起始编号 | 随机读/写(random) |
3.3 软元件地址解析
protocol/melsec/model/McDeviceAddress.java:204-225:
204→ public static McDeviceAddress createBy(String address, int count) {
205→ if (address == null || address.length() == 0) {
206→ throw new IllegalArgumentException("address is null or empty");
207→ }
208→ if (count <= 0) {
209→ throw new IllegalArgumentException("count <= 0");
210→ }
211→ // 转换为大写
212→ address = address.toUpperCase();
213→ EMcDeviceCode deviceCode = EMcDeviceCode.identifyDeviceCodeByStart(address);
214→ if (deviceCode == null) {
215→ throw new McCommException("device code is not exist");
216→ }
217→ // 提取数字数据
218→ int startIndex = deviceCode.getSymbol().length();
219→ String number = address.substring(startIndex).trim();
220→ if ("".equals(number)) {
221→ throw new McCommException("address of device is error");
222→ }
223→ int headDeviceNumber = Integer.parseInt(number, deviceCode.getNotation());
224→ return new McDeviceAddress(deviceCode, headDeviceNumber, count);
225→ }
两个关键设计:
identifyDeviceCodeByStart()(EMcDeviceCode,521 行的软元件码表):按前缀匹配识别软元件,支持X、Y、D、M、L、B、W、R、ZR等;getNotation():每个软元件自带进制!这是 MC 协议的特点——X/Y是十六进制(XFF= 255),D/M是十进制。第 223 行Integer.parseInt(number, deviceCode.getNotation())直接用对应的进制解析。
源码里保留了一份被注释掉的旧实现(
McDeviceAddress.java:157-194),用正则\d找第一个数字位置切分。新实现(第 204 行)改为按软元件符号长度切分,注释写着"兼容XFF,X0F,D100,YFF等表示法"(第 198 行)——说明作者踩过 "X" 后面跟十六进制字母的坑。
3.3.1 软元件编码(按系列分叉)
McDeviceAddress.addBaseData()(McDeviceAddress.java:132-145):
132→ private ByteWriteBuff addBaseData(EMcSeries series, int length) {
133→ ByteWriteBuff buff = ByteWriteBuff.newInstance(length, true);
134→ if (series == EMcSeries.QnA || series == EMcSeries.Q_L) {
135→ buff.putBytes(IntegerUtil.toCustomByteArray(this.headDeviceNumber, 0, 3, true)); // 3 字节编号
136→ buff.putByte(this.deviceCode.getBinaryCode()); // 1 字节软元件码
137→ } else if (series == EMcSeries.IQ_R) {
138→ buff.putInteger(this.headDeviceNumber); // 4 字节编号
139→ buff.putShort(this.deviceCode.getBinaryCodeIqr()); // 2 字节软元件码
140→ } else if (series == EMcSeries.A) {
141→ buff.putInteger(this.headDeviceNumber); // 4 字节
142→ buff.putShort(this.deviceCode.getBinaryCode1E()); // 2 字节(1E 帧码)
143→ }
144→ return buff;
145→ }
即 EMcDeviceCode 为每个软元件维护 3 套编码:binaryCode(Q/L/QnA)、binaryCodeIqr(iQ-R)、binaryCode1E(A 系列)。这是 MC 协议跨系列的必要妥协。
⚠️ 注意:
QnA和Q_L用 3 字节编号,但addBaseData里length由series.getHeadDeviceNumberByteLength()决定(QnA/Q_L= 3)。而IntegerUtil.toCustomByteArray(headDeviceNumber, 0, 3, true)显式写 3 字节——一致。
3.4 批量读写构造
3.4.1 五种批量模式
McNetwork 提供 5 组批量 API,全部用 McReqBuilder 构造报文:
| 模式 | McNetwork 方法 |
McReqBuilder 方法 |
分包算法 |
|---|---|---|---|
| 批量读(字) | readDeviceBatchInWord |
createReadDeviceBatchInWordReq |
按系列上限 |
| 批量读(位) | readDeviceBatchInBit |
createReadDeviceBatchInBitReq |
按系列上限 |
| 批量写(字/位) | writeDeviceBatchInWord/Bit |
createWriteDeviceBatch... |
按系列上限 |
| 随机读(字/D字) | readDeviceRandomInWord |
createReadDeviceRandomInWordReq |
biLoopExecute(双组) |
| 随机写(字/D字) | writeDeviceRandomInWord |
createWriteDeviceRandomInWordReq |
biLoopExecute(双组) |
| 随机写(位) | writeDeviceRandomInBit |
createWriteDeviceRandomInBitReq |
loopExecute(单组) |
| 多块批量读 | readDeviceBatchMultiBlocks |
createReadDeviceBatchMultiBlocksReq |
biLoopExecute(双组) |
| 多块批量写 | writeDeviceBatchMultiBlocks |
createWriteDeviceBatchMultiBlocksReq |
biLoopExecute(双组) |
3.4.2 双组循环:随机读的典型实现
McNetwork.java:609-639:
609→ try {
610→ List<McDeviceContent> result = new ArrayList<>();
611→ int maxLength = this.series.getDeviceRandomReadInWordPointsCount();
614→ BiPredicate<LoopGroupItem, LoopGroupItem> biPredicate = (i1, i2) -> i1.getLen() + i2.getLen() >= maxLength;
615→ LoopGroupItem wordItem = new LoopGroupItem(wordAddresses.size());
616→ LoopGroupItem dwordItem = new LoopGroupItem(dwordAddresses.size());
617→
618→ LoopGroupAlg.biLoopExecute(wordItem, dwordItem, biPredicate, (i1, i2) -> {
619→ List<McDeviceAddress> newWords = wordAddresses.subList(i1.getOff(), i1.getOff() + i1.getLen());
620→ List<McDeviceAddress> newDWords = dwordAddresses.subList(i2.getOff(), i2.getOff() + i2.getLen());
621→ McHeaderReq header = McHeaderReq.createByFrameType(this.frameType, this.accessRoute, this.monitoringTimer);
622→ McMessageReq req = McReqBuilder.createReadDeviceRandomInWordReq(this.series, header, newWords, newDWords);
623→ McMessageAck ack = this.readFromServer(req);
624→
625→ ByteReadBuff buff = new ByteReadBuff(((McAckData) ack.getData()).getData());
626→ for (McDeviceAddress word : newWords) {
627→ result.add(McDeviceContent.createByAddress(word, buff.getBytes(2)));
628→ }
629→ for (McDeviceAddress dword : newDWords) {
630→ result.add(McDeviceContent.createByAddress(dword, buff.getBytes(4)));
631→ }
632→ });
633→ return result;
为什么需要"双组"? 因为 MC 的随机读指令允许同一报文里混装 word 和 dword 软元件,两者的容量占用不同(word 占 1 个点数,dword 占 2 个)。LoopGroupAlg.biLoopExecute 就是"同时推进两个列表,直到满足容量条件就切一包"的算法(详见第四篇 §4)。
biPredicate 的写法也很直白:i1.getLen() + i2.getLen() >= maxLength(第 614 行)。
3.4.3 最原始入口
McNetwork 还留了两个 *Raw 方法,暴露全部参数(McNetwork.java:311-331):
311→ public byte[] readDeviceBatchRaw(EMcCommand command, int subCommand, EMcDeviceCode deviceCode,
312→ int headDeviceNumber, int devicePointsCount) {
313→ try {
314→ McHeaderReq header = McHeaderReq.createByFrameType(this.frameType, this.accessRoute, this.monitoringTimer);
315→ McDeviceAddress deviceAddress = new McDeviceAddress(deviceCode, headDeviceNumber, devicePointsCount);
316→ McReadDeviceBatchReqData data = new McReadDeviceBatchReqData();
317→ data.setSeries(this.series);
318→ data.setCommand(command);
319→ data.setSubcommand(subCommand);
320→ data.setDeviceAddress(deviceAddress);
321→ McMessageReq req = new McMessageReq(header, data);
322→ req.selfCheck();
323→ McMessageAck ack = this.readFromServer(req);
324→ McAckData ackData = (McAckData) ack.getData();
325→ return ackData.getData();
这是很好的"逃生舱"设计——高级 API 覆盖不了的场景可以直接手工组包。
3.4.4 限制校验
MC 模块做了大量前置校验,这是三个协议里最严谨的。例如 checkDeviceRandomCode()(McNetwork.java:648-655)禁止随机访问长定时器/计数器:
648→ private boolean checkDeviceRandomCode(List<? extends McDeviceAddress> addresses) {
649→ return addresses.stream().allMatch(x -> x.getDeviceCode() != EMcDeviceCode.LTS
650→ && x.getDeviceCode() != EMcDeviceCode.LTC
651→ && x.getDeviceCode() != EMcDeviceCode.LSTS
652→ && x.getDeviceCode() != EMcDeviceCode.LSTC
653→ && x.getDeviceCode() != EMcDeviceCode.LCS
654→ && x.getDeviceCode() != EMcDeviceCode.LCC);
655→ }
以及多块读写的帧限制(McNetwork.java:803-810):
803→ if (this.series.getFrameType() == EMcFrameType.FRAME_1E) {
804→ throw new McCommException("Frame 1E not supported in writing device batch multi blocks");
805→ }
806→ // TODO: 待确认
807→ if (this.frameType == EMcFrameType.FRAME_3E) {
808→ // 3E暂不支持批量块读写
809→ throw new McCommException("3E Currently does not support batch block read and write");
810→ }
⚠️ 重要限制:3E 帧不支持多块批量读写(第 807-810 行,作者标了 "TODO: 待确认")。而 FX5U / Q 系列默认就是 3E 帧 → 意味着
readDeviceBatchMultiBlocks在这些 PLC 上用不了,只能走单块readDeviceBatchInWord或随机读readDeviceRandomInWord。
第四部分:三大协议横向对比
| 维度 | Modbus | S7 | MC |
|---|---|---|---|
| 门面类 | ModbusTcp 等 3 个 |
S7PLC |
McPLC |
| 门面行数 | 1522(骨架) | 1440 | 795 |
| 支持模式 | 3(TCP/RTUoT/ASCIIoT) | 1(ISO-on-TCP) | 3(1E/3E/4E) |
| 报文顶层类 | MbTcpRequest |
S7Data |
McMessageReq |
| 层次数 | 2(头 + PDU) | 4(TPKT+COTP+Header+Param/Datum) | 2(头 + Data) |
| 字节序 | 大端 | 大端 | 小端 |
| 长度字段 | MBAP len | TPKT len | 3E/4E dataLength |
| 校验 | CRC16 / LRC | 无 | 无(endCode) |
| 事务标识 | transactionId |
pduReference |
4E serialNumber |
| 应用层握手 | 无 | ✅ COTP CR + Setup | 无 |
| 容量协商 | 无 | ✅ PDU 协商 | 无(靠 EMcSeries 静态表) |
| 单请求项跨包拆分 | ❌ 无 | ✅ 有(S7SequentialGroupAlg) |
❌ 无 |
| 多不连续地址合并 | ❌ 无 | ✅ 有(MultiAddressRead) |
✅ 有(randomInWord) |
| 地址解析复杂度 | 低(纯 int) | 中(正则 + 分段) | 高(软元件表 + 进制) |
| 服务端 | ✅ ModbusTcpServer |
✅ S7PLCServer |
❌ 无 |
| 注解序列化 | ❌ | ✅ S7Serializer |
❌ |
| 特殊能力 | — | NCK 机床、上传下载 | — |
| 每个协议的前置校验 | 少 | 中 | 最多(软元件类型/系列/帧限制) |
能力排序(个人观察):S7 > MC > Modbus。
- S7 最完整:握手、PDU 协商、单请求跨包拆分、多地址合并、服务端、注解序列化、NCK 机床、程序块上传下载——全部都有;
- MC 次之:多地址随机读、多块批量、跨系列适配都做了,但缺服务端和序列化,且 3E 帧有多块限制;
- Modbus 最薄:只有自动分包,没有地址合并、没有服务端外的额外能力;且模块注释里还有多处 TODO / 被注释的旧代码。
设计优点 / 缺点 / 可借鉴点
✅ 设计优点
- Modbus 的四层解耦(枚举 → 报文 → 骨架 → 传输):
ModbusSkeletonAbstract<T,R>用泛型 + 3 个抽象方法复用了全部数据类型 API,TCP/RTU/ASCII 只写 ~100 行各自实现。全库最成功的抽象。 - S7 的分包算法真正解决了"单请求项超长":
S7SequentialGroupAlg支持把 400 字节的请求切成64|221|115跨组传输并拼回,比很多同类库的"一个请求项不能超 PDU"要强。 - MC 的容量参数表化:
EMcSeries把 4 个系列 × 11 个协议容量参数集中在一处,批量读写的上限判断全部查表,没有魔数散落。 - 逐字节注释 + 实测结论入库:TSAP 取值(
PLCNetwork.java:161)、PDU 大小推算(PLCNetwork.java:50-58)、字符串首字节偏移(S7Serializer.java:201-203)——作者把真机调试的结论写进了注释和代码,这是最有价值的部分。 selfCheck()统一回填长度/校验码:三个协议一致采用,避免"改 body 忘改 header length"。- MC 的
Raw逃生舱方法:readDeviceBatchRaw/writeDeviceBatchRaw暴露全部参数,高级 API 覆盖不了时可手工组包。 - 服务端错误不抛异常:
ModbusTcpServer把异常转成 Modbus 异常响应(|0x80+ 异常码),一个客户端的错误不影响连接。
❌ 设计缺点
- 命名与文档不一致:题面提到的
S7Client/McClient在源码中不存在(实际是S7PLC/McPLC),外部使用者容易找不到入口。 - Modbus 无地址合并:读 10 个不相邻地址 = 10 次往返,是三兄弟里最弱的批量能力。
- MC 的 3E 帧不支持多块批量读写(
McNetwork.java:807-810),而 FX5U/Q 系列默认就是 3E —— 主流机型用不了这个能力。 MbapHeader.getNewNumber()的归零非原子(MbapHeader.java:108-115):getAndIncrement()后判断再set(0),高并发下可能短暂返回 >65536 的值。- 地址解析鲁棒性不足:
AddressUtil.extractNumber("")会抛NumberFormatException;McDeviceAddress的旧实现被注释掉留了 38 行死代码(McDeviceAddress.java:157-194)。 - 字节序容易踩坑:MC 全程
ByteWriteBuff.newInstance(len, true)小端,S7/Modbus 默认大端(false)。混用协议时若复制代码会静默出错。 - 字符集默认值不一致:
S7Serializer默认 GB2312,ByteArraySerializer默认 US_ASCII。 McPLC.readFloat64(String...)/(List<String>)被注释掉(McPLC.java:397,407),多地址读 double 是缺口。- MC 无服务端实现(Modbus 和 S7 都有)。
- 多处 TODO / 待确认:
EMcSeries的 IQ_R 帧类型、McNetwork的 3E 多块限制、ModbusSkeletonAbstract的 125 上限——说明这些点未经充分实测。
💡 可借鉴点
| 做法 | 借鉴到 |
|---|---|
selfCheck() 回填长度/校验码 |
任何需要"header 里有 body 长度"的协议 |
ModbusSkeletonAbstract<T,R> 泛型骨架 + 3 个抽象方法 |
同一协议多种传输变体(TCP/RTU/ASCII/UDP) |
EMcSeries 把协议容量表化 |
多型号/多系列设备的参数集中管理,避免魔数 |
S7SequentialGroupAlg 的 extraSize + threshold 双参数模型 |
需要"按协议开销精确分包"的场景 |
| 把真机实测结论写进注释(TSAP、PDU 推算、字符串首字节) | 协议类库的文档价值核心 |
McReqBuilder 静态工厂集中构造报文 |
报文构造逻辑收拢,与网络层解耦 |
Raw 逃生舱方法 |
高级 API 与原始能力的平衡 |
McDeviceCode 一个软元件维护 3 套编码 |
同一概念在不同协议版本/系列下的编码差异 |
| 服务端把异常转成协议错误响应而非抛异常 | 服务端健壮性 |
软元件自带进制(getNotation()) |
地址字符串解析(X/Y 十六进制、D/M 十进制) |
下一篇:基础设施与工程特性 —— common 包的 Buff / EDataType / 注解序列化 / LoopGroupAlg、异常体系、回调机制、线程安全、设计模式、src/test 结构与流媒体模块。
本文是《工业通信与数据采集》栏目第 17/22 篇 · 上一篇:协议实现详解(上):Modbus 与 S7 | 栏目总览 | 下一篇:基础设施与工程特性