8 changed files with 741 additions and 1 deletions
@ -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()) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
package com.techsor.datacenter.sender.entitiy; |
package com.techsor.datacenter.sender.entitiy.csdj; |
||||
|
|
||||
import lombok.Data; |
import lombok.Data; |
||||
|
|
||||
@ -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<CsdjDigitalInputTerminal> digitalInputTerminals; |
||||
|
} |
||||
@ -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<CsdjDigitalInputTerminal> 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> 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); |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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<CsdjDigitalInputTerminal> 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(); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue