Skip to content

Java使用DLT698协议进行串口通信 #360

@RandolphChin

Description

@RandolphChin

DL/T 698.45 电力通信协议,用于通过串口读取电能表的电压和电流数据。实现已通过 iESlab 工具验证。

协议帧格式

┌─────────────────────────────────────────────────────────────────┐
│ 前导符(物理层)                                                 │
│ FE FE FE FE                                                     │
├─────────────────────────────────────────────────────────────────┤
│ 起始符                                                          │
│ 68                                                              │
├─────────────────────────────────────────────────────────────────┤
│ 长度域(L,小端序)                                              │
│ LL HH                                                           │
├─────────────────────────────────────────────────────────────────┤
│ 控制域(C)                                                      │
│ 1字节                                                           │
├─────────────────────────────────────────────────────────────────┤
│ 地址域(A)                                                      │
│ 地址类型(1) + 服务器地址(6) + 客户机地址(1) = 8字节              │
├─────────────────────────────────────────────────────────────────┤
│ HCS 帧头校验(2字节,小端序)                                    │
│ 校验范围:长度域到地址域结束                                     │
├─────────────────────────────────────────────────────────────────┤
│ APDU(应用协议数据单元)                                         │
│ 服务类型(1) + PIID(1) + 服务选择(1) + OAD(4) + ...             │
├─────────────────────────────────────────────────────────────────┤
│ FCS 帧校验(2字节,小端序)                                      │
│ 校验范围:长度域到APDU结束                                       │
├─────────────────────────────────────────────────────────────────┤
│ 结束符                                                          │
│ 16                                                              │
└─────────────────────────────────────────────────────────────────┘
  • 前导符(FE*N):若干个 0xFE,用于唤醒从站设备和同步(不计入长度)
  • 起始符(68):帧起始标志 0x68(不计入长度)
  • 长度域(2字节):从控制域开始到FCS之前的字节数,小端序
    • 计算公式:控制(1) + 地址域(8) + HCS(2) + APDU(N) + FCS(2)
  • 控制域(1字节)
    • bit7:DIR(0=主站发出,1=从站响应)
    • bit6:PRM(1=主站)
    • bit5:分帧标志(0=无后续帧)
    • bit0-4:功能码(00011=请求帧)
  • 地址域(8字节)
    • 地址类型(1字节):0x45表示逻辑地址,长度5+1字节
    • 服务器地址(6字节):BCD编码的电能表地址
    • 客户机地址(1字节):主站地址
  • HCS(2字节):帧头校验,CRC16,小端序
    • 校验范围:从长度域到地址域结束(共11字节)
  • APDU(N字节):应用协议数据单元
    • GET请求:服务类型(1) + PIID(1) + 服务选择(1) + OAD(4) + 时间标签(1)
    • GET响应:服务类型(1) + PIID-ACD(1) + 服务选择(1) + OAD(4) + Result(1) + Data(N)
  • FCS(2字节):帧校验,CRC16,小端序
    • 校验范围:从长度域到APDU结束
  • 结束符(16):帧结束标志 0x16(不计入长度)

CRC-16算法(关键突破)

通过暴力搜索找到正确的CRC参数:

  • 多项式: 0x8408 (低位先行LSB first)
  • 初始值: 0xFFFF
  • 异或输出: 0xFFFF
  • 方向: 低位先行(低位先行LSB first)

APDU结构(8字节)

Byte 0: 0x05 - 服务类型 (GET-Request)
Byte 1: 0x01 - PIID (服务优先级和序号)
Byte 2: 0x00 - 时间标签选择
Byte 3-6: OAD (对象属性描述符,4字节)
Byte 7: 0x00 - 时间标签

长度域计算

长度值 = 长度域本身(2) + 控制(1) + 地址域(8) + HCS(2) + APDU(8) + FCS(2)
       = 23字节

HCS(帧头校验):

  • 校验范围:从长度域到地址域结束(共11字节)
  • 算法:CRC16/CCITT-FALSE
  • 字节序:小端序

FCS(帧校验):

  • 校验范围:从长度域到APDU结束
  • 算法:CRC16/CCITT-FALSE
  • 字节序:小端序
// 读取地址报文
FE FE FE FE 68 17 00 43 45 AA AA AA AA AA AA A0 51 EA 05 01 01 40 01 02 00 00 C6 07 16

示例2:电压数据响应

完整报文:

FE FE FE FE 68 24 00 C3 45 00 00 00 00 00 00 00 28 B5 
85 01 05 20 00 02 00 01 01 03 12 08 98 12 0D AC 12 0D AC 00 00 
FF B7 16

APDU结构:

85: GET-Response
01: PIID-ACD
05: GetResponseNormal ← 关键字段
20 00 02 00: OAD (电压_分相数值组)
01: Result (Data)
01: 数据类型 (array)
03: 数组元素数量
  12 08 98: A相 (220.0V)
  12 0D AC: B相 (350.0V)
  12 0D AC: C相 (350.0V)
00: 跟随上报
00: 时间标签

APDU长度: 23字节
总长度: 1 + 8 + 2 + 23 + 2 = 36字节 ✓

注意事项

  1. CRC16算法:使用 CRC16/CCITT-FALSE,多项式0x1021,小端序
  2. 地址编码:服务器地址使用BCD编码
  3. 数据类型:响应中的数据类型标识(0x12 = long-unsigned)
  4. 数值转换
    • 电压:原始值 × 0.1 = 实际值(V)
    • 电流:原始值 × 0.001 = 实际值(A)

Java实现

库依赖

        <!-- Serial Port Communication -->
        <dependency>
            <groupId>com.fazecast</groupId>
            <artifactId>jSerialComm</artifactId>
            <version>2.10.4</version>
        </dependency>

构建请求和响应DLT698Protocol.java


import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * DL/T 698.45 电能表通信协议处理类
 *
 * 协议帧格式:
 * +------+------+------+------+------+------+------+
 * | 前导 | 起始 | 长度 | 控制 | 地址 | HCS  | APDU |
 * +------+------+------+------+------+------+------+
 * | FE*N | 68   | 2B   | 1B   | 8B   | 2B   | N    |
 * +------+------+------+------+------+------+------+
 * +------+------+
 * | FCS  | 结束 |
 * +------+------+
 * | 2B   | 16   |
 * +------+------+
 *
 * @author ywhc
 * @date 2025-10-15
 */
public class DLT698Protocol {

    // 帧起始符和结束符
    private static final byte FRAME_START = 0x68;
    private static final byte FRAME_END = 0x16;
    private static final byte PREAMBLE = (byte) 0xFE;

    // 服务类型
    public static final byte GET_REQUEST_NORMAL = 0x05;
    public static final byte GET_REQUEST_NORMAL_LIST = 0x02;  // 读取若干个对象属性请求
    public static final byte GET_RESPONSE_NORMAL = (byte) 0x85;
    public static final byte GET_RESPONSE_NORMAL_LIST = 0x02;  // 读取若干个对象属性响应
    public static final byte ERROR_RESPONSE = (byte) 0xEE;  // 异常响应

    // 常用OAD(对象属性描述符)
    public static final int OAD_VOLTAGE = 0x20000200;  // 电压_分相数值组
    public static final int OAD_CURRENT = 0x20010200;  // 电流_分相数值组

    /**
     * 电能表数据结果类
     */
    public static class MeterData {
        private String oad;
        private String description;
        private List<PhaseData> phases;

        public MeterData(String oad, String description) {
            this.oad = oad;
            this.description = description;
            this.phases = new ArrayList<>();
        }

        public void addPhase(String phaseName, double value, String unit) {
            phases.add(new PhaseData(phaseName, value, unit));
        }

        public String getOad() {
            return oad;
        }

        public String getDescription() {
            return description;
        }

        public List<PhaseData> getPhases() {
            return phases;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(description).append(" (OAD: ").append(oad).append(")\n");
            for (PhaseData phase : phases) {
                sb.append("  ").append(phase).append("\n");
            }
            return sb.toString();
        }
    }

    /**
     * 相数据类
     */
    public static class PhaseData {
        private String phaseName;
        private double value;
        private String unit;

        public PhaseData(String phaseName, double value, String unit) {
            this.phaseName = phaseName;
            this.value = value;
            this.unit = unit;
        }

        public String getPhaseName() {
            return phaseName;
        }

        public double getValue() {
            return value;
        }

        public String getUnit() {
            return unit;
        }

        @Override
        public String toString() {
            return String.format("%s: %.1f%s", phaseName, value, unit);
        }
    }

    /**
     * 构建GET请求报文(读取电能表数据)
     *
     * @param address 电能表地址(6字节)
     * @param oad 对象属性描述符(4字节)
     * @param piid 服务优先级和序号(1字节)
     * @return 完整的请求报文
     */
    public static byte[] buildGetRequest(byte[] address, int oad, byte piid) {
        if (address == null || address.length != 6) {
            throw new IllegalArgumentException("地址必须为6字节");
        }

        // 构建APDU部分
        // 根据iESlab实际报文:APDU = 05 01 00 20 00 02 00 00 (8字节)
        // 结构:服务类型(1) + PIID(1) + 时间标签选择(1) + OAD(4) + 时间标签(1)
        ByteBuffer apdu = ByteBuffer.allocate(8);
        apdu.put(GET_REQUEST_NORMAL);           // [0] 0x05 服务类型:GET-Request
        apdu.put(piid);                         // [1] PIID
        apdu.put((byte) 0x00);                  // [2] 时间标签选择:无时间标签
        apdu.put((byte) ((oad >> 24) & 0xFF));  // [3] OAD高字节
        apdu.put((byte) ((oad >> 16) & 0xFF));  // [4] OAD
        apdu.put((byte) ((oad >> 8) & 0xFF));   // [5] OAD
        apdu.put((byte) (oad & 0xFF));          // [6] OAD低字节
        apdu.put((byte) 0x00);                  // [7] 时间标签:无

        byte[] apduBytes = apdu.array();

        // 计算长度域的值
        // 根据iESlab:长度 = 长度域本身(2) + 控制(1) + 地址域(8) + HCS(2) + APDU(n) + FCS(2)
        int lengthValue = 2 + 1 + 8 + 2 + apduBytes.length + 2;

        // 构建帧内容(从长度域开始,不包括前导和起始符)
        // frameData大小 = lengthValue + 结束符(1)
        ByteBuffer frameData = ByteBuffer.allocate(lengthValue + 1);

        // 1. 长度字段(2字节,小端序)
        frameData.put((byte) (lengthValue & 0xFF));
        frameData.put((byte) ((lengthValue >> 8) & 0xFF));

        // 2. 控制字段(1字节)
        // bit7=0(主站发出), bit6=1, bit5=0(无分帧), bit0-4=00011(请求帧)
        byte control = 0x43;
        frameData.put(control);

        // 3. 地址域(8字节)
        // 地址类型(1) + 服务器地址(6) + 客户机地址(1)
        frameData.put((byte) 0x45);  // 地址类型:逻辑地址,长度5+1
        frameData.put(address);      // 6字节服务器地址
        frameData.put((byte) 0x00);  // 客户机地址

        // 4. 计算HCS(帧头校验)
        // HCS校验范围:从长度域到地址域结束(共11字节)
        byte[] headerForHCS = new byte[11];
        frameData.position(0);
        frameData.get(headerForHCS);
        byte[] hcs = calculateFCS(headerForHCS);
        frameData.put(hcs);

        // 5. APDU数据
        frameData.put(apduBytes);

        // 6. 计算FCS(帧校验)
        // FCS校验范围:从长度域到APDU结束
        // lengthValue包含了长度域本身,所以实际数据长度 = lengthValue - 2(FCS)
        byte[] dataForFCS = new byte[lengthValue - 2];
        frameData.position(0);
        frameData.get(dataForFCS);
        byte[] fcs = calculateFCS(dataForFCS);
        frameData.put(fcs);

        // 7. 结束符
        frameData.put(FRAME_END);

        // 构建完整报文:起始符 + 帧数据
        // 注意:不添加前导符FE,因为iESlab显示的报文不包含前导符
        byte[] frameBytes = frameData.array();
        ByteBuffer fullPacket = ByteBuffer.allocate(1 + frameBytes.length);

        // 起始符
        fullPacket.put(FRAME_START);

        // 帧内容
        fullPacket.put(frameBytes);

        return fullPacket.array();
    }

    /**
     * 构建读取电压数据的请求报文
     *
     * @param address 电能表地址(6字节)
     * @param piid 服务序号(默认0x01)
     * @return 完整的请求报文
     */
    public static byte[] buildVoltageRequest(byte[] address, byte piid) {
        return buildGetRequest(address, OAD_VOLTAGE, piid);
    }

    /**
     * 构建读取电流数据的请求报文
     *
     * @param address 电能表地址(6字节)
     * @param piid 服务序号(默认0x01)
     * @return 完整的请求报文
     */
    public static byte[] buildCurrentRequest(byte[] address, byte piid) {
        return buildGetRequest(address, OAD_CURRENT, piid);
    }

    /**
     * 构建GET请求报文(读取多个对象属性)- GetRequestNormalList
     *
     * @param address 电能表地址(6字节)
     * @param oads 对象属性描述符数组(每个4字节)
     * @param piid 服务优先级和序号(1字节)
     * @return 完整的请求报文
     */
    public static byte[] buildGetRequestList(byte[] address, int[] oads, byte piid) {
        if (address == null || address.length != 6) {
            throw new IllegalArgumentException("地址必须为6字节");
        }
        if (oads == null || oads.length == 0) {
            throw new IllegalArgumentException("OAD数组不能为空");
        }
        if (oads.length > 255) {
            throw new IllegalArgumentException("OAD数量不能超过255");
        }

        // 构建APDU部分
        // 结构:服务类型(1) + 服务子类型(1) + PIID(1) + OAD数量(1) + OAD列表(4*N) + 时间标签(1)
        int apduLength = 1 + 1 + 1 + 1 + (oads.length * 4) + 1;
        ByteBuffer apdu = ByteBuffer.allocate(apduLength);
        
        apdu.put(GET_REQUEST_NORMAL);           // [0] 0x05 服务类型:GET-Request
        apdu.put(GET_REQUEST_NORMAL_LIST);      // [1] 0x02 子类型:读取若干个对象属性请求
        apdu.put(piid);                         // [2] PIID
        apdu.put((byte) oads.length);           // [3] OAD数量
        
        // 添加所有OAD
        for (int oad : oads) {
            apdu.put((byte) ((oad >> 24) & 0xFF));  // OAD高字节
            apdu.put((byte) ((oad >> 16) & 0xFF));
            apdu.put((byte) ((oad >> 8) & 0xFF));
            apdu.put((byte) (oad & 0xFF));          // OAD低字节
        }
        
        apdu.put((byte) 0x00);                  // 时间标签:无

        byte[] apduBytes = apdu.array();

        // 计算长度域的值
        int lengthValue = 2 + 1 + 8 + 2 + apduBytes.length + 2;

        // 构建帧内容
        ByteBuffer frameData = ByteBuffer.allocate(lengthValue + 1);

        // 1. 长度字段(2字节,小端序)
        frameData.put((byte) (lengthValue & 0xFF));
        frameData.put((byte) ((lengthValue >> 8) & 0xFF));

        // 2. 控制字段(1字节)
        byte control = 0x43;
        frameData.put(control);

        // 3. 地址域(8字节)
        frameData.put((byte) 0x45);  // 地址类型
        frameData.put(address);      // 6字节服务器地址
        frameData.put((byte) 0x00);  // 客户机地址

        // 4. 计算HCS(帧头校验)
        byte[] headerForHCS = new byte[11];
        frameData.position(0);
        frameData.get(headerForHCS);
        byte[] hcs = calculateFCS(headerForHCS);
        frameData.put(hcs);

        // 5. APDU数据
        frameData.put(apduBytes);

        // 6. 计算FCS(帧校验)
        byte[] dataForFCS = new byte[lengthValue - 2];
        frameData.position(0);
        frameData.get(dataForFCS);
        byte[] fcs = calculateFCS(dataForFCS);
        frameData.put(fcs);

        // 7. 结束符
        frameData.put(FRAME_END);

        // 构建完整报文
        byte[] frameBytes = frameData.array();
        ByteBuffer fullPacket = ByteBuffer.allocate(1 + frameBytes.length);

        // 起始符
        fullPacket.put(FRAME_START);

        // 帧内容
        fullPacket.put(frameBytes);

        return fullPacket.array();
    }

    /**
     * 构建同时读取电压和电流数据的请求报文
     *
     * @param address 电能表地址(6字节)
     * @param piid 服务序号(默认0x04)
     * @return 完整的请求报文
     */
    public static byte[] buildVoltageAndCurrentRequest(byte[] address, byte piid) {
        return buildGetRequestList(address, new int[]{OAD_VOLTAGE, OAD_CURRENT}, piid);
    }

    /**
     * 解析GET响应报文
     *
     * @param response 响应报文
     * @return 解析后的电能表数据
     * @throws IllegalArgumentException 如果报文格式错误
     */
    public static MeterData parseGetResponse(byte[] response) {
        if (response == null || response.length < 20) {
            throw new IllegalArgumentException("响应报文长度不足");
        }

        // 跳过前导符,找到起始符
        int startIndex = 0;
        while (startIndex < response.length && response[startIndex] == PREAMBLE) {
            startIndex++;
        }

        if (startIndex >= response.length || response[startIndex] != FRAME_START) {
            throw new IllegalArgumentException("未找到起始符0x68");
        }

        startIndex++; // 跳过起始符

        // 读取长度字段(小端序)
        int length = (response[startIndex] & 0xFF) | ((response[startIndex + 1] & 0xFF) << 8);

        // 验证结束符
        // 长度域的值包含了长度域本身,所以结束符位置 = startIndex + length
        int endIndex = startIndex + length;
        if (endIndex >= response.length || response[endIndex] != FRAME_END) {
            throw new IllegalArgumentException(
                String.format("结束符错误或报文不完整。期望位置[%d]=0x16,实际=0x%02X,报文长度=%d",
                    endIndex,
                    endIndex < response.length ? response[endIndex] & 0xFF : -1,
                    response.length));
        }

        // 跳过:长度(2) + 控制(1) + 地址域(8) + HCS(2) = 13字节
        int apduStart = startIndex + 13;

        // 解析APDU
        int apduIndex = apduStart;

        // 1. 服务类型(1字节)
        byte serviceType = response[apduIndex++];

        // 检查是否为错误响应
        if (serviceType == ERROR_RESPONSE) {
            // 错误响应格式:服务类型(1) + PIID(1) + DAR(1)
            byte piid = response[apduIndex++];
            byte dar = response[apduIndex++];  // DAR: Data Access Result
            throw new IllegalArgumentException(
                String.format("电表返回错误响应 [PIID=0x%02X, DAR=0x%02X]。可能原因:PIID不匹配、OAD不支持或权限不足",
                    piid & 0xFF, dar & 0xFF));
        }

        if (serviceType != GET_RESPONSE_NORMAL) {
            throw new IllegalArgumentException(
                String.format("不支持的服务类型: 0x%02X", serviceType & 0xFF));
        }

        // 2. PIID-ACD(1字节)
        // byte piidAcd = response[apduIndex++];  // 服务优先级和序号
        apduIndex++;  // 跳过PIID-ACD字段

        // 3. 时间标签选择或其他字段(1字节)
        // byte timeTagOrOther = response[apduIndex++];
        apduIndex++;  // 跳过该字段

        // 4. OAD(4字节)
        int oad = ((response[apduIndex] & 0xFF) << 24) |
                  ((response[apduIndex + 1] & 0xFF) << 16) |
                  ((response[apduIndex + 2] & 0xFF) << 8) |
                  (response[apduIndex + 3] & 0xFF);
        apduIndex += 4;

        // 5. Result标志(1字节)
        byte result = response[apduIndex++];
        if (result != 0x01) {
            throw new IllegalArgumentException("响应结果不是Data类型");
        }

        // 数据类型(array)
        byte dataType = response[apduIndex++];
        if (dataType != 0x01) {
            throw new IllegalArgumentException("数据类型不是array");
        }

        // 数组元素数量
        int arrayCount = response[apduIndex++] & 0xFF;

        // 创建结果对象
        String oadStr = String.format("%08X", oad);
        String description = getOADDescription(oad);
        MeterData meterData = new MeterData(oadStr, description);

        // 解析数组元素
        String[] phaseNames = {"A相", "B相", "C相"};
        String unit = getOADUnit(oad);

        for (int i = 0; i < arrayCount && i < phaseNames.length; i++) {
            // 读取数据类型
            byte elementType = response[apduIndex++];
            
            // 根据数据类型读取不同长度的数值
            long rawValue;
            if (elementType == 0x05) {
                // double-long-unsigned (4字节,大端序)
                rawValue = ((response[apduIndex] & 0xFFL) << 24) |
                          ((response[apduIndex + 1] & 0xFFL) << 16) |
                          ((response[apduIndex + 2] & 0xFFL) << 8) |
                          (response[apduIndex + 3] & 0xFFL);
                apduIndex += 4;
            } else if (elementType == 0x12) {
                // long-unsigned (2字节,大端序)
                rawValue = ((response[apduIndex] & 0xFF) << 8) | 
                          (response[apduIndex + 1] & 0xFF);
                apduIndex += 2;
            } else {
                throw new IllegalArgumentException(
                    String.format("不支持的数据类型: 0x%02X", elementType & 0xFF));
            }

            // 转换为实际值(电压单位:0.1V,电流单位:0.001A)
            double value = convertValue(oad, rawValue);

            meterData.addPhase(phaseNames[i], value, unit);
        }

        return meterData;
    }

    /**
     * 解析GET响应报文(读取多个对象属性)- GetResponseNormalList
     *
     * @param response 响应报文
     * @return 解析后的电能表数据列表
     * @throws IllegalArgumentException 如果报文格式错误
     */
    public static List<MeterData> parseGetResponseList(byte[] response) {
        if (response == null || response.length < 20) {
            throw new IllegalArgumentException("响应报文长度不足");
        }

        // 跳过前导符,找到起始符
        int startIndex = 0;
        while (startIndex < response.length && response[startIndex] == PREAMBLE) {
            startIndex++;
        }

        if (startIndex >= response.length || response[startIndex] != FRAME_START) {
            throw new IllegalArgumentException("未找到起始符0x68");
        }

        startIndex++; // 跳过起始符

        // 读取长度字段(小端序)
        int length = (response[startIndex] & 0xFF) | ((response[startIndex + 1] & 0xFF) << 8);

        // 验证结束符
        int endIndex = startIndex + length;
        if (endIndex >= response.length || response[endIndex] != FRAME_END) {
            throw new IllegalArgumentException(
                String.format("结束符错误或报文不完整。期望位置[%d]=0x16,实际=0x%02X,报文长度=%d",
                    endIndex,
                    endIndex < response.length ? response[endIndex] & 0xFF : -1,
                    response.length));
        }

        // 跳过:长度(2) + 控制(1) + 地址域(8) + HCS(2) = 13字节
        int apduStart = startIndex + 13;

        // 解析APDU
        int apduIndex = apduStart;

        // 1. 服务类型(1字节)
        byte serviceType = response[apduIndex++];

        // 检查是否为错误响应
        if (serviceType == ERROR_RESPONSE) {
            byte piid = response[apduIndex++];
            byte dar = response[apduIndex++];
            throw new IllegalArgumentException(
                String.format("电表返回错误响应 [PIID=0x%02X, DAR=0x%02X]。可能原因:PIID不匹配、OAD不支持或权限不足",
                    piid & 0xFF, dar & 0xFF));
        }

        if (serviceType != GET_RESPONSE_NORMAL) {
            throw new IllegalArgumentException(
                String.format("不支持的服务类型: 0x%02X", serviceType & 0xFF));
        }

        // 2. 服务子类型(1字节)
        byte serviceSubType = response[apduIndex++];
        if (serviceSubType != GET_RESPONSE_NORMAL_LIST) {
            throw new IllegalArgumentException(
                String.format("不支持的服务子类型: 0x%02X,期望0x02", serviceSubType & 0xFF));
        }

        // 3. PIID-ACD(1字节)
        apduIndex++;  // 跳过PIID-ACD字段

        // 4. 结果数量(1字节)
        int resultCount = response[apduIndex++] & 0xFF;

        // 解析每个结果
        List<MeterData> resultList = new ArrayList<>();
        
        for (int resultIndex = 0; resultIndex < resultCount; resultIndex++) {
            // 读取OAD(4字节)
            int oad = ((response[apduIndex] & 0xFF) << 24) |
                      ((response[apduIndex + 1] & 0xFF) << 16) |
                      ((response[apduIndex + 2] & 0xFF) << 8) |
                      (response[apduIndex + 3] & 0xFF);
            apduIndex += 4;

            // Result标志(1字节)
            byte result = response[apduIndex++];
            if (result != 0x01) {
                throw new IllegalArgumentException(
                    String.format("结果[%d]的响应结果不是Data类型,实际值=0x%02X", resultIndex, result & 0xFF));
            }

            // 数据类型(array)
            byte dataType = response[apduIndex++];
            if (dataType != 0x01) {
                throw new IllegalArgumentException(
                    String.format("结果[%d]的数据类型不是array,实际值=0x%02X", resultIndex, dataType & 0xFF));
            }

            // 数组元素数量
            int arrayCount = response[apduIndex++] & 0xFF;

            // 创建结果对象
            String oadStr = String.format("%08X", oad);
            String description = getOADDescription(oad);
            MeterData meterData = new MeterData(oadStr, description);

            // 解析数组元素
            String[] phaseNames = {"A相", "B相", "C相"};
            String unit = getOADUnit(oad);

            for (int i = 0; i < arrayCount && i < phaseNames.length; i++) {
                // 读取数据类型
                byte elementType = response[apduIndex++];
                
                // 根据数据类型读取不同长度的数值
                long rawValue;
                if (elementType == 0x05) {
                    // double-long-unsigned (4字节,大端序)
                    rawValue = ((response[apduIndex] & 0xFFL) << 24) |
                              ((response[apduIndex + 1] & 0xFFL) << 16) |
                              ((response[apduIndex + 2] & 0xFFL) << 8) |
                              (response[apduIndex + 3] & 0xFFL);
                    apduIndex += 4;
                } else if (elementType == 0x12) {
                    // long-unsigned (2字节,大端序)
                    rawValue = ((response[apduIndex] & 0xFF) << 8) | 
                              (response[apduIndex + 1] & 0xFF);
                    apduIndex += 2;
                } else {
                    throw new IllegalArgumentException(
                        String.format("结果[%d]不支持的数据类型: 0x%02X", resultIndex, elementType & 0xFF));
                }

                // 转换为实际值
                double value = convertValue(oad, rawValue);
                meterData.addPhase(phaseNames[i], value, unit);
            }

            resultList.add(meterData);
        }

        return resultList;
    }

    /**
     * 计算FCS/HCS校验值
     * DL/T 698.45使用的CRC-16算法参数:
     * - 多项式: 0x8408 (LSB first)
     * - 初始值: 0xFFFF
     * - 异或输出: 0xFFFF
     * - 输入反转: 否
     * - 输出反转: 否
     *
     * @param data 需要计算校验的数据
     * @return 校验值(2字节,小端序)
     */
    private static byte[] calculateFCS(byte[] data) {
        int crc = 0xFFFF;  // 初始值

        // LSB first (低位先行)
        for (byte b : data) {
            crc ^= (b & 0xFF);  // 直接异或到低8位

            for (int i = 0; i < 8; i++) {
                if ((crc & 0x0001) != 0) {  // 检查最低位
                    crc = (crc >> 1) ^ 0x8408;  // 右移并异或多项式
                } else {
                    crc = crc >> 1;
                }
            }
        }

        // 异或输出
        crc = crc ^ 0xFFFF;

        // 返回小端序
        return new byte[] {
            (byte) (crc & 0xFF),         // 低字节
            (byte) ((crc >> 8) & 0xFF)   // 高字节
        };
    }

    /**
     * 验证FCS校验
     *
     * @param data 包含FCS的完整数据(FCS在最后2个字节)
     * @return true表示校验通过
     */
    public static boolean verifyFCS(byte[] data) {
        if (data == null || data.length < 3) {
            return false;
        }

        int dataLength = data.length - 2;
        byte[] dataOnly = new byte[dataLength];
        System.arraycopy(data, 0, dataOnly, 0, dataLength);

        byte[] calculated = calculateFCS(dataOnly);

        // 对比最后2字节
        return calculated[0] == data[data.length - 2] &&
               calculated[1] == data[data.length - 1];
    }

    /**
     * 获取OAD描述
     */
    private static String getOADDescription(int oad) {
        switch (oad) {
            case OAD_VOLTAGE:
                return "电压_分相数值组";
            case OAD_CURRENT:
                return "电流_分相数值组";
            default:
                return "未知OAD";
        }
    }

    /**
     * 获取OAD单位
     */
    private static String getOADUnit(int oad) {
        switch (oad) {
            case OAD_VOLTAGE:
                return "V";
            case OAD_CURRENT:
                return "A";
            default:
                return "";
        }
    }

    /**
     * 转换原始值为实际值
     */
    private static double convertValue(int oad, long rawValue) {
        switch (oad) {
            case OAD_VOLTAGE:
                // 电压单位:0.1V
                return rawValue / 10.0;
            case OAD_CURRENT:
                // 电流单位:0.001A
                return rawValue / 1000.0;
            default:
                return rawValue;
        }
    }

    /**
     * 将字节数组转换为十六进制字符串(用于调试和日志)
     *
     * @param bytes 字节数组
     * @return 十六进制字符串,每个字节用空格分隔
     */
    public static String bytesToHex(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b & 0xFF));
        }
        return sb.toString().trim();
    }

    /**
     * 将十六进制字符串转换为字节数组
     *
     * @param hexString 十六进制字符串,可以包含空格
     * @return 字节数组
     */
    public static byte[] hexToBytes(String hexString) {
        if (hexString == null || hexString.isEmpty()) {
            return new byte[0];
        }

        // 移除空格
        hexString = hexString.replace(" ", "");

        if (hexString.length() % 2 != 0) {
            throw new IllegalArgumentException("十六进制字符串长度必须为偶数");
        }

        byte[] bytes = new byte[hexString.length() / 2];
        for (int i = 0; i < bytes.length; i++) {
            int index = i * 2;
            bytes[i] = (byte) Integer.parseInt(hexString.substring(index, index + 2), 16);
        }
        return bytes;
    }

    /**
     * 解析地址字符串为字节数组
     *
     * @param addressStr 地址字符串(12位十六进制,如"000000000000")
     * @return 6字节地址数组
     */
    public static byte[] parseAddress(String addressStr) {
        if (addressStr == null || addressStr.length() != 12) {
            throw new IllegalArgumentException("地址必须为12位十六进制字符串");
        }
        return hexToBytes(addressStr);
    }
}


串口通信服务类SerialPortService.java

import com.fazecast.jSerialComm.SerialPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.TimeUnit;

/**
 * 串口通信服务类
 * 使用jSerialComm库实现串口通信
 * 
 * @author ywhc
 * @date 2025-10-15
 */
@Service
public class SerialPortService {
    
    private static final Logger log = LoggerFactory.getLogger(SerialPortService.class);
    
    private SerialPort serialPort;
    private boolean isConnected = false;
    
    // 默认串口配置
    private static final int DEFAULT_BAUD_RATE = 9600;
    private static final int DEFAULT_DATA_BITS = 8;
    private static final int DEFAULT_STOP_BITS = SerialPort.ONE_STOP_BIT;
    private static final int DEFAULT_PARITY = SerialPort.NO_PARITY;
    private static final int DEFAULT_TIMEOUT = 5000; // 5秒超时
    
    /**
     * 列出所有可用的串口
     * 
     * @return 串口名称数组
     */
    public String[] listAvailablePorts() {
        SerialPort[] ports = SerialPort.getCommPorts();
        String[] portNames = new String[ports.length];
        for (int i = 0; i < ports.length; i++) {
            portNames[i] = ports[i].getSystemPortName();
            log.info("发现串口: {} - {}", ports[i].getSystemPortName(), ports[i].getDescriptivePortName());
        }
        return portNames;
    }
    
    /**
     * 打开串口连接(使用默认配置)
     * 
     * @param portName 串口名称,例如 "COM3" 或 "/dev/ttyUSB0"
     * @return true表示成功,false表示失败
     */
    public boolean openPort(String portName) {
        return openPort(portName, DEFAULT_BAUD_RATE, DEFAULT_DATA_BITS, 
                       DEFAULT_STOP_BITS, DEFAULT_PARITY);
    }
    
    /**
     * 打开串口连接(自定义配置)
     * 
     * @param portName 串口名称
     * @param baudRate 波特率
     * @param dataBits 数据位
     * @param stopBits 停止位
     * @param parity 校验位
     * @return true表示成功,false表示失败
     */
    public boolean openPort(String portName, int baudRate, int dataBits, 
                           int stopBits, int parity) {
        try {
            // 如果已经连接,先关闭
            if (isConnected) {
                closePort();
            }
            
            // 获取串口
            serialPort = SerialPort.getCommPort(portName);
            
            // 配置串口参数
            serialPort.setComPortParameters(baudRate, dataBits, stopBits, parity);
            
            // 设置超时
            serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 
                                         DEFAULT_TIMEOUT, DEFAULT_TIMEOUT);
            
            // 打开串口
            if (serialPort.openPort()) {
                isConnected = true;
                log.info("串口打开成功: {} [波特率={}, 数据位={}, 停止位={}, 校验位={}]",
                        portName, baudRate, dataBits, stopBits, parity);
                return true;
            } else {
                log.error("串口打开失败: {}", portName);
                return false;
            }
        } catch (Exception e) {
            log.error("打开串口异常: {}", portName, e);
            return false;
        }
    }
    
    /**
     * 关闭串口连接
     */
    public void closePort() {
        if (serialPort != null && isConnected) {
            serialPort.closePort();
            isConnected = false;
            log.info("串口已关闭: {}", serialPort.getSystemPortName());
        }
    }
    
    /**
     * 检查串口是否已连接
     * 
     * @return true表示已连接
     */
    public boolean isConnected() {
        return isConnected && serialPort != null && serialPort.isOpen();
    }
    
    /**
     * 发送数据到串口
     * 
     * @param data 要发送的字节数组
     * @return true表示发送成功
     * @throws IOException 如果发送失败
     */
    public boolean sendData(byte[] data) throws IOException {
        if (!isConnected()) {
            throw new IOException("串口未连接");
        }
        
        try {
            OutputStream outputStream = serialPort.getOutputStream();
            outputStream.write(data);
            outputStream.flush();
            
            log.debug("发送数据: {} ({} 字节)", TD3600Protocol.bytesToHex(data), data.length);
            return true;
        } catch (Exception e) {
            log.error("发送数据失败", e);
            throw new IOException("发送数据失败: " + e.getMessage(), e);
        }
    }
    
    /**
     * 从串口接收数据
     * 
     * @param maxLength 最大接收长度
     * @param timeoutMs 超时时间(毫秒)
     * @return 接收到的字节数组
     * @throws IOException 如果接收失败或超时
     */
    public byte[] receiveData(int maxLength, int timeoutMs) throws IOException {
        return receiveData(maxLength, timeoutMs, (byte) 0x03);
    }
    
    /**
     * 从串口接收数据(支持自定义结束符)
     * 
     * @param maxLength 最大接收长度
     * @param timeoutMs 超时时间(毫秒)
     * @param endMarker 结束符(0表示不检查结束符,接收到数据后等待超时)
     * @return 接收到的字节数组
     * @throws IOException 如果接收失败或超时
     */
    public byte[] receiveData(int maxLength, int timeoutMs, byte endMarker) throws IOException {
        if (!isConnected()) {
            throw new IOException("串口未连接");
        }
        
        try {
            InputStream inputStream = serialPort.getInputStream();
            byte[] buffer = new byte[maxLength];
            
            long startTime = System.currentTimeMillis();
            int totalBytesRead = 0;
            long lastReceiveTime = startTime;
            
            // 读取数据直到超时或读取到完整报文
            while (System.currentTimeMillis() - startTime < timeoutMs) {
                int available = inputStream.available();
                if (available > 0) {
                    int bytesToRead = Math.min(available, maxLength - totalBytesRead);
                    int bytesRead = inputStream.read(buffer, totalBytesRead, bytesToRead);
                    if (bytesRead > 0) {
                        totalBytesRead += bytesRead;
                        lastReceiveTime = System.currentTimeMillis();
                        
                        // 检查是否接收到完整报文(以指定结束符结尾)
                        if (endMarker != 0 && totalBytesRead > 0 && buffer[totalBytesRead - 1] == endMarker) {
                            byte[] result = new byte[totalBytesRead];
                            System.arraycopy(buffer, 0, result, 0, totalBytesRead);
                            log.debug("接收数据: {} ({} 字节)", 
                                    TD3600Protocol.bytesToHex(result), totalBytesRead);
                            return result;
                        }
                    }
                }
                
                // 如果已经接收到数据,且超过100ms没有新数据,认为接收完成
                if (totalBytesRead > 0 && (System.currentTimeMillis() - lastReceiveTime) > 100) {
                    byte[] result = new byte[totalBytesRead];
                    System.arraycopy(buffer, 0, result, 0, totalBytesRead);
                    log.debug("接收数据完成: {} ({} 字节)", 
                            TD3600Protocol.bytesToHex(result), totalBytesRead);
                    return result;
                }
                
                // 短暂休眠避免CPU占用过高
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new IOException("接收数据被中断", e);
                }
            }
            
            // 超时
            if (totalBytesRead > 0) {
                byte[] result = new byte[totalBytesRead];
                System.arraycopy(buffer, 0, result, 0, totalBytesRead);
                log.warn("接收数据超时,已接收 {} 字节: {}", 
                        totalBytesRead, TD3600Protocol.bytesToHex(result));
                return result;
            } else {
                throw new IOException("接收数据超时,未接收到任何数据");
            }
        } catch (Exception e) {
            log.error("接收数据失败", e);
            throw new IOException("接收数据失败: " + e.getMessage(), e);
        }
    }
    
    /**
     * 发送TD3600请求并接收响应
     * 
     * @param command 命令字符串
     * @param timeoutMs 超时时间(毫秒)
     * @return 响应的ASCII字符串
     * @throws IOException 如果通信失败
     */
    public String sendTD3600Request(String command, int timeoutMs) throws IOException {
        // 构建请求报文
        byte[] request = TD3600Protocol.buildOutRequest(command);
        
        log.info("发送TD3600请求: {}", command);
        log.debug("请求报文: {}", TD3600Protocol.bytesToHex(request));
        
        // 发送请求
        sendData(request);
        
        // 接收响应
        byte[] response = receiveData(1024, timeoutMs);
        
        // 解析响应
        try {
            String result = TD3600Protocol.parseResponse(response);
            log.info("接收TD3600响应: {}", result);
            return result;
        } catch (Exception e) {
            log.error("解析响应失败: {}", TD3600Protocol.bytesToHex(response), e);
            throw new IOException("解析响应失败: " + e.getMessage(), e);
        }
    }
    
    /**
     * 发送TD3600请求并接收响应(使用默认超时)
     * 
     * @param command 命令字符串
     * @return 响应的ASCII字符串
     * @throws IOException 如果通信失败
     */
    public String sendTD3600Request(String command) throws IOException {
        return sendTD3600Request(command, DEFAULT_TIMEOUT);
    }
    
    /**
     * 清空串口缓冲区
     */
    public void clearBuffer() {
        if (isConnected()) {
            try {
                InputStream inputStream = serialPort.getInputStream();
                while (inputStream.available() > 0) {
                    inputStream.read();
                }
                log.debug("串口缓冲区已清空");
            } catch (Exception e) {
                log.warn("清空缓冲区失败", e);
            }
        }
    }
}

测试查询电压电流DLT698Example.java


import com.fazecast.jSerialComm.SerialPort;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * DL/T 698协议使用示例
 *
 * 演示如何在Spring Boot应用中使用DL/T 698协议读取电能表数据
 *
 * @author ywhc
 * @date 2025-10-15
 */
@Component
public class DLT698Example {

    @Autowired
    private SerialPortService serialPortService;

    /**
     * 示例1:读取单个电能表的电压数据
     */
    public static void example1_ReadVoltage() {
        SerialPortService service =new SerialPortService();;
        try {
            // 1. 打开串口(偶校验)
            boolean opened = service.openPort(
                "COM16",                    // 串口号
                9600,                       // 波特率
                8,                          // 数据位
                SerialPort.ONE_STOP_BIT,    // 停止位
                SerialPort.EVEN_PARITY      // 偶校验
            );

            if (!opened) {
                System.err.println("串口打开失败");
                return;
            }

            // 2. 构建请求报文
            byte[] address = DLT698Protocol.parseAddress("000000000000");
            byte[] request = DLT698Protocol.buildVoltageRequest(address, (byte) 0x01);

            // 3. 清空缓冲区
            service.clearBuffer();

            // 4. 发送请求
            // System.out.println("发送请求: " + DLT698Protocol.bytesToHex(request));  // 调试用
            service.sendData(request);

            // 5. 接收响应(DL/T 698使用0x16作为结束符)
            byte[] response = service.receiveData(1024, 5000, (byte) 0x16);

            // 调试:打印接收到的原始数据(可选)
            // System.out.println("接收响应: " + DLT698Protocol.bytesToHex(response));
            // System.out.println("响应长度: " + response.length + " 字节");

            // 6. 解析响应
            DLT698Protocol.MeterData voltageData = DLT698Protocol.parseGetResponse(response);

            // 7. 处理数据
            System.out.println("电压数据:");
            for (DLT698Protocol.PhaseData phase : voltageData.getPhases()) {
                System.out.println(String.format("  %s: %.1f%s",
                    phase.getPhaseName(),
                    phase.getValue(),
                    phase.getUnit()));
            }

        } catch (IOException e) {
            System.err.println("通信错误:" + e.getMessage());
        } catch (Exception e) {
            System.err.println("处理错误:" + e.getMessage());
        } finally {
            // 8. 关闭串口
            service.closePort();
        }
    }

    /**
     * 示例2:读取电压和电流数据
     */
    public void example2_ReadVoltageAndCurrent() {
        try {
            // 打开串口
            serialPortService.openPort("COM16", 9600, 8,
                SerialPort.ONE_STOP_BIT, SerialPort.EVEN_PARITY);

            String meterAddress = "000000000000";
            byte[] address = DLT698Protocol.parseAddress(meterAddress);

            // 读取电压
            DLT698Protocol.MeterData voltageData = readMeterData(address,
                DLT698Protocol.OAD_VOLTAGE, (byte) 0x01);
            System.out.println("电压数据:" + voltageData);

            // 等待一下再读取下一个数据
            Thread.sleep(200);

            // 读取电流
            DLT698Protocol.MeterData currentData = readMeterData(address,
                DLT698Protocol.OAD_CURRENT, (byte) 0x01);
            System.out.println("电流数据:" + currentData);

        } catch (Exception e) {
            System.err.println("错误:" + e.getMessage());
        } finally {
            serialPortService.closePort();
        }
    }

    /**
     * 示例3:批量读取多个电能表数据
     */
    public void example3_ReadMultipleMeters() {
        String[] meterAddresses = {
            "000000000001",
            "000000000002",
            "000000000003"
        };

        try {
            // 打开串口
            serialPortService.openPort("COM16", 9600, 8,
                SerialPort.ONE_STOP_BIT, SerialPort.EVEN_PARITY);

            for (String addressStr : meterAddresses) {
                try {
                    System.out.println("\n读取电能表: " + addressStr);

                    byte[] address = DLT698Protocol.parseAddress(addressStr);

                    // 读取电压
                    DLT698Protocol.MeterData voltageData = readMeterData(address,
                        DLT698Protocol.OAD_VOLTAGE, (byte) 0x01);
                    System.out.println("  电压:" + voltageData);

                    Thread.sleep(200);

                    // 读取电流
                    DLT698Protocol.MeterData currentData = readMeterData(address,
                        DLT698Protocol.OAD_CURRENT, (byte) 0x01);
                    System.out.println("  电流:" + currentData);

                    Thread.sleep(500);  // 电能表之间的间隔

                } catch (Exception e) {
                    System.err.println("  读取失败:" + e.getMessage());
                }
            }

        } catch (Exception e) {
            System.err.println("错误:" + e.getMessage());
        } finally {
            serialPortService.closePort();
        }
    }

    /**
     * 示例4:使用扩展服务类(推荐方式)
     */
    public void example4_UseExtendedService() {
        DLT698SerialService dltService = new DLT698SerialService();

        try {
            // 打开串口
            dltService.openPort("COM16", 9600, 8,
                SerialPort.ONE_STOP_BIT, SerialPort.EVEN_PARITY);

            String meterAddress = "000000000000";

            // 直接读取电压
            DLT698Protocol.MeterData voltageData = dltService.readVoltage(meterAddress);
            System.out.println("电压数据:" + voltageData);

            // 直接读取电流
            DLT698Protocol.MeterData currentData = dltService.readCurrent(meterAddress);
            System.out.println("电流数据:" + currentData);

        } catch (Exception e) {
            System.err.println("错误:" + e.getMessage());
        } finally {
            dltService.closePort();
        }
    }

    /**
     * 辅助方法:读取电能表数据
     */
    private DLT698Protocol.MeterData readMeterData(byte[] address, int oad, byte piid)
            throws IOException {
        // 清空缓冲区
        serialPortService.clearBuffer();

        // 构建请求
        byte[] request = DLT698Protocol.buildGetRequest(address, oad, piid);

        // 发送请求
        serialPortService.sendData(request);

        // 接收响应
        byte[] response = serialPortService.receiveData(1024, 5000, (byte) 0x16);

        // 解析响应
        return DLT698Protocol.parseGetResponse(response);
    }

    /**
     * DL/T 698扩展串口服务类
     */
    public static class DLT698SerialService extends SerialPortService {

        /**
         * 读取电压数据
         */
        public DLT698Protocol.MeterData readVoltage(String address) throws IOException {
            return readMeterData(address, DLT698Protocol.OAD_VOLTAGE);
        }

        /**
         * 读取电流数据
         */
        public DLT698Protocol.MeterData readCurrent(String address) throws IOException {
            return readMeterData(address, DLT698Protocol.OAD_CURRENT);
        }

        /**
         * 读取电能表数据
         */
        private DLT698Protocol.MeterData readMeterData(String address, int oad)
                throws IOException {
            if (!isConnected()) {
                throw new IOException("串口未连接");
            }

            // 解析地址
            byte[] addressBytes = DLT698Protocol.parseAddress(address);

            // 构建请求(统一使用PIID=0x01,服务序号为0)
            byte[] request = DLT698Protocol.buildGetRequest(addressBytes, oad, (byte) 0x01);

            // 清空缓冲区
            clearBuffer();

            // 发送请求
            sendData(request);

            // 接收响应(使用0x16作为结束符)
            byte[] response = receiveData(1024, 5000, (byte) 0x16);

            // 解析响应
            return DLT698Protocol.parseGetResponse(response);
        }
    }

    public static void main(String[] args) {
        example1_ReadVoltage();
    }
}

DLT698Test.java测试


import com.fazecast.jSerialComm.SerialPort;

import java.io.IOException;

/**
 * DL/T 698.45协议测试类
 *
 * 测试环境:
 * - 串口:COM16
 * - 波特率:9600
 * - 数据位:8
 * - 停止位:1
 * - 校验位:Even(偶校验)
 * - 地址:000000000000
 *
 * @author ywhc
 * @date 2025-10-15
 */
public class DLT698Test {

    // 串口配置
    private static final String PORT_NAME = "COM16";
    private static final int BAUD_RATE = 9600;
    private static final int DATA_BITS = 8;
    private static final int STOP_BITS = SerialPort.ONE_STOP_BIT;
    private static final int PARITY = SerialPort.EVEN_PARITY;  // 偶校验
    private static final int TIMEOUT_MS = 5000;

    // 电能表地址
    private static final String METER_ADDRESS = "000000000000";

    /**
     * 测试1:验证协议报文生成
     */
    public static void testProtocolGeneration() {
        System.out.println("========== 测试1:验证协议报文生成 ==========");

        // 解析地址
        byte[] address = DLT698Protocol.parseAddress(METER_ADDRESS);

        // 构建电压请求报文
        byte[] voltageRequest = DLT698Protocol.buildVoltageRequest(address, (byte) 0x05);

        System.out.println("生成的电压请求报文:");
        System.out.println(DLT698Protocol.bytesToHex(voltageRequest));

        // 期望的报文(从iESlab工具获取)
        String expectedHex = "68 17 00 43 45 00 00 00 00 00 00 00 83 1C 05 01 05 20 00 02 00 00 62 AA 16";
        byte[] expected = DLT698Protocol.hexToBytes(expectedHex);

        System.out.println("\n期望的报文:");
        System.out.println(DLT698Protocol.bytesToHex(expected));

        // 比较(跳过前导符)
        boolean match = compareBytes(voltageRequest, expected);
        System.out.println("\n报文匹配结果:" + (match ? "✓ 通过" : "✗ 失败"));
        System.out.println();
    }

    /**
     * 测试2:解析响应报文
     */
    public static void testResponseParsing() {
        System.out.println("========== 测试2:解析响应报文 ==========");

        // iESlab工具接收到的响应报文
        String responseHex = "FE FE FE FE 68 24 00 C3 45 00 00 00 00 00 00 00 28 B5 85 01 05 20 00 02 00 01 01 03 12 08 98 12 0D AC 12 0D AC 00 00 FF B7 16";
        byte[] response = DLT698Protocol.hexToBytes(responseHex);

        System.out.println("响应报文:");
        System.out.println(DLT698Protocol.bytesToHex(response));

        try {
            // 解析响应
            DLT698Protocol.MeterData data = DLT698Protocol.parseGetResponse(response);

            System.out.println("\n解析结果:");
            System.out.println(data);

            // 验证解析结果
            System.out.println("期望值:");
            System.out.println("  A相: 220.0V");
            System.out.println("  B相: 350.0V");
            System.out.println("  C相: 350.0V");

            // 检查解析是否正确
            boolean correct = data.getPhases().size() == 3 &&
                            Math.abs(data.getPhases().get(0).getValue() - 220.0) < 0.1 &&
                            Math.abs(data.getPhases().get(1).getValue() - 350.0) < 0.1 &&
                            Math.abs(data.getPhases().get(2).getValue() - 350.0) < 0.1;

            System.out.println("\n解析验证:" + (correct ? "✓ 通过" : "✗ 失败"));
        } catch (Exception e) {
            System.err.println("解析失败:" + e.getMessage());
            e.printStackTrace();
        }
        System.out.println();
    }

    /**
     * 测试3:通过串口读取电压数据
     */
    public static void testReadVoltage() {
        System.out.println("========== 测试3:通过串口读取电压数据 ==========");

        SerialPortService serialService = new SerialPortService();

        try {
            // 打开串口
            System.out.println("正在打开串口 " + PORT_NAME + "...");
            boolean opened = serialService.openPort(PORT_NAME, BAUD_RATE, DATA_BITS, STOP_BITS, PARITY);

            if (!opened) {
                System.err.println("串口打开失败!");
                return;
            }

            System.out.println("串口已打开");

            // 清空缓冲区
            serialService.clearBuffer();
            Thread.sleep(100);

            // 构建请求报文
            byte[] address = DLT698Protocol.parseAddress(METER_ADDRESS);
            byte[] request = DLT698Protocol.buildVoltageRequest(address, (byte) 0x05);

            System.out.println("\n发送请求报文:");
            System.out.println(DLT698Protocol.bytesToHex(request));

            // 发送请求
            serialService.sendData(request);
            System.out.println("请求已发送,等待响应...");

            // 接收响应
            byte[] response = serialService.receiveData(1024, TIMEOUT_MS);

            System.out.println("\n接收到响应报文:");
            System.out.println(DLT698Protocol.bytesToHex(response));

            // 解析响应
            DLT698Protocol.MeterData data = DLT698Protocol.parseGetResponse(response);

            System.out.println("\n电压数据:");
            System.out.println(data);

        } catch (IOException e) {
            System.err.println("串口通信错误:" + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("测试失败:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭串口
            serialService.closePort();
            System.out.println("串口已关闭");
        }
        System.out.println();
    }

    /**
     * 测试4:通过串口读取电流数据
     */
    public static void testReadCurrent() {
        System.out.println("========== 测试4:通过串口读取电流数据 ==========");

        SerialPortService serialService = new SerialPortService();

        try {
            // 打开串口
            System.out.println("正在打开串口 " + PORT_NAME + "...");
            boolean opened = serialService.openPort(PORT_NAME, BAUD_RATE, DATA_BITS, STOP_BITS, PARITY);

            if (!opened) {
                System.err.println("串口打开失败!");
                return;
            }

            System.out.println("串口已打开");

            // 清空缓冲区
            serialService.clearBuffer();
            Thread.sleep(100);

            // 构建请求报文
            byte[] address = DLT698Protocol.parseAddress(METER_ADDRESS);
            byte[] request = DLT698Protocol.buildCurrentRequest(address, (byte) 0x05);

            System.out.println("\n发送请求报文:");
            System.out.println(DLT698Protocol.bytesToHex(request));

            // 发送请求
            serialService.sendData(request);
            System.out.println("请求已发送,等待响应...");

            // 接收响应
            byte[] response = serialService.receiveData(1024, TIMEOUT_MS);

            System.out.println("\n接收到响应报文:");
            System.out.println(DLT698Protocol.bytesToHex(response));

            // 解析响应
            DLT698Protocol.MeterData data = DLT698Protocol.parseGetResponse(response);

            System.out.println("\n电流数据:");
            System.out.println(data);

        } catch (IOException e) {
            System.err.println("串口通信错误:" + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            System.err.println("测试失败:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭串口
            serialService.closePort();
            System.out.println("串口已关闭");
        }
        System.out.println();
    }

    /**
     * 测试5:使用SerialPortService扩展方法读取电压数据
     */
    public static void testReadVoltageWithService() {
        System.out.println("========== 测试5:使用SerialPortService扩展方法 ==========");

        DLT698SerialService dltService = new DLT698SerialService();

        try {
            // 打开串口
            System.out.println("正在打开串口 " + PORT_NAME + "...");
            boolean opened = dltService.openPort(PORT_NAME, BAUD_RATE, DATA_BITS, STOP_BITS, PARITY);

            if (!opened) {
                System.err.println("串口打开失败!");
                return;
            }

            System.out.println("串口已打开");

            // 读取电压数据
            DLT698Protocol.MeterData voltageData = dltService.readVoltage(METER_ADDRESS);
            System.out.println("\n电压数据:");
            System.out.println(voltageData);

            // 等待一下再读取电流
            Thread.sleep(500);

            // 读取电流数据
            DLT698Protocol.MeterData currentData = dltService.readCurrent(METER_ADDRESS);
            System.out.println("电流数据:");
            System.out.println(currentData);

        } catch (Exception e) {
            System.err.println("测试失败:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭串口
            dltService.closePort();
            System.out.println("串口已关闭");
        }
        System.out.println();
    }

    /**
     * 测试6:验证多OAD请求报文生成
     */
    public static void testMultiOADRequestGeneration() {
        System.out.println("========== 测试6:验证多OAD请求报文生成 ==========");

        // 解析地址
        byte[] address = DLT698Protocol.parseAddress(METER_ADDRESS);

        // 构建同时读取电压和电流的请求报文
        byte[] request = DLT698Protocol.buildVoltageAndCurrentRequest(address, (byte) 0x04);

        System.out.println("生成的请求报文:");
        System.out.println(DLT698Protocol.bytesToHex(request));

        // 期望的报文(从iESlab工具获取)
        String expectedHex = "68 1C 00 43 45 00 00 00 00 00 00 00 9A 5E 05 02 04 02 20 00 02 00 20 01 02 00 00 24 22 16";
        byte[] expected = DLT698Protocol.hexToBytes(expectedHex);

        System.out.println("\n期望的报文(iESlab工具):");
        System.out.println(DLT698Protocol.bytesToHex(expected));

        // 比较
        boolean match = compareBytes(request, expected);
        System.out.println("\n报文匹配结果:" + (match ? "✓ 通过" : "✗ 失败"));

        if (match) {
            System.out.println("✓ 生成的报文与iESlab工具一致,可以一次请求获取电压和电流数据!");
        }
        System.out.println();
    }

    /**
     * 测试7:一次请求同时获取电压和电流数据
     */
    public static void testReadVoltageAndCurrent() {
        System.out.println("========== 测试7:一次请求同时获取电压和电流数据 ==========");

        DLT698SerialService dltService = new DLT698SerialService();

        try {
            // 打开串口
            System.out.println("正在打开串口 " + PORT_NAME + "...");
            boolean opened = dltService.openPort(PORT_NAME, BAUD_RATE, DATA_BITS, STOP_BITS, PARITY);

            if (!opened) {
                System.err.println("串口打开失败!");
                return;
            }

            System.out.println("串口已打开");

            // 构建请求报文并显示
            byte[] address = DLT698Protocol.parseAddress(METER_ADDRESS);
            byte[] request = DLT698Protocol.buildVoltageAndCurrentRequest(address, (byte) 0x04);

            System.out.println("\n生成的请求报文:");
            System.out.println(DLT698Protocol.bytesToHex(request));

            System.out.println("\niESlab工具期望的请求报文:");
            System.out.println("68 1C 00 43 45 00 00 00 00 00 00 00 9A 5E 05 02 04 02 20 00 02 00 20 01 02 00 00 24 22 16");

            // 一次请求同时读取电压和电流数据
            java.util.List<DLT698Protocol.MeterData> dataList = dltService.readVoltageAndCurrent(METER_ADDRESS);

            System.out.println("\n一次请求获取到的数据:");
            System.out.println("=========================================");
            for (int i = 0; i < dataList.size(); i++) {
                System.out.println("结果 " + (i + 1) + ":");
                System.out.println(dataList.get(i));
            }
            System.out.println("=========================================");

            System.out.println("✓ 测试成功!一次请求同时获取到了电压和电流数据");

        } catch (Exception e) {
            System.err.println("测试失败:" + e.getMessage());
            e.printStackTrace();
        } finally {
            // 关闭串口
            dltService.closePort();
            System.out.println("串口已关闭");
        }
        System.out.println();
    }

    /**
     * 比较两个字节数组(跳过前导符)
     */
    private static boolean compareBytes(byte[] a, byte[] b) {
        // 跳过前导符FE
        int aStart = 0;
        while (aStart < a.length && a[aStart] == (byte) 0xFE) {
            aStart++;
        }

        int bStart = 0;
        while (bStart < b.length && b[bStart] == (byte) 0xFE) {
            bStart++;
        }

        // 比较有效长度
        int aLen = a.length - aStart;
        int bLen = b.length - bStart;

        if (aLen != bLen) {
            System.out.println("长度不匹配:" + aLen + " vs " + bLen);
            return false;
        }

        // 逐字节比较
        for (int i = 0; i < aLen; i++) {
            if (a[aStart + i] != b[bStart + i]) {
                System.out.println(String.format("字节不匹配 [%d]: 0x%02X vs 0x%02X",
                    i, a[aStart + i] & 0xFF, b[bStart + i] & 0xFF));
                return false;
            }
        }

        return true;
    }

    /**
     * 主函数:运行所有测试
     */
    public static void main(String[] args) {
        System.out.println("DL/T 698.45 协议测试");
        System.out.println("=============================================\n");

        // 根据需要取消注释以下测试方法
        // testProtocolGeneration();           // 测试1:验证协议报文生成
        // testResponseParsing();              // 测试2:解析响应报文
        // testReadVoltage();                  // 测试3:通过串口读取电压数据
        // testReadCurrent();                  // 测试4:通过串口读取电流数据
        // testReadVoltageWithService();       // 测试5:使用SerialPortService扩展方法
        // testMultiOADRequestGeneration();    // 测试6:验证多OAD请求报文生成
        testReadVoltageAndCurrent();        // 测试7:一次请求同时获取电压和电流数据

        System.out.println("\n提示:请取消注释相应的测试方法以运行测试");
    }

    /**
     * DL/T 698串口服务扩展类
     * 提供便捷的电能表数据读取方法
     */
    public static class DLT698SerialService extends SerialPortService {

        private static final int DEFAULT_PIID = 0x05;

        /**
         * 读取电压数据
         *
         * @param address 电能表地址(12位十六进制字符串)
         * @return 电压数据
         * @throws IOException 如果通信失败
         */
        public DLT698Protocol.MeterData readVoltage(String address) throws IOException {
            return readMeterData(address, DLT698Protocol.OAD_VOLTAGE);
        }

        /**
         * 读取电流数据
         *
         * @param address 电能表地址(12位十六进制字符串)
         * @return 电流数据
         * @throws IOException 如果通信失败
         */
        public DLT698Protocol.MeterData readCurrent(String address) throws IOException {
            return readMeterData(address, DLT698Protocol.OAD_CURRENT);
        }

        /**
         * 一次请求读取多个对象属性数据
         *
         * @param address 电能表地址(12位十六进制字符串)
         * @param oads 对象属性描述符数组
         * @return 电能表数据列表
         * @throws IOException 如果通信失败
         */
        public java.util.List<DLT698Protocol.MeterData> readMultiple(String address, int[] oads) throws IOException {
            if (!isConnected()) {
                throw new IOException("串口未连接");
            }

            // 解析地址
            byte[] addressBytes = DLT698Protocol.parseAddress(address);

            // 构建请求报文
            byte[] request = DLT698Protocol.buildGetRequestList(addressBytes, oads, (byte) DEFAULT_PIID);

            // 清空缓冲区
            clearBuffer();

            // 发送请求
            sendData(request);

            // 接收响应
            byte[] response = receiveData(1024, 5000);

            // 解析响应
            return DLT698Protocol.parseGetResponseList(response);
        }

        /**
         * 一次请求同时读取电压和电流数据
         *
         * @param address 电能表地址(12位十六进制字符串)
         * @return 电能表数据列表(第一个是电压,第二个是电流)
         * @throws IOException 如果通信失败
         */
        public java.util.List<DLT698Protocol.MeterData> readVoltageAndCurrent(String address) throws IOException {
            return readMultiple(address, new int[]{DLT698Protocol.OAD_VOLTAGE, DLT698Protocol.OAD_CURRENT});
        }

        /**
         * 读取电能表数据
         *
         * @param address 电能表地址
         * @param oad 对象属性描述符
         * @return 电能表数据
         * @throws IOException 如果通信失败
         */
        private DLT698Protocol.MeterData readMeterData(String address, int oad) throws IOException {
            if (!isConnected()) {
                throw new IOException("串口未连接");
            }

            // 解析地址
            byte[] addressBytes = DLT698Protocol.parseAddress(address);

            // 构建请求报文
            byte[] request = DLT698Protocol.buildGetRequest(addressBytes, oad, (byte) DEFAULT_PIID);

            // 清空缓冲区
            clearBuffer();

            // 发送请求
            sendData(request);

            // 接收响应
            byte[] response = receiveData(1024, 5000);

            // 解析响应
            return DLT698Protocol.parseGetResponse(response);
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions