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→    }

两个关键设计:

  1. identifyDeviceCodeByStart()(EMcDeviceCode,521 行的软元件码表):按前缀匹配识别软元件,支持 X、Y、D、M、L、B、W、R、ZR 等;
  2. 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。


设计优点 / 缺点 / 可借鉴点

✅ 设计优点

  1. Modbus 的四层解耦(枚举 → 报文 → 骨架 → 传输):ModbusSkeletonAbstract<T,R> 用泛型 + 3 个抽象方法复用了全部数据类型 API,TCP/RTU/ASCII 只写 ~100 行各自实现。全库最成功的抽象。
  2. S7 的分包算法真正解决了"单请求项超长":S7SequentialGroupAlg 支持把 400 字节的请求切成 64|221|115 跨组传输并拼回,比很多同类库的"一个请求项不能超 PDU"要强。
  3. MC 的容量参数表化:EMcSeries 把 4 个系列 × 11 个协议容量参数集中在一处,批量读写的上限判断全部查表,没有魔数散落。
  4. 逐字节注释 + 实测结论入库:TSAP 取值(PLCNetwork.java:161)、PDU 大小推算(PLCNetwork.java:50-58)、字符串首字节偏移(S7Serializer.java:201-203)——作者把真机调试的结论写进了注释和代码,这是最有价值的部分。
  5. selfCheck() 统一回填长度/校验码:三个协议一致采用,避免"改 body 忘改 header length"。
  6. MC 的 Raw 逃生舱方法:readDeviceBatchRaw / writeDeviceBatchRaw 暴露全部参数,高级 API 覆盖不了时可手工组包。
  7. 服务端错误不抛异常:ModbusTcpServer 把异常转成 Modbus 异常响应(|0x80 + 异常码),一个客户端的错误不影响连接。

❌ 设计缺点

  1. 命名与文档不一致:题面提到的 S7Client / McClient 在源码中不存在(实际是 S7PLC / McPLC),外部使用者容易找不到入口。
  2. Modbus 无地址合并:读 10 个不相邻地址 = 10 次往返,是三兄弟里最弱的批量能力。
  3. MC 的 3E 帧不支持多块批量读写(McNetwork.java:807-810),而 FX5U/Q 系列默认就是 3E —— 主流机型用不了这个能力。
  4. MbapHeader.getNewNumber() 的归零非原子(MbapHeader.java:108-115):getAndIncrement() 后判断再 set(0),高并发下可能短暂返回 >65536 的值。
  5. 地址解析鲁棒性不足:AddressUtil.extractNumber("") 会抛 NumberFormatException;McDeviceAddress 的旧实现被注释掉留了 38 行死代码(McDeviceAddress.java:157-194)。
  6. 字节序容易踩坑:MC 全程 ByteWriteBuff.newInstance(len, true) 小端,S7/Modbus 默认大端(false)。混用协议时若复制代码会静默出错。
  7. 字符集默认值不一致:S7Serializer 默认 GB2312,ByteArraySerializer 默认 US_ASCII。
  8. McPLC.readFloat64(String...) / (List<String>) 被注释掉(McPLC.java:397,407),多地址读 double 是缺口。
  9. MC 无服务端实现(Modbus 和 S7 都有)。
  10. 多处 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 | 栏目总览 | 下一篇:基础设施与工程特性