From 73aa0d06aa208c657c20c472780503a10595c485 Mon Sep 17 00:00:00 2001 From: "review512jwy@163.com" <“review512jwy@163.com”> Date: Thu, 25 Jun 2026 13:43:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=90=88=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/MainReceiverController.java | 6 + .../datacenter/sender/dao/CsdjDao.java | 60 ++++ .../sender/entitiy/csdj/CsdjAlarmHistory.java | 93 ++++++ .../csdj/CsdjDigitalInputTerminal.java | 46 +++ .../sender/entitiy/{ => csdj}/CsdjEntity.java | 2 +- .../entitiy/csdj/CsdjNotificationData.java | 44 +++ .../sender/service/CsdjService.java | 195 ++++++++++++ .../utils/csdj/CsdjNotificationParser.java | 296 ++++++++++++++++++ 8 files changed, 741 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/techsor/datacenter/sender/dao/CsdjDao.java create mode 100644 src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjAlarmHistory.java create mode 100644 src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjDigitalInputTerminal.java rename src/main/java/com/techsor/datacenter/sender/entitiy/{ => csdj}/CsdjEntity.java (78%) create mode 100644 src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjNotificationData.java create mode 100644 src/main/java/com/techsor/datacenter/sender/service/CsdjService.java create mode 100644 src/main/java/com/techsor/datacenter/sender/utils/csdj/CsdjNotificationParser.java diff --git a/src/main/java/com/techsor/datacenter/sender/controllers/MainReceiverController.java b/src/main/java/com/techsor/datacenter/sender/controllers/MainReceiverController.java index 0d6ed3f..a5bfcaa 100644 --- a/src/main/java/com/techsor/datacenter/sender/controllers/MainReceiverController.java +++ b/src/main/java/com/techsor/datacenter/sender/controllers/MainReceiverController.java @@ -7,6 +7,7 @@ import com.techsor.datacenter.sender.config.DataSourceContextHolder; import com.techsor.datacenter.sender.constants.CompanyConstants; import com.techsor.datacenter.sender.dao.CsdjLogDao; import com.techsor.datacenter.sender.entitiy.*; +import com.techsor.datacenter.sender.entitiy.csdj.CsdjEntity; import com.techsor.datacenter.sender.entitiy.f10.RedisStreamEntity; import com.techsor.datacenter.sender.entitiy.zaiot.ProcessZAIoTRAWEntity; import com.techsor.datacenter.sender.processers.*; @@ -64,6 +65,9 @@ public class MainReceiverController { @Resource F10Service f10Service; + @Resource + CsdjService csdjService; + @Autowired private AsyncDataProcessor asyncDataProcessor; @@ -225,6 +229,8 @@ public class MainReceiverController { csdjEntity.getData(), System.currentTimeMillis() ); + + csdjService.handle(csdjEntity); }catch (Exception e){ logger.error("csdjProcess error", e); } diff --git a/src/main/java/com/techsor/datacenter/sender/dao/CsdjDao.java b/src/main/java/com/techsor/datacenter/sender/dao/CsdjDao.java new file mode 100644 index 0000000..3a9e8a3 --- /dev/null +++ b/src/main/java/com/techsor/datacenter/sender/dao/CsdjDao.java @@ -0,0 +1,60 @@ + +package com.techsor.datacenter.sender.dao; + +import com.techsor.datacenter.sender.entitiy.csdj.CsdjAlarmHistory; +import jakarta.annotation.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class CsdjDao { + + @Resource + private JdbcTemplate jdbcTemplate; + + /** + * 插入CSDJ告警历史记录 + * @param csdjAlarmHistory 告警历史对象 + */ + public void insertAlarm(CsdjAlarmHistory csdjAlarmHistory) { + String sql = "INSERT IGNORE INTO csdj_alarm_history (" + + "company_id, raw_hex, terminal_id, terminal_no, " + + "alarm_time, alarm_timestamp, recovery_time, recovery_timestamp, " + + "alarm_state, state_raw, abnormal_mode, alert_history_id, " + + "created_at, updated_at" + + ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + + jdbcTemplate.update(sql, + csdjAlarmHistory.getCompanyId(), + csdjAlarmHistory.getRawHex(), + csdjAlarmHistory.getTerminalId(), + csdjAlarmHistory.getTerminalNo(), + + // 时间处理:LocalDateTime 转 java.sql.Timestamp + csdjAlarmHistory.getAlarmTime() != null + ? java.sql.Timestamp.valueOf(csdjAlarmHistory.getAlarmTime()) + : null, + csdjAlarmHistory.getAlarmTimestamp(), + csdjAlarmHistory.getRecoveryTime() != null + ? java.sql.Timestamp.valueOf(csdjAlarmHistory.getRecoveryTime()) + : null, + csdjAlarmHistory.getRecoveryTimestamp(), + + csdjAlarmHistory.getAlarmState(), + csdjAlarmHistory.getStateRaw(), + csdjAlarmHistory.getAbnormalMode(), + csdjAlarmHistory.getAlertHistoryId(), + + // 自动时间字段 + csdjAlarmHistory.getCreatedAt() != null + ? java.sql.Timestamp.valueOf(csdjAlarmHistory.getCreatedAt()) + : java.sql.Timestamp.valueOf(LocalDateTime.now()), + csdjAlarmHistory.getUpdatedAt() != null + ? java.sql.Timestamp.valueOf(csdjAlarmHistory.getUpdatedAt()) + : java.sql.Timestamp.valueOf(LocalDateTime.now()) + ); + } + +} diff --git a/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjAlarmHistory.java b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjAlarmHistory.java new file mode 100644 index 0000000..b7f1054 --- /dev/null +++ b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjAlarmHistory.java @@ -0,0 +1,93 @@ + +package com.techsor.datacenter.sender.entitiy.csdj; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * CSDJ告警历史表实体类 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CsdjAlarmHistory { + /** + * 主键ID + */ + private Long id; + + /** + * 公司ID + */ + private Long companyId; + + /** + * 原始HEX报文 + */ + private String rawHex; + + /** + * 终端ID + */ + private String terminalId; + + /** + * 端子番号 + */ + private Integer terminalNo; + + /** + * 告警时间(日本时区) + */ + private LocalDateTime alarmTime; + + /** + * 告警时间戳(毫秒) + */ + private Long alarmTimestamp; + + /** + * 恢复时间(日本时区) + */ + private LocalDateTime recoveryTime; + + /** + * 恢复时间戳(毫秒) + */ + private Long recoveryTimestamp; + + /** + * 告警状态(0=已恢复,1=告警中) + */ + private Integer alarmState; + + /** + * 端子状态原始值 + */ + private Integer stateRaw; + + /** + * 异常模式 + */ + private Integer abnormalMode; + + /** + * 告警表主键ID + */ + private Long alertHistoryId; + + /** + * 创建时间 + */ + private LocalDateTime createdAt; + + /** + * 更新时间 + */ + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjDigitalInputTerminal.java b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjDigitalInputTerminal.java new file mode 100644 index 0000000..e3d28da --- /dev/null +++ b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjDigitalInputTerminal.java @@ -0,0 +1,46 @@ + +package com.techsor.datacenter.sender.entitiy.csdj; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * CSDJ デジタル入力端子(D开头)数据 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CsdjDigitalInputTerminal { + /** + * 端子番号 + */ + private Integer terminalNo; + + /** + * 端子状态原始字节 + */ + private Integer stateRaw; + + /** + * 是否是异常告警(true=合上/告警,false=断开/恢复) + */ + private Boolean isAlarm; + + /** + * 异常模式:0=イベント,1=パルス積算,2=時間積算 + */ + private Integer abnormalMode; + + /** + * 附加数据(只有パルス積算/時間積算时有值) + */ + private Long additionalData; + + /** + * 端子状态文本说明 + */ + private String stateText; +} diff --git a/src/main/java/com/techsor/datacenter/sender/entitiy/CsdjEntity.java b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjEntity.java similarity index 78% rename from src/main/java/com/techsor/datacenter/sender/entitiy/CsdjEntity.java rename to src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjEntity.java index bef7281..7c6d2f4 100644 --- a/src/main/java/com/techsor/datacenter/sender/entitiy/CsdjEntity.java +++ b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjEntity.java @@ -1,4 +1,4 @@ -package com.techsor.datacenter.sender.entitiy; +package com.techsor.datacenter.sender.entitiy.csdj; import lombok.Data; diff --git a/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjNotificationData.java b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjNotificationData.java new file mode 100644 index 0000000..50be03f --- /dev/null +++ b/src/main/java/com/techsor/datacenter/sender/entitiy/csdj/CsdjNotificationData.java @@ -0,0 +1,44 @@ + +package com.techsor.datacenter.sender.entitiy.csdj; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * CSDJ 通報データ解析结果 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CsdjNotificationData { + /** + * 通報データ的总长度(infoPart里的长度) + */ + private Integer dataLength; + + /** + * 通報类型:0x07=端子状态通知(トリガ動作) + */ + private Integer notificationType; + + /** + * 通報发生时间 + */ + private LocalDateTime notificationTime; + + /** + * 日本时区时间戳(毫秒) + */ + private Long japanTimestamp; + + /** + * デジタル入力端子列表 + */ + private List digitalInputTerminals; +} diff --git a/src/main/java/com/techsor/datacenter/sender/service/CsdjService.java b/src/main/java/com/techsor/datacenter/sender/service/CsdjService.java new file mode 100644 index 0000000..554ec19 --- /dev/null +++ b/src/main/java/com/techsor/datacenter/sender/service/CsdjService.java @@ -0,0 +1,195 @@ + +package com.techsor.datacenter.sender.service; + +import cn.hutool.core.collection.CollectionUtil; +import com.techsor.datacenter.sender.components.CommonOpt; +import com.techsor.datacenter.sender.components.GuavaRedisCache; +import com.techsor.datacenter.sender.config.DataSourceContextHolder; +import com.techsor.datacenter.sender.dao.CsdjDao; +import com.techsor.datacenter.sender.dao.DashboardAlertDao; +import com.techsor.datacenter.sender.dao.DeviceDao; +import com.techsor.datacenter.sender.dao.F10Dao; +import com.techsor.datacenter.sender.dto.DeviceInfoVO; +import com.techsor.datacenter.sender.entitiy.DeviceEntity; +import com.techsor.datacenter.sender.entitiy.DynamodbEntity; +import com.techsor.datacenter.sender.entitiy.csdj.CsdjAlarmHistory; +import com.techsor.datacenter.sender.entitiy.csdj.CsdjDigitalInputTerminal; +import com.techsor.datacenter.sender.entitiy.csdj.CsdjEntity; +import com.techsor.datacenter.sender.entitiy.csdj.CsdjNotificationData; +import com.techsor.datacenter.sender.utils.RedisUtils; +import com.techsor.datacenter.sender.utils.csdj.CsdjNotificationParser; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * CSDJ 业务处理服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CsdjService { + + private final CsdjNotificationParser csdjNotificationParser; + + private final CommonOpt commonOpt; + + private final GuavaRedisCache guavaRedisCache; + + private final RedisUtils redisUtils; + + private final CsdjDao csdjDao; + + private final DeviceDao deviceDao; + + private final DashboardAlertDao dashboardAlertDao; + + private final IDataProcessService dataProcessService; + + /** + * 处理 CSDJ 通報数据 + * @param csdjEntity 接收到的数据 + */ + public void handle(CsdjEntity csdjEntity) { + try { + log.info("[CSDJ] 开始处理 CSDJ 数据,terminalId:{}, data:{}", + csdjEntity.getTerminalId(), csdjEntity.getData()); + + // 1. 解析通報数据 + CsdjNotificationData notificationData = csdjNotificationParser.parse(csdjEntity.getData()); + if (notificationData == null) { + log.warn("[CSDJ] 解析通報数据失败,terminalId:{}", csdjEntity.getTerminalId()); + return; + } + + log.info("[CSDJ] 解析成功,notificationType:0x{}, time:{}", + Integer.toHexString(notificationData.getNotificationType() & 0xFF).toUpperCase(), + notificationData.getNotificationTime()); + + // 2. 处理デジタル入力端子(D开头,告警/恢复) + List digitalTerminals = notificationData.getDigitalInputTerminals(); + if (digitalTerminals != null && !digitalTerminals.isEmpty()) { + for (CsdjDigitalInputTerminal terminal : digitalTerminals) { + processDigitalTerminal(csdjEntity, notificationData, terminal); + } + } else { + log.info("[CSDJ] 没有デジタル入力端子数据"); + } + + log.info("[CSDJ] 处理 CSDJ 数据完成,terminalId:{}", csdjEntity.getTerminalId()); + } catch (Exception e) { + log.error("[CSDJ] 处理 CSDJ 数据失败,terminalId:{}", csdjEntity.getTerminalId(), e); + } + } + + /** + * 处理デジタル入力端子的告警/恢复 + */ + private void processDigitalTerminal(CsdjEntity csdjEntity, CsdjNotificationData notificationData, CsdjDigitalInputTerminal terminal) { + String terminalId = csdjEntity.getTerminalId(); + Integer terminalNo = terminal.getTerminalNo(); + Boolean isAlarm = terminal.getIsAlarm(); + String stateText = terminal.getStateText(); + + log.info("[CSDJ] デジタル入力端子,terminalId:{}, No:{}, state:{}, isAlarm:{}", + terminalId, terminalNo, stateText, isAlarm); + +// // ========== 在这里写你的告警/恢复逻辑 ========== +// if (isAlarm) { +// // TODO: 告警逻辑(合上) +// // 比如: +// // - 查设备信息 +// // - 保存告警记录 +// // - 推送告警通知 +// log.info("[CSDJ] 发生告警!terminalId:{}, terminalNo:{}, state:{}", +// terminalId, terminalNo, stateText); +// } else { +// // TODO: 恢复逻辑(断开) +// // 比如: +// // - 更新告警记录为恢复状态 +// // - 发送恢复通知 +// log.info("[CSDJ] 告警恢复!terminalId:{}, terminalNo:{}, state:{}", +// terminalId, terminalNo, stateText); +// } + + String topCompanyId= commonOpt.getTopCompanyId(terminalId); + if (topCompanyId==null || topCompanyId.equals("0")){ + log.error("no companyId is found========================>>>> {}",terminalId); + return; + } + DataSourceContextHolder.clearCurrentDataSourceKey(); + DataSourceContextHolder.setCurrentDataSourceKey("dataSourceForCompany_"+topCompanyId); + + log.info("Use datasource for company:"+topCompanyId); + List deviceEntity=this.deviceDao.getDeviceInfoByDeviceId(terminalId); + this.guavaRedisCache.incrementDailyDeviceIdCount(terminalId); + if (CollectionUtil.isEmpty(deviceEntity)){ + log.error("no deviceEntity is found========================>>>> {}",terminalId); + return; + } + + long currentTs = System.currentTimeMillis(); + + Long companyId = deviceEntity.get(0).getCompanyId(); + + CsdjAlarmHistory csdjAlarmHistory = new CsdjAlarmHistory(); + + // 设置基础字段 + csdjAlarmHistory.setCompanyId(companyId); + csdjAlarmHistory.setTerminalId(terminalId); + csdjAlarmHistory.setTerminalNo(terminalNo); + csdjAlarmHistory.setStateRaw(terminal.getStateRaw()); + csdjAlarmHistory.setAbnormalMode(terminal.getAbnormalMode()); + + // 设置原始HEX报文 + String rawHex = csdjEntity.getData() != null ? csdjEntity.getData() : "N/A"; + csdjAlarmHistory.setRawHex(rawHex); + + DeviceInfoVO deviceInfoVO = new DeviceInfoVO(); + deviceInfoVO.setDeviceId(deviceEntity.get(0).getDeviceId()); + deviceInfoVO.setBuildingId(deviceEntity.get(0).getBuildingId()); + deviceInfoVO.setBigClassId(deviceEntity.get(0).getBigClassId()); + + DynamodbEntity entity = new DynamodbEntity(); + entity.setDeviceId(terminalId); + + if (isAlarm){ + // 告警逻辑 + dataProcessService.setAlertBlock(deviceInfoVO); + entity.setAlertBlock(deviceInfoVO.getAlertBlock()); + + entity.setReceive_ts(currentTs); + Long dbId = dashboardAlertDao.insertAlertHistory(entity, 3); + csdjAlarmHistory.setAlertHistoryId(dbId); + + // 设置告警相关字段 + csdjAlarmHistory.setAlarmTime(notificationData.getNotificationTime()); + csdjAlarmHistory.setAlarmTimestamp(notificationData.getJapanTimestamp()); + csdjAlarmHistory.setAlarmState(1); // 1=告警中 + csdjAlarmHistory.setRecoveryTime(null); + csdjAlarmHistory.setRecoveryTimestamp(null); + + log.info("[CSDJ] 发生告警!terminalId:{}, terminalNo:{}, state:{}", + terminalId, terminalNo, stateText); + } else { + // 恢复逻辑 + dashboardAlertDao.updateLatestAlertToAutoRecovered(entity); + + // 设置恢复相关字段 + csdjAlarmHistory.setAlarmState(0); // 0=已恢复 + csdjAlarmHistory.setRecoveryTime(notificationData.getNotificationTime()); + csdjAlarmHistory.setRecoveryTimestamp(notificationData.getJapanTimestamp()); + + log.info("[CSDJ] 告警恢复!terminalId:{}, terminalNo:{}, state:{}", + terminalId, terminalNo, stateText); + } + + // 插入告警历史记录 + csdjDao.insertAlarm(csdjAlarmHistory); + + } +} diff --git a/src/main/java/com/techsor/datacenter/sender/utils/csdj/CsdjNotificationParser.java b/src/main/java/com/techsor/datacenter/sender/utils/csdj/CsdjNotificationParser.java new file mode 100644 index 0000000..61901be --- /dev/null +++ b/src/main/java/com/techsor/datacenter/sender/utils/csdj/CsdjNotificationParser.java @@ -0,0 +1,296 @@ + +package com.techsor.datacenter.sender.utils.csdj; + +import com.techsor.datacenter.sender.entitiy.csdj.CsdjDigitalInputTerminal; +import com.techsor.datacenter.sender.entitiy.csdj.CsdjNotificationData; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * CSDJ 通報データ解析工具类 + */ +@Slf4j +@Component +public class CsdjNotificationParser { + /** + * 日本时区 ZoneId + */ + private static final ZoneId JAPAN_ZONE = ZoneId.of("Asia/Tokyo"); + + /** + * 端子種別 - デジタル入力 + */ + private static final byte TERMINAL_TYPE_DI = (byte) 0xC0; + /** + * 端子種別 - アナログ入力 + */ + private static final byte TERMINAL_TYPE_AI = (byte) 0xC2; + /** + * 端子種別 - デジタル出力 + */ + private static final byte TERMINAL_TYPE_DO = (byte) 0xC3; + /** + * 端子種別 - システム状態 + */ + private static final byte TERMINAL_TYPE_SYSTEM = (byte) 0xC6; + + /** + * 解析通報データ的十六进制字符串 + * @param hexData 十六进制字符串(带空格或不带都可以) + * @return 解析结果 + */ + public CsdjNotificationData parse(String hexData) { + try { + // 1. 把十六进制字符串转成字节数组 + byte[] data = hexStringToBytes(hexData); + + log.info("[CSDJ] 开始解析,原始数据长度:{}", data.length); + log.info("[CSDJ] 原始数据 hex:{}", bytesToHex(data)); +// for (int i = 0; i < data.length; i++) { +// log.info("[CSDJ] index {} → {}", i, String.format("%02X", data[i] & 0xFF)); +// } + + // 2. 解析通報数据 + return parseData(data); + } catch (Exception e) { + log.error("[CSDJ] 解析通報数据失败,hexData:{}", hexData, e); + return null; + } + } + + /** + * 解析通報データ的字节数组 + * @param data infoPart字节数组 + * @return 解析结果 + */ + public CsdjNotificationData parseData(byte[] data) { + if (data == null || data.length < 10) { + log.warn("[CSDJ] 数据长度太短,无法解析,length:{}", data == null ? 0 : data.length); + return null; + } + + CsdjNotificationData.CsdjNotificationDataBuilder builder = CsdjNotificationData.builder(); + + // ========== 1. 解析开头的部分 ========== + int offset = 0; + + // 数据长(2字节,大端序) + int dataLength = ((data[offset + 0] & 0xFF) << 8) | (data[offset + 1] & 0xFF); + builder.dataLength(dataLength); + offset += 2; + log.info("[CSDJ] [Offset:{}] 数据长:{}", offset - 2, dataLength); + + // 端子状态通知(1字节) + int notificationType = data[offset] & 0xFF; + builder.notificationType(notificationType); + offset += 1; + log.info("[CSDJ] [Offset:{}] 通報类型:0x{}", offset - 1, Integer.toHexString(notificationType & 0xFF).toUpperCase()); + + // 预留(1字节) + offset += 1; + log.info("[CSDJ] [Offset:{}] 预留 1字节", offset - 1); + + // 时间数据(6字节:年两位、月、日、时、分、秒,都是BCD码) + LocalDateTime time = parseTime(data, offset); + builder.notificationTime(time); + offset += 6; + log.info("[CSDJ] [Offset:{}] 时间:{}", offset - 6, time); + + // 计算日本时区时间戳 + if (time != null) { + long timestamp = convertToJapanTimestamp(time); + builder.japanTimestamp(timestamp); + log.info("[CSDJ] [Offset:{}] 日本时区时间戳:{}", offset - 6, timestamp); + } + + log.info("[CSDJ] [Offset:{}] 开头部分解析完成,准备解析端子", offset); + + // ========== 2. 解析各个端子块 ========== + List digitalInputTerminals = new ArrayList<>(); + + while (offset < data.length - 1) { + byte terminalType = data[offset]; + offset += 1; + + log.info("[CSDJ] [Offset:{}] 端子类型:0x{}", offset - 1, Integer.toHexString(terminalType & 0xFF).toUpperCase()); + + if (terminalType == TERMINAL_TYPE_DI) { + // ========== デジタル入力端子 ========== + if (offset >= data.length) { + log.warn("[CSDJ] 数据不足,无法读取端子数"); + break; + } + + int terminalCount = data[offset] & 0xFF; + offset += 1; + log.info("[CSDJ] [Offset:{}] 端子数:{}", offset - 1, terminalCount); + + for (int i = 0; i < terminalCount; i++) { + if (offset + 6 > data.length) { + log.warn("[CSDJ] 数据不足,无法读取第{}个端子,offset:{}, length:{}", i + 1, offset, data.length); + break; + } + + CsdjDigitalInputTerminal terminal = parseDigitalInput(data, offset); + digitalInputTerminals.add(terminal); + offset += 6; // 每个端子6字节 + } + } else if (terminalType == TERMINAL_TYPE_AI || terminalType == TERMINAL_TYPE_DO || terminalType == TERMINAL_TYPE_SYSTEM) { + // ========== 其他类型暂时跳过 ========== + if (offset >= data.length) { + break; + } + + int terminalCount = data[offset] & 0xFF; + offset += 1; + log.info("[CSDJ] [Offset:{}] 跳过端子类型:0x{}, 端子数:{}", offset - 1, Integer.toHexString(terminalType & 0xFF).toUpperCase(), terminalCount); + offset += 6 * terminalCount; + } else { + // 未知类型,继续往下走 + log.warn("[CSDJ] [Offset:{}] 未知的端子类型:0x{}", offset - 1, Integer.toHexString(terminalType & 0xFF).toUpperCase()); + } + } + + builder.digitalInputTerminals(digitalInputTerminals); + log.info("[CSDJ] 解析完成,找到デジタル入力端子数:{}", digitalInputTerminals.size()); + return builder.build(); + } + + /** + * 解析デジタル入力端子 + */ + private CsdjDigitalInputTerminal parseDigitalInput(byte[] data, int offset) { + CsdjDigitalInputTerminal.CsdjDigitalInputTerminalBuilder builder = CsdjDigitalInputTerminal.builder(); + + // 第1字节:端子状态 + byte stateByte = data[offset]; + int stateRaw = stateByte & 0xFF; + builder.stateRaw(stateRaw); + offset += 1; + + // 第2字节:端子番号 + int terminalNo = data[offset] & 0xFF; + builder.terminalNo(terminalNo); + offset += 1; + + // 第3-6字节:附加数据(大端序) + long additionalData = 0; + additionalData = (additionalData << 8) | (data[offset + 0] & 0xFF); + additionalData = (additionalData << 8) | (data[offset + 1] & 0xFF); + additionalData = (additionalData << 8) | (data[offset + 2] & 0xFF); + additionalData = (additionalData << 8) | (data[offset + 3] & 0xFF); + builder.additionalData(additionalData); + + // ========== 解析端子状态 ========== + // b3位:1=異常,0=復旧 + boolean isAlarm = (stateByte & 0x08) != 0; + builder.isAlarm(isAlarm); + + // b6-b5位:异常模式 + int abnormalMode = (stateByte >> 5) & 0x03; + builder.abnormalMode(abnormalMode); + + // 状态文本 + String stateText = isAlarm ? "異常(告警/合上)" : "復旧(恢复/断开)"; + switch (abnormalMode) { + case 0: + stateText = isAlarm ? "イベント異常" : "イベント復旧"; + break; + case 1: + stateText = isAlarm ? "パルス積算異常" : "パルス積算復旧"; + break; + case 2: + stateText = isAlarm ? "時間積算異常" : "時間積算復旧"; + break; + } + builder.stateText(stateText); + + log.info("[CSDJ] [Offset:{}] デジタル入力端子解析:No:{}, state:0x{}, isAlarm:{}", + offset - 6, terminalNo, Integer.toHexString(stateRaw).toUpperCase(), isAlarm); + + return builder.build(); + } + + /** + * 解析时间(BCD码) + */ + private LocalDateTime parseTime(byte[] data, int offset) { + try { + // BCD码转十进制 + int yearBcd = data[offset + 0] & 0xFF; + int year = (yearBcd >> 4) * 10 + (yearBcd & 0x0F); + year += 2000; // 加上2000年 + + int monthBcd = data[offset + 1] & 0xFF; + int month = (monthBcd >> 4) * 10 + (monthBcd & 0x0F); + + int dayBcd = data[offset + 2] & 0xFF; + int day = (dayBcd >> 4) * 10 + (dayBcd & 0x0F); + + int hourBcd = data[offset + 3] & 0xFF; + int hour = (hourBcd >> 4) * 10 + (hourBcd & 0x0F); + + int minuteBcd = data[offset + 4] & 0xFF; + int minute = (minuteBcd >> 4) * 10 + (minuteBcd & 0x0F); + + int secondBcd = data[offset + 5] & 0xFF; + int second = (secondBcd >> 4) * 10 + (secondBcd & 0x0F); + + return LocalDateTime.of(year, month, day, hour, minute, second); + } catch (Exception e) { + log.warn("[CSDJ] 解析时间失败", e); + return null; + } + } + + /** + * LocalDateTime 转日本时区时间戳(毫秒) + */ + private long convertToJapanTimestamp(LocalDateTime time) { + ZonedDateTime zonedDateTime = ZonedDateTime.of(time, JAPAN_ZONE); + return zonedDateTime.toInstant().toEpochMilli(); + } + + /** + * 十六进制字符串转字节数组 + */ + public static byte[] hexStringToBytes(String hexStr) { + // 去掉空格和换行 + String cleanHex = hexStr.replaceAll("\\s+", "").toUpperCase(); + + if (cleanHex.length() % 2 != 0) { + throw new IllegalArgumentException("十六进制字符串长度必须为偶数"); + } + + int len = cleanHex.length(); + byte[] data = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + int high = Character.digit(cleanHex.charAt(i), 16); + int low = Character.digit(cleanHex.charAt(i + 1), 16); + if (high < 0 || low < 0) { + throw new IllegalArgumentException("无效的十六进制字符"); + } + data[i / 2] = (byte) ((high << 4) | low); + } + + return data; + } + + /** + * 字节数组转十六进制字符串 + */ + public static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().trim(); + } +}