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; |
|||
|
|||
@ -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