diff --git a/src/main/java/com/zhgd/netty/tcp/handler/TcpNettyHandler.java b/src/main/java/com/zhgd/netty/tcp/handler/TcpNettyHandler.java index 3883ee2ef..87f07bb75 100644 --- a/src/main/java/com/zhgd/netty/tcp/handler/TcpNettyHandler.java +++ b/src/main/java/com/zhgd/netty/tcp/handler/TcpNettyHandler.java @@ -34,6 +34,14 @@ public class TcpNettyHandler extends SimpleChannelInboundHandler { public static final String PREFIX = "##"; public static final String PREFIX1 = "7E"; + + public static final String TOWER_PREFIX = "A55A"; + + public static final String TOWER_END = "CC33C33C"; + + @Autowired + private TowerCraneHandler towerCraneHandler; + @Autowired private HighFormworkSupportService highFormworkSupportService; @@ -50,7 +58,6 @@ public class TcpNettyHandler extends SimpleChannelInboundHandler { String str = getString(bytes); str = str.trim(); log.info("tcp接收数据 >>> {} ", str); -// ctx.writeAndFlush(Unpooled.copiedBuffer("response message", CharsetUtil.UTF_8)); if (StringUtils.startsWith(str, HighFormworkSupport.TCP_DATA_PREFIX) && StringUtils.endsWith(str, HighFormworkSupport.TCP_DATA_END)) { log.info("高支模接收数据 >>> {} ", str); //接收高支模数据保存到mysql中 @@ -113,7 +120,9 @@ public class TcpNettyHandler extends SimpleChannelInboundHandler { //16进制判断 String hexString = readToHexString(bytes); log.info("tcp接收数据(16进制转字符串) >>> {} ", hexString); - if (StringUtils.startsWith(hexString, PREFIX1)) { + if (StringUtils.startsWith(hexString, TOWER_PREFIX) && StringUtils.endsWith(hexString, TOWER_END)) { + towerCraneHandler.parseData(hexString, ctx); + } else if (StringUtils.startsWith(hexString, PREFIX1)) { //有害气体 poisonousGasDevCurrentDataService.addDataFromTcp(hexString.trim()); } diff --git a/src/main/java/com/zhgd/netty/tcp/handler/TowerCraneHandler.java b/src/main/java/com/zhgd/netty/tcp/handler/TowerCraneHandler.java new file mode 100644 index 000000000..65d1e9f07 --- /dev/null +++ b/src/main/java/com/zhgd/netty/tcp/handler/TowerCraneHandler.java @@ -0,0 +1,927 @@ +package com.zhgd.netty.tcp.handler; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.zhgd.hj212.hbt212.core.T212Mapper; +import com.zhgd.hj212.hbt212.model.CpData; +import com.zhgd.hj212.hbt212.model.Data; +import com.zhgd.hj212.hbt212.model.Pollution; +import com.zhgd.netty.tcp.constant.HighFormworkSupport; +import com.zhgd.netty.tcp.service.HighFormworkSupportService; +import com.zhgd.xmgl.modules.bigdevice.entity.Tower; +import com.zhgd.xmgl.modules.bigdevice.entity.TowerAlarm; +import com.zhgd.xmgl.modules.bigdevice.entity.TowerCurrentData; +import com.zhgd.xmgl.modules.bigdevice.entity.TowerWorkCycle; +import com.zhgd.xmgl.modules.bigdevice.service.ITowerAlarmService; +import com.zhgd.xmgl.modules.bigdevice.service.ITowerCurrentDataService; +import com.zhgd.xmgl.modules.bigdevice.service.ITowerService; +import com.zhgd.xmgl.modules.bigdevice.service.ITowerWorkCycleService; +import com.zhgd.xmgl.modules.bigdevice.service.impl.TowerServiceImpl; +import com.zhgd.xmgl.modules.environment.entity.DustNoiseData; +import com.zhgd.xmgl.modules.environment.service.IDustNoiseDataService; +import com.zhgd.xmgl.modules.poisonous.service.IPoisonousGasDevCurrentDataService; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.util.CharsetUtil; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import java.io.UnsupportedEncodingException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; + +/** + * 塔机TCP上传数据解析工具 + * 遵循规则:大端序、物理量系数换算、时间年份+2000 + */ +@Slf4j +@Component +public class TowerCraneHandler { + + @Autowired + private ITowerService towerService; + + @Autowired + private ITowerCurrentDataService towerCurrentDataService; + + @Autowired + private ITowerAlarmService towerAlarmService; + + @Autowired + private ITowerWorkCycleService towerWorkCycleService; + + // 各物理量换算系数 + private static final double WEIGHT_COEFFICIENT = 0.001; // 吊重:1单位=0.001吨 + private static final double MOMENT_COEFFICIENT = 0.01; // 力矩:1单位=0.01T.M + private static final double HEIGHT_AMPLITUDE_COEFFICIENT = 0.1; // 高度/幅度:1单位=0.1米 + private static final double ANGLE_COEFFICIENT = 0.1; // 角度:1单位=0.1度 + + // 基础换算系数 + private static final double HEIGHT_AMPLITUDE_WIND_COEFF = 0.1; // 高度/幅度/风速:0.1 + private static final double WEIGHT_COEFF = 0.001; // 起重量:0.001吨 + private static final double ROTATE_COEFF = 0.1; // 回转:0.1度 + private static final int ANGLE_OFFSET = 1500; // 倾角偏移量 + private static final double ANGLE_COEFF = 100.0; // 倾角系数 + + // 固定帧头、帧尾 + private static final byte[] FRAME_HEAD = {(byte) 0xA5, (byte) 0x5A}; + + private static final byte[] FRAME_TYPE = hexStringToByteArray("61"); + private static final byte[] FRAME_TAIL = {(byte) 0xCC, (byte) 0x33, (byte) 0xC3, (byte) 0x3C}; + + /** + * 解析塔机十六进制数据字符串 + * @param hexStr + * @return 解析后的字段键值对 + */ + public void parseData(String hexStr, ChannelHandlerContext ctx) { + // 1. 十六进制字符串转字节数组 + byte[] data = hexStringToByteArray(hexStr); + // 2. 初始化ByteBuffer,设置大端序(网络字节序) + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.BIG_ENDIAN); + + // -------------------------- 固定字段解析 -------------------------- + // 起始符(2字节) + byte[] startFlag = new byte[2]; + buffer.get(startFlag); + System.out.println(bytesToHex(startFlag)); + + // 命令字(1字节) + byte cmd = buffer.get(); + String format = String.format("%02X", cmd); + + // 塔机编号(1字节) + byte towerId = buffer.get(); + + // 设备编号(3字节:特殊处理,手动拼接大端序) + byte[] deviceIdBytes = new byte[3]; + buffer.get(deviceIdBytes); + int deviceId = ((deviceIdBytes[0] & 0xFF) << 16) + | ((deviceIdBytes[1] & 0xFF) << 8) + | (deviceIdBytes[2] & 0xFF); + Tower tower = towerService.getOne(Wrappers.lambdaQuery().eq(Tower::getDevSn, deviceId)); + if (tower == null) { + if (format.equals("60")) { + register(hexStr, ctx, 0); + } + log.info("TCP塔吊不存在:" + deviceId); + return; + } + System.out.println("收到命令" + format); + // 循环数据 + if (format.equals("3D")) { + parseWorkCycleData(hexStr, tower); + } else if (format.equals("0C")) { + parseRealTimeData(hexStr, tower); + } else if (format.equals("60")) { + register(hexStr, ctx, 1); + } else if (format.equals("20")) { + parseAlarmData(hexStr, tower); + } + } + + /** + * 解析命令字20的塔机报警上传数据 + * @param hexStr 无空格的十六进制字符串(如:A55A200290F72F01140A0F100232110000004100620801000D000005E400000000000000009621CC33C33C) + * @return 解析后的字段键值对 + */ + public Map parseAlarmData(String hexStr, Tower tower) { + Map result = new HashMap<>(); + try { + // 1. 十六进制字符串转字节数组(去除空格) + byte[] data = hexStringToByteArray(hexStr.replaceAll("\\s+", "")); + // 2. 初始化ByteBuffer,强制大端序(网络字节序) + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.BIG_ENDIAN); + + // -------------------------- 基础固定字段 -------------------------- + // 起始符(2字节) + byte[] startFlag = new byte[2]; + buffer.get(startFlag); + result.put("起始符", bytesToHex(startFlag)); + + // 命令字(1字节) + byte cmd = buffer.get(); + result.put("命令字", String.format("%02X", cmd)); + if (!"20".equals(result.get("命令字"))) { + result.put("解析提示", "非命令字20的报警数据帧"); + } + + // 塔机编号(1字节,转无符号十进制) + byte towerId = buffer.get(); + result.put("塔机编号", (int) towerId & 0xFF); + + // 设备编号(3字节,手动拼接大端序) + byte[] deviceIdBytes = new byte[3]; + buffer.get(deviceIdBytes); + int deviceId = ((deviceIdBytes[0] & 0xFF) << 16) + | ((deviceIdBytes[1] & 0xFF) << 8) + | (deviceIdBytes[2] & 0xFF); + result.put("设备编号", deviceId); + + // 版本号(1字节) + byte version = buffer.get(); + result.put("版本号", (int) version & 0xFF); + + // -------------------------- 时间字段 -------------------------- + // 时间(6字节:年、月、日、时、分、秒) + byte[] timeBytes = new byte[6]; + buffer.get(timeBytes); + String alarmTime = parseTime(timeBytes); + result.put("报警数据生成时间", alarmTime); + + // -------------------------- 基础属性字段 -------------------------- + // 厂家及设备类型(1字节) + byte factoryDeviceType = buffer.get(); + result.put("厂家及设备类型编码", (int) factoryDeviceType & 0xFF); + + // 警报原因(1字节,解析描述) + byte alarmReason = buffer.get(); + int alarmReasonCode = (int) alarmReason & 0xFF; + result.put("警报原因编码", alarmReasonCode); +// result.put("警报原因描述", parseAlarmReason(alarmReasonCode)); + + // -------------------------- 报警时刻的核心数据 -------------------------- + // 报警时刻高度数据(2字节) + short alarmHeightRaw = buffer.getShort(); + double alarmHeight = alarmHeightRaw * HEIGHT_AMPLITUDE_WIND_COEFF; + result.put("报警时刻高度(米)", alarmHeight); + + // 报警时刻幅度数据(2字节) + short alarmAmplitudeRaw = buffer.getShort(); + double alarmAmplitude = alarmAmplitudeRaw * HEIGHT_AMPLITUDE_WIND_COEFF; + result.put("报警时刻幅度(米)", alarmAmplitude); + + // 报警时刻回转角度(2字节) + short alarmRotateRaw = buffer.getShort(); + double alarmRotate = alarmRotateRaw * ROTATE_COEFF; + result.put("报警时刻回转角度(度)", alarmRotate); + + // 报警时刻起重量数据(2字节) + short alarmWeightRaw = buffer.getShort(); + double alarmWeight = alarmWeightRaw * WEIGHT_COEFF; + result.put("报警时刻起重量(吨)", alarmWeight); + + // 报警时刻风速数据(2字节) + short alarmWindRaw = buffer.getShort(); + double alarmWind = alarmWindRaw * HEIGHT_AMPLITUDE_WIND_COEFF; + result.put("报警时刻风速(米/秒)", alarmWind); + + // 报警时刻倾角数据(2字节,特殊换算) + short alarmAngleRaw = buffer.getShort(); + double alarmAngle = (alarmAngleRaw - ANGLE_OFFSET) / ANGLE_COEFF; + result.put("报警时刻倾角(度)", alarmAngle); + + // -------------------------- 报警时刻百分比字段 -------------------------- + // 报警时刻重量百分比(1字节) + byte alarmWeightPercent = buffer.get(); + result.put("报警时刻重量百分比(%)", (int) alarmWeightPercent & 0xFF); + + // 报警时刻力矩百分比(1字节) + byte alarmMomentPercent = buffer.get(); + result.put("报警时刻力矩百分比(%)", (int) alarmMomentPercent & 0xFF); + + // 报警时刻风速百分比(1字节) + byte alarmWindPercent = buffer.get(); + result.put("报警时刻风速百分比(%)", (int) alarmWindPercent & 0xFF); + + // 报警时刻倾斜百分比(1字节) + byte alarmTiltPercent = buffer.get(); + result.put("报警时刻倾斜百分比(%)", (int) alarmTiltPercent & 0xFF); + + // -------------------------- 报警时刻制动状态 -------------------------- + // 报警时刻制动状态(1字节,解析bit位) + byte alarmBrakeStatus = buffer.get(); + int brakeCode = (int) alarmBrakeStatus & 0xFF; + result.put("报警时刻制动状态编码", brakeCode); + result.put("报警时刻制动状态描述", parseBrakeStatus(brakeCode)); + + // -------------------------- 结束符 -------------------------- + // 结束符(6字节:前2和校验 + 后4固定CC33C33C) + byte[] endFlag = new byte[6]; + buffer.get(endFlag); + result.put("结束符", bytesToHex(endFlag)); + // 提取结束符中的固定帧尾(后4字节) + byte[] fixedTail = new byte[4]; + System.arraycopy(endFlag, 2, fixedTail, 0, 4); + result.put("结束符-固定帧尾", bytesToHex(fixedTail)); + + TowerAlarm towerAlarm = new TowerAlarm(); + towerAlarm.setDevSn(tower.getDevSn()); + towerAlarm.setDevName(tower.getDevName()); + towerAlarm.setProjectSn(tower.getProjectSn()); + towerAlarm.setAddTime(DateUtil.parseDateTime(alarmTime)); + if (alarmReasonCode == 111 || alarmReasonCode == 112 || alarmReasonCode == 113 || alarmReasonCode == 114) { + towerAlarm.setMultiAlarm(1); + towerAlarm.setAlarmType(1); + } + if (alarmReasonCode == 211 || alarmReasonCode == 212 || alarmReasonCode == 213 || alarmReasonCode == 214) { + towerAlarm.setMultiAlarm(2); + towerAlarm.setAlarmType(2); + } + if (alarmReasonCode == 13) { + towerAlarm.setMomentAlarm(1); + towerAlarm.setAlarmType(1); + } + if (alarmReasonCode == 23) { + towerAlarm.setMomentAlarm(2); + towerAlarm.setAlarmType(2); + } + if (alarmReasonCode == 14) { + towerAlarm.setObliguityAlarm(1); + towerAlarm.setAlarmType(1); + } + if (alarmReasonCode == 24) { + towerAlarm.setObliguityAlarm(2); + towerAlarm.setAlarmType(2); + } + if (alarmReasonCode == 15) { + towerAlarm.setWindSpeedAlarm(1); + towerAlarm.setAlarmType(1); + } + if (alarmReasonCode == 25) { + towerAlarm.setWindSpeedAlarm(2); + towerAlarm.setAlarmType(2); + } + if (alarmReasonCode == 101) { + towerAlarm.setMinRangeAlarm(1); + towerAlarm.setMaxRangeAlarm(1); + towerAlarm.setAlarmType(1); + } + if (alarmReasonCode == 201) { + towerAlarm.setMinRangeAlarm(2); + towerAlarm.setMaxRangeAlarm(2); + towerAlarm.setAlarmType(2); + } + if (alarmReasonCode == 102) { + towerAlarm.setHeightAlarm(1); + towerAlarm.setHeightLowerAlarm(1); + towerAlarm.setAlarmType(1); + } + if (alarmReasonCode == 202) { + towerAlarm.setHeightAlarm(2); + towerAlarm.setHeightLowerAlarm(2); + towerAlarm.setAlarmType(2); + } + if (alarmReasonCode == 103) { + towerAlarm.setPosAngleAlarm(1); + towerAlarm.setNegAngleAlarm(1); + towerAlarm.setAlarmType(1); + } + if (alarmReasonCode == 203) { + towerAlarm.setPosAngleAlarm(2); + towerAlarm.setNegAngleAlarm(2); + towerAlarm.setAlarmType(2); + } + towerAlarmService.saveTowerAlarm(towerAlarm); + } catch (Exception e) { + log.info("解析异常", e.getMessage()); + } + return result; + } + + public void register(String hexStr, ChannelHandlerContext ctx, int registerResult) { + Map result = new HashMap<>(); + try { + byte[] data = hexStringToByteArray(hexStr); + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.BIG_ENDIAN); // 大端序 + + // 1. 解析通用固定字段 + // 帧头 + byte[] startFlag = new byte[2]; + buffer.get(startFlag); + result.put("帧头", bytesToHex(startFlag)); + + // 帧类型 + byte frameType = buffer.get(); + result.put("帧类型", String.format("%02X", frameType)); + + // 塔吊编号 + byte towerId = buffer.get(); + result.put("塔吊编号", (int) towerId & 0xFF); + int towerCode = (int) towerId & 0xFF; + + // 设备编号(3字节) + byte[] deviceIdBytes = new byte[3]; + buffer.get(deviceIdBytes); + int deviceId = ((deviceIdBytes[0] & 0xFF) << 16) + | ((deviceIdBytes[1] & 0xFF) << 8) + | (deviceIdBytes[2] & 0xFF); + result.put("设备编号", deviceId); + + // 设备上行60帧:厂家代码 + 和校验 + 帧尾 + byte factoryCode = buffer.get(); + result.put("厂家代码", (int) factoryCode & 0xFF); + Date date = new Date(); + // 和校验 + // 1. 转换各字段为字节数组 + byte[] towerCraneNoBytes = decimalToFixedLengthBytes(towerCode, 1); + byte[] deviceNoBytes = decimalToFixedLengthBytes(deviceId, 3); + byte[] registerResultBytes = decimalToFixedLengthBytes(registerResult, 1); + byte[] timeBytes = parseTimeToBytes(DateUtil.formatDateTime(date)); + byte[] uploadIntervalBytes = decimalToFixedLengthBytes(15, 1); + // 2. 拼接需要参与和校验的字节数组(除和校验、帧尾外的所有字段) + byte[] checkSumSource = concatBytes( + FRAME_HEAD, FRAME_TYPE, towerCraneNoBytes, + deviceNoBytes, registerResultBytes, timeBytes, uploadIntervalBytes + ); + // 3. 计算和校验(不进位累加,转2字节) + byte[] checkSumBytes = calculateCheckSum(checkSumSource); + + + // 帧尾(4字节) + byte[] endFlag = new byte[4]; + buffer.get(endFlag); + result.put("帧尾", bytesToHex(endFlag)); + + // 4. 拼接所有字段生成完整帧 + byte[] fullFrameBytes = concatBytes( + FRAME_HEAD, FRAME_TYPE, towerCraneNoBytes, + deviceNoBytes, registerResultBytes, timeBytes, uploadIntervalBytes, + checkSumBytes, FRAME_TAIL + ); + +// int year = DateUtil.year(date); +// int month = DateUtil.month(date) + 1; +// int day = DateUtil.dayOfMonth(date); +// int hour = DateUtil.hour(date, true); +// int minute = DateUtil.minute(date); +// int second = DateUtil.second(date); +// String s = generateServerDownFrame(towerCode, deviceId, registerResult, year, month, day, hour, minute, second, 15); + // 5. 字节数组转十六进制字符串(大写) + String s = byteArrayToHexStringWithSpace(fullFrameBytes); + ctx.writeAndFlush(Unpooled.copiedBuffer(s, CharsetUtil.UTF_8)); + } catch (Exception e) { + log.info("解析异常", e.getMessage()); + } + } + + /** + * 字节数组转带空格分隔的十六进制字符串(大写) + * @param bytes 字节数组 + * @return 十六进制字符串(每个字节间用空格分隔) + */ + private static String byteArrayToHexStringWithSpace(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + // 每个字节转两位十六进制,大写 + sb.append(String.format("%02X", bytes[i])); + // 最后一个字节后不加空格 + if (i != bytes.length - 1) { + sb.append(" "); + } + } + return sb.toString(); + } + + /** + * 字节数组拼接 + * @param arrays 待拼接的字节数组 + * @return 拼接后的字节数组 + */ + private static byte[] concatBytes(byte[]... arrays) { + int totalLength = 0; + for (byte[] arr : arrays) { + totalLength += arr.length; + } + byte[] result = new byte[totalLength]; + int pos = 0; + for (byte[] arr : arrays) { + System.arraycopy(arr, 0, result, pos, arr.length); + pos += arr.length; + } + return result; + } + + /** + * 十进制数转指定长度的字节数组(大端序,不足补0) + * @param num 十进制数 + * @param length 字节长度 + * @return 字节数组 + */ + private static byte[] decimalToFixedLengthBytes(long num, int length) { + byte[] bytes = new byte[length]; + for (int i = length - 1; i >= 0; i--) { + bytes[i] = (byte) (num & 0xFF); + num >>= 8; + } + return bytes; + } + + /** + * 解析时间字符串为6字节数组(日、月、年后两位、时、分、秒) + * @param timeStr 时间字符串(yyyy-MM-dd HH:mm:ss) + * @return 6字节时间数组 + */ + private static byte[] parseTimeToBytes(String timeStr) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDateTime time = LocalDateTime.parse(timeStr, formatter); + + int day = time.getDayOfMonth(); + int month = time.getMonthValue(); + int yearLast2 = time.getYear() % 100; // 年取后两位 + int hour = time.getHour(); + int minute = time.getMinute(); + int second = time.getSecond(); + + return new byte[]{ + (byte) yearLast2, + (byte) month, + (byte) day, + (byte) hour, + (byte) minute, + (byte) second + }; + } + +// /** +// * 服务器端生成61帧(下行应答帧) +// */ +// private static String generateServerDownFrame(int towerId, int deviceId, int registerResult, +// int year, int month, int day, int hour, int minute, int second, +// int uploadInterval) { +// ByteBuffer buffer = ByteBuffer.allocate(20); // 2+1+1+3+1+6+1+2 +4(帧尾) = 21,先分配17 +// buffer.order(ByteOrder.BIG_ENDIAN); +// +// // 帧头 A55A +// buffer.put(FRAME_HEAD); +// // 帧类型 61 +// buffer.put((byte) 0x61); +// // 塔吊编号 +// buffer.put((byte) towerId); +// // 设备编号(3字节) +// buffer.put((byte) (deviceId >> 16 & 0xFF)); +// buffer.put((byte) (deviceId >> 8 & 0xFF)); +// buffer.put((byte) (deviceId & 0xFF)); +// // 注册结果 +// buffer.put((byte) registerResult); +// // 时间(年-2000) +// buffer.put((byte) (year - 2000)); +// buffer.put((byte) month); +// buffer.put((byte) day); +// buffer.put((byte) hour); +// buffer.put((byte) minute); +// buffer.put((byte) second); +// // 上传间隔 +// buffer.put((byte) uploadInterval); +// +// buffer.put(FRAME_TAIL); +// // 拼接帧尾 +// byte[] frameBytes = buffer.array(); +// String frameHex = bytesToHex(frameBytes); +// log.info(frameHex); +// // 3. 将字节数组转为带空格分隔的十六进制字符串(匹配示例格式) +// return bytesToHexWithSpace(buffer.array()); +// } + + /** + * 计算和校验:所有字节不进位累加,结果转2字节(低位在前/高位在前?示例中18 08是累加结果) + * @param source 参与校验的字节数组 + * @return 2字节的校验和(示例中累加结果为0x0818,转成字节数组是0x18, 0x08) + */ + private static byte[] calculateCheckSum(byte[] source) { + int sum = 0; + // 不进位累加:逐个字节转无符号int后累加 + for (byte b : source) { + sum += (b & 0xFF); // 转无符号,避免负数影响 + } + // 校验和占2字节,取低16位(sum可能超过16位,只保留后两位) + sum = sum & 0xFFFF; + // 拆分高低位(示例中sum=0x0818,对应字节18 08,即低位在前) + byte lowByte = (byte) (sum & 0xFF); // 低位:0x18 + byte highByte = (byte) ((sum >> 8) & 0xFF); // 高位:0x08 + return new byte[]{lowByte, highByte}; + } + + /** + * 字节数组转【带空格分隔】的十六进制字符串(核心调整点) + * 示例:byte[]{0xA5,0x5A} → "A5 5A" + */ + private static String bytesToHexWithSpace(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + // 转大写十六进制,补零 + sb.append(String.format("%02X", bytes[i])); + // 最后一个字节不加空格 + if (i < bytes.length - 1) { + sb.append(" "); + } + } + return sb.toString(); + } + + + /** + * 解析命令字0C的塔机实时数据 + * @param hexStr 无空格的十六进制字符串 + * @return 解析结果键值对 + */ + public void parseRealTimeData(String hexStr, Tower tower) { + TowerCurrentData towerCurrentData = new TowerCurrentData(); + towerCurrentData.setDevSn(tower.getDevSn()); + towerCurrentData.setDevName(tower.getDevName()); + towerCurrentData.setProjectSn(tower.getProjectSn()); + Map result = new HashMap<>(); + try { + // 1. 十六进制转字节数组 + byte[] data = hexStringToByteArray(hexStr); + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.BIG_ENDIAN); // 强制大端序 + + // -------------------------- 固定字段 -------------------------- + // 起始符 + byte[] startFlag = new byte[2]; + buffer.get(startFlag); + result.put("起始符", bytesToHex(startFlag)); + + // 命令字 + byte cmd = buffer.get(); + result.put("命令字", String.format("%02X", cmd)); + + // 塔机编号 + byte towerId = buffer.get(); + result.put("塔机编号", (int) towerId & 0xFF); + + // 设备编号(3字节手动拼接) + byte[] deviceIdBytes = new byte[3]; + buffer.get(deviceIdBytes); + int deviceId = ((deviceIdBytes[0] & 0xFF) << 16) + | ((deviceIdBytes[1] & 0xFF) << 8) + | (deviceIdBytes[2] & 0xFF); + result.put("设备编号", deviceId); + + // 版本号 + byte version = buffer.get(); + result.put("程序版本号", (int) version & 0xFF); + + // -------------------------- 时间字段 -------------------------- + byte[] timeBytes = new byte[6]; + buffer.get(timeBytes); + String time = parseTime(timeBytes); + result.put("时间", time); + towerCurrentData.setReciveTime(DateUtil.parseDateTime(time)); + towerCurrentData.setStartTime(DateUtil.parseDateTime(time)); + + // -------------------------- 基础数据 -------------------------- + // 厂家及设备类型 + byte factoryType = buffer.get(); + result.put("厂家及设备类型", (int) factoryType & 0xFF); + + // 高度数据 + short heightRaw = buffer.getShort(); + double height = heightRaw * HEIGHT_AMPLITUDE_WIND_COEFF; + result.put("高度实时值(米)", height); + towerCurrentData.setHeight(String.valueOf(height)); + + // 幅度数据 + short amplitudeRaw = buffer.getShort(); + double amplitude = amplitudeRaw * HEIGHT_AMPLITUDE_WIND_COEFF; + result.put("幅度(米)", amplitude); + towerCurrentData.setRanger(String.valueOf(amplitude)); + + // 回转角度 + short rotateRaw = buffer.getShort(); + double rotate = rotateRaw * ROTATE_COEFF; + result.put("回转(度)", rotate); + + // 起重量数据 + short weightRaw = buffer.getShort(); + double weight = weightRaw * WEIGHT_COEFF; + result.put("重量(吨)", weight); + towerCurrentData.setLoading(String.valueOf(weight)); + + // 风速数据 + short windRaw = buffer.getShort(); + double wind = windRaw * HEIGHT_AMPLITUDE_WIND_COEFF; + result.put("风速(米/秒)", wind); + towerCurrentData.setWindspeed(String.valueOf(wind)); + + // 倾角数据(核心特殊换算) + short angleRaw = buffer.getShort(); + double angle = (angleRaw - ANGLE_OFFSET) / ANGLE_COEFF; + result.put("倾角(度)", angle); + towerCurrentData.setObliguity(String.valueOf(angle)); + + // -------------------------- 百分比字段 -------------------------- + // 重量百分比 + byte weightPercent = buffer.get(); + result.put("重量百分比(%)", (int) weightPercent & 0xFF); + towerCurrentData.setLoadRatio(String.valueOf((int) weightPercent & 0xFF)); + + // 力矩百分比 + byte momentPercent = buffer.get(); + result.put("力矩百分比(%)", (int) momentPercent & 0xFF); + towerCurrentData.setTorqueRatio(String.valueOf((int) momentPercent & 0xFF)); + + // 风速百分比 + byte windPercent = buffer.get(); + result.put("风速百分比(%)", (int) windPercent & 0xFF); + + // 倾斜百分比 + byte tiltPercent = buffer.get(); + result.put("倾斜百分比(%)", (int) tiltPercent & 0xFF); + + // -------------------------- 报警/制动 -------------------------- + // 警报原因 + byte alarmReason = buffer.get(); + int alarmCode = (int) alarmReason & 0xFF; + result.put("警报原因编码", alarmCode); + result.put("警报原因描述", parseAlarmReason(alarmCode)); + + // 制动状态 + byte brakeStatus = buffer.get(); + int brakeInt = (int) brakeStatus & 0xFF; + result.put("制动状态编码", brakeInt); + result.put("制动状态描述", parseBrakeStatus(brakeInt)); + + // -------------------------- 结束符 -------------------------- + byte[] endFlag = new byte[6]; + buffer.get(endFlag); + result.put("结束符", bytesToHex(endFlag)); + towerCurrentDataService.saveTowerCurrentData(towerCurrentData); + } catch (Exception e) { + log.info("解析异常", e.getMessage()); + } + } + + /** + * 解析工作循环数据 + * @param hexStr 无空格的十六进制字符串(实例:A55A3D0290F72F02111301080A1C211301080A1D2117D300C7012C008200000000000000000082000000000104B52DCC33C33C) + * @return 解析后的字段键值对 + */ + public void parseWorkCycleData(String hexStr, Tower tower) { + TowerWorkCycle workCycle = new TowerWorkCycle(); + try { + // 1. 十六进制字符串转字节数组 + byte[] data = hexStringToByteArray(hexStr); + // 2. 初始化ByteBuffer,设置大端序(网络字节序) + ByteBuffer buffer = ByteBuffer.wrap(data); + buffer.order(ByteOrder.BIG_ENDIAN); + + // -------------------------- 固定字段解析 -------------------------- + // 起始符(2字节) + byte[] startFlag = new byte[2]; + buffer.get(startFlag); + + // 命令字(1字节) + byte cmd = buffer.get(); + + // 塔机编号(1字节) + byte towerId = buffer.get(); + + // 设备编号(3字节:特殊处理,手动拼接大端序) + byte[] deviceIdBytes = new byte[3]; + buffer.get(deviceIdBytes); + int deviceId = ((deviceIdBytes[0] & 0xFF) << 16) + | ((deviceIdBytes[1] & 0xFF) << 8) + | (deviceIdBytes[2] & 0xFF); + workCycle.setDevSn(tower.getDevSn()); + workCycle.setDevName(tower.getDevName()); + workCycle.setProjectSn(tower.getProjectSn()); + + // 版本号(1字节) + byte version = buffer.get(); + + // 厂家及设备类型(1字节:位运算解析) + byte factoryDeviceType = buffer.get(); + int typeInt = factoryDeviceType & 0xFF; + boolean isLocked = (typeInt & 0x80) != 0; // 最高位(b7):1=锁机,0=未锁机 + int factoryCode = typeInt & 0x0F; // 低4位:厂家/设备类型编码 + + // -------------------------- 时间字段解析 -------------------------- + // 开始时间(6字节:年、月、日、时、分、秒) + byte[] startTimeBytes = new byte[6]; + buffer.get(startTimeBytes); + String startTime = parseTime(startTimeBytes); + workCycle.setStartTime(DateUtil.parseDateTime(startTime)); + + // 结束时间(6字节:年、月、日、时、分、秒) + byte[] endTimeBytes = new byte[6]; + buffer.get(endTimeBytes); + String endTime = parseTime(endTimeBytes); + workCycle.setEndTime(DateUtil.parseDateTime(endTime)); + if (startTime != null && endTime != null) { + workCycle.setWorkTime(String.valueOf(DateUtil.between(DateUtil.parseDateTime(startTime), DateUtil.parseDateTime(endTime), DateUnit.SECOND))); + } + workCycle.setAddTime(new Date()); + + // -------------------------- 物理量解析 -------------------------- + // 本次循环最大吊重(2字节) + short maxWeightRaw = buffer.getShort(); + double maxWeight = maxWeightRaw * WEIGHT_COEFFICIENT; + workCycle.setLoading(String.valueOf(maxWeight)); + + // 本次循环最大力矩(2字节) + short maxMomentRaw = buffer.getShort(); + double maxMoment = maxMomentRaw * MOMENT_COEFFICIENT; + workCycle.setWorkMaxForce(String.valueOf(maxMoment)); + + // 最大高度(2字节) + short maxHeightRaw = buffer.getShort(); + double maxHeight = maxHeightRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setMaxHeight(String.valueOf(maxHeight)); + + // 最小高度(2字节) + short minHeightRaw = buffer.getShort(); + double minHeight = minHeightRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setMinHeight(String.valueOf(minHeight)); + + // 最大幅度(2字节) + short maxAmplitudeRaw = buffer.getShort(); + double maxAmplitude = maxAmplitudeRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setMaxRange(String.valueOf(maxAmplitude)); + + // 最小幅度(2字节) + short minAmplitudeRaw = buffer.getShort(); + double minAmplitude = minAmplitudeRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setMinRange(String.valueOf(minAmplitude)); + + // 起吊点角度(2字节) + short liftAngleRaw = buffer.getShort(); + double liftAngle = liftAngleRaw * ANGLE_COEFFICIENT; + workCycle.setSlingStartRotation(String.valueOf(liftAngle)); + + // 起吊点幅度(2字节) + short liftAmplitudeRaw = buffer.getShort(); + double liftAmplitude = liftAmplitudeRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setSlingStartRange(String.valueOf(liftAmplitude)); + + // 起吊点高度(2字节) + short liftHeightRaw = buffer.getShort(); + double liftHeight = liftHeightRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setSlingStartHeight(String.valueOf(liftHeight)); + + // 卸吊点角度(2字节) + short unloadAngleRaw = buffer.getShort(); + double unloadAngle = unloadAngleRaw * ANGLE_COEFFICIENT; + workCycle.setSlingEndRotation(String.valueOf(unloadAngle)); + + // 卸吊点幅度(2字节) + short unloadAmplitudeRaw = buffer.getShort(); + double unloadAmplitude = unloadAmplitudeRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setSlingEndRange(String.valueOf(unloadAmplitude)); + + // 卸吊点高度(2字节) + short unloadHeightRaw = buffer.getShort(); + double unloadHeight = unloadHeightRaw * HEIGHT_AMPLITUDE_COEFFICIENT; + workCycle.setSlingEndHeight(String.valueOf(unloadHeight)); + + // -------------------------- 结束符解析 -------------------------- + byte[] endFlag = new byte[6]; + buffer.get(endFlag); + + } catch (Exception e) { + log.info("TCP解析异常", e.getMessage()); + } + towerWorkCycleService.save(workCycle); + } + + /** + * 解析警报原因编码 + */ + private String parseAlarmReason(int code) { + String alarm = ""; + switch (code) { + case 0 : + alarm = "未报警"; + break; +// case 111 -> "碰撞预警"; +// case 211 -> "碰撞报警"; +// case 101 -> "幅度限位报警"; +// case 102 -> "高度限位报警"; +// case 103 -> "回转限位报警"; +// case 201 -> "幅度限位预警"; +// case 202 -> "高度限位预警"; +// case 203 -> "回转限位预警"; +// case 12 -> "重量报警"; +// case 13 -> "力矩报警"; +// case 14 -> "倾斜报警"; +// case 15 -> "风速报警"; +// case 22 -> "重量预警"; +// case 23 -> "力矩预警"; +// case 24 -> "倾斜预警"; +// case 25 -> "风速预警"; +// default -> "未知报警/预警类型"; + }; + return alarm; + } + + /** + * 解析制动状态(8bit:低到高=上下前后左右,1=断开,0=闭合) + */ + private static String parseBrakeStatus(int code) { + if (code == 0) { + return "未制动(所有方向闭合)"; + } + StringBuilder desc = new StringBuilder(); + String[] directions = {"下", "上", "后", "前", "右", "左"}; // 低到高bit0~bit5 + for (int i = 0; i < 6; i++) { + int bit = (code >> i) & 1; + desc.append(directions[i]).append(":").append(bit == 1 ? "断开" : "闭合").append(";"); + } + return desc.toString(); + } + + + /** + * 解析6字节时间(年、月、日、时、分、秒) + * @param timeBytes 6字节时间数组 + * @return 格式化时间字符串:yyyy-MM-dd HH:mm:ss + */ + private static String parseTime(byte[] timeBytes) { + // 年:字节转无符号十进制 + 2000 + int year = (timeBytes[0] & 0xFF) + 2000; + // 月/日/时/分/秒:字节转无符号十进制 + int month = timeBytes[1] & 0xFF; + int day = timeBytes[2] & 0xFF; + int hour = timeBytes[3] & 0xFF; + int minute = timeBytes[4] & 0xFF; + int second = timeBytes[5] & 0xFF; + // 格式化(补零) + return String.format("%04d-%02d-%02d %02d:%02d:%02d", + year, month, day, hour, minute, second); + } + + /** + * 十六进制字符串转字节数组(去除空格) + */ + private static byte[] hexStringToByteArray(String hexStr) { + hexStr = hexStr.replaceAll("\\s+", ""); // 移除所有空格 + int len = hexStr.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(hexStr.charAt(i), 16) << 4) + + Character.digit(hexStr.charAt(i + 1), 16)); + } + return data; + } + + /** + * 字节数组转十六进制字符串(用于输出起始符/结束符) + */ + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X", b)); + } + return sb.toString(); + } +}