package com.inspect.nvr.utils; import lombok.Data; import lombok.extern.slf4j.Slf4j; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; /** * 规范参考 DL/T 664——2016 《带电设备红外诊断应用规范》 */ @Slf4j public class DLT664Service { // 各字段字节长度:数值字段直接取对应 Java 类型的字节数,定长字节数组用命名常量 private static final int SIZE_FILE_VERSION = Short.BYTES; private static final int SIZE_WIDTH = Short.BYTES; private static final int SIZE_HEIGHT = Short.BYTES; private static final int SIZE_DATE_TIME = 14; private static final int SIZE_EMISS = Float.BYTES; private static final int SIZE_AMBIENT_TEMPERATURE = Float.BYTES; private static final int SIZE_LEN = Byte.BYTES; private static final int SIZE_DISTANCE = Integer.BYTES; private static final int SIZE_RELATIVE_HUMIDITY = Byte.BYTES; private static final int SIZE_REFLECTED_TEMPERATURE = Float.BYTES; private static final int SIZE_STRING_FIELD = 32; private static final int SIZE_LONGITUDE = Double.BYTES; private static final int SIZE_LATITUDE = Double.BYTES; private static final int SIZE_ALTITUDE = Integer.BYTES; private static final int SIZE_DESCRIPTION_LENGTH = Integer.BYTES; private static final int SIZE_OFFSET = Integer.BYTES; private static final int SIZE_FILE_END_TYPE = 16; // 定长 ASCII 字符串字段的补齐字节 private static final byte STRING_PAD = (byte) ' '; public static byte[] generate(byte[] imageBytes, float[][] temperatureMatrix, String productor) { Info info = new Info(); info.setWidth((short) temperatureMatrix[0].length); info.setHeight((short) temperatureMatrix.length); info.setIrData(temperatureMatrix); String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); info.setDateTime(toAsciiBytes(dateTime)); info.setProductor(toAsciiBytes(productor)); return generate(imageBytes, info); } public static byte[] generate(byte[] imageBytes, Info info) { float[][] irData = info.getIrData(); int width = info.getWidth(); int height = info.getHeight(); int descriptionLength = info.getDescriptionLength(); // 红外数据块大小,表达式顺序与下方写入顺序一一对应 int dataSize = SIZE_FILE_VERSION + SIZE_WIDTH + SIZE_HEIGHT + SIZE_DATE_TIME + width * height * Float.BYTES + SIZE_EMISS + SIZE_AMBIENT_TEMPERATURE + SIZE_LEN + SIZE_DISTANCE + SIZE_RELATIVE_HUMIDITY + SIZE_REFLECTED_TEMPERATURE + SIZE_STRING_FIELD * 3 + SIZE_LONGITUDE + SIZE_LATITUDE + SIZE_ALTITUDE + SIZE_DESCRIPTION_LENGTH + descriptionLength; ByteBuffer dataBuf = ByteBuffer.allocate(dataSize).order(ByteOrder.LITTLE_ENDIAN); // 文件版本、矩阵宽高 dataBuf.putShort(info.getFileVersion()); dataBuf.putShort((short) width); dataBuf.putShort((short) height); // 拍摄时间 putFixedBytes(dataBuf, info.getDateTime(), SIZE_DATE_TIME, STRING_PAD); // 温度矩阵 for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { dataBuf.putFloat(irData[row][col]); } } // 环境参数 dataBuf.putFloat(info.getEmiss()); dataBuf.putFloat(info.getAmbientTemperature()); dataBuf.put(info.getLen()); dataBuf.putInt(info.getDistance()); dataBuf.put(info.getRelativeHumidity()); dataBuf.putFloat(info.getReflectedTemperature()); // 定长字符串 putFixedBytes(dataBuf, info.getProductor(), SIZE_STRING_FIELD, STRING_PAD); putFixedBytes(dataBuf, info.getType(), SIZE_STRING_FIELD, STRING_PAD); putFixedBytes(dataBuf, info.getSerialNo(), SIZE_STRING_FIELD, STRING_PAD); // 经纬度、海拔、备注 dataBuf.putDouble(info.getLongitude()); dataBuf.putDouble(info.getLatitude()); dataBuf.putInt(info.getAltitude()); dataBuf.putInt(descriptionLength); if (descriptionLength > 0) { putFixedBytes(dataBuf, info.getDescriptionData(), descriptionLength, (byte) 0); } if (dataBuf.hasRemaining()) { throw new IllegalStateException("dataSize 与实际写入字节数不一致,剩余 " + dataBuf.remaining() + " 字节"); } byte[] dataBlock = dataBuf.array(); // 拼接: 图片 + 红外数据块 + 偏移量(4) + 文件末尾标识(16) int offset = imageBytes.length; byte[] result = new byte[offset + dataBlock.length + SIZE_OFFSET + SIZE_FILE_END_TYPE]; System.arraycopy(imageBytes, 0, result, 0, offset); System.arraycopy(dataBlock, 0, result, offset, dataBlock.length); ByteBuffer.wrap(result, offset + dataBlock.length, SIZE_OFFSET) .order(ByteOrder.LITTLE_ENDIAN) .putInt(offset); System.arraycopy(info.getFileEndType(), 0, result, offset + dataBlock.length + SIZE_OFFSET, SIZE_FILE_END_TYPE); log.info("DLT664文件组装成功"); return result; } /** * 写入定长字节:不足补 fill,超出截断。 */ private static void putFixedBytes(ByteBuffer buf, byte[] src, int len, byte fill) { byte[] bytes = new byte[len]; Arrays.fill(bytes, fill); if (src != null) { System.arraycopy(src, 0, bytes, 0, Math.min(src.length, len)); } buf.put(bytes); } private static byte[] toAsciiBytes(String s) { return s != null ? s.getBytes(StandardCharsets.US_ASCII) : null; } @Data public static class Info { // 文件版本 private short fileVersion = (short) 0x0100; // 矩阵宽度 private short width; // 矩阵高度 private short height; // 拍摄时间 (14 bytes ASCII) private byte[] dateTime; // 整个温度矩阵 private float[][] irData; // 辐射率 private float emiss; // 环境温度 private float ambientTemperature; // 镜头度数 private byte len; // 拍摄距离 (4 bytes) private int distance; // 相对湿度 private byte relativeHumidity; // 反射温度 private float reflectedTemperature; // 生产厂家 (32 bytes) private byte[] productor; // 产品型号 (32 bytes) private byte[] type; // 产品序列号 (32 bytes) private byte[] serialNo; // 经度 private double longitude = 0; // 纬度 private double latitude = 0; // 海拔 (4 bytes) private int altitude = 0; // 备注信息长度, 0 表示没有存储信息 private int descriptionLength = 0; // 备注信息 (descriptionLength bytes) private byte[] descriptionData; // 红外数据的起始偏移地址 private int irDataOffset; // 文件末尾标识 (16 bytes) private byte[] fileEndType = { (byte) 0x37, (byte) 0x66, (byte) 0x07, (byte) 0x1a, (byte) 0x12, (byte) 0x3a, (byte) 0x4c, (byte) 0x9f, (byte) 0xa9, (byte) 0x5d, (byte) 0x21, (byte) 0xd2, (byte) 0xda, (byte) 0x7d, (byte) 0x26, (byte) 0xbc }; } }