feat(device): 新增星汉设备控制功能

- 添加星汉设备控制器 AppDeviceXinghanController- 实现星汉设备业务逻辑 DeviceXinghanBizService
- 增加开机 LOGO 下发规则 XinghanBootLogoRule
- 添加设备发送消息规则 XinghanSendMsgRule
- 更新 MQTT 命令类型常量 XingHanCommandTypeConstants
- 修改设备状态 JSON 结构 MqttXinghanJson
This commit is contained in:
2025-08-22 18:09:08 +08:00
parent 4077fd303f
commit 95aa01e1c2
7 changed files with 645 additions and 7 deletions

View File

@ -13,8 +13,17 @@ public final class MqttXinghanCommandType {
private MqttXinghanCommandType() {}
public enum XinghanCommandTypeEnum {
/**
* 星汉设备主动上报数据
*/
GRADE_INFO(101),
/**
* 星汉开机LOGO
*/
PIC_TRANS(102),
/**
* 星汉设备发送消息 (XingHan send msg)
*/
TEX_TRANS(103),
BREAK_NEWS(104),
UNKNOWN(0);

View File

@ -20,37 +20,37 @@ public class MqttXinghanJson {
* 第三键值对SOS档位2,1,0, 分别表示红蓝模式/爆闪模式/关闭
*/
@JsonProperty("sta_SOSGrade")
public int staSOSGrade;
public Integer staSOSGrade;
/**
* 第四键值对剩余照明时间0-5999单位分钟。
*/
@JsonProperty("sta_PowerTime")
public int staPowerTime;
public Integer staPowerTime;
/**
* 第五键值对剩余电量百分比0-100
*/
@JsonProperty("sta_PowerPercent")
public int staPowerPercent;
public Integer staPowerPercent;
/**
* 第六键值对, 近电预警级别, 0-无预警1-弱预警2-中预警3-强预警4-非常强预警。
*/
@JsonProperty("sta_DetectResult")
public int staDetectResult;
public Integer staDetectResult;
/**
* 第七键值对, 静止报警状态0-未静止报警1-正在静止报警。
*/
@JsonProperty("staShakeBit")
public int sta_ShakeBit;
public Integer sta_ShakeBit;
/**
* 第八键值对, 4G信号强度0-32数值越大信号越强。
*/
@JsonProperty("sta_4gSinal")
public int sta4gSinal;
public Integer sta4gSinal;
/**
* 第九键值对IMIE卡号
*/
@JsonProperty("sta_imei")
public int staimei;
public String staimei;
/**
* 第十键值对,经度
*/

View File

@ -13,4 +13,8 @@ public class XingHanCommandTypeConstants {
* 星汉设备发送消息 (XingHan send msg)
*/
public static final String XingHan_ESEND_MSG = "Light_103";
/**
* 星汉设备发送紧急通知 (XingHan break news)
*/
public static final String XingHan_BREAK_NEWS = "Light_104";
}

View File

@ -0,0 +1,143 @@
package com.fuyuanshen.global.mqtt.rule.xinghan;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fuyuanshen.common.core.utils.ImageToCArrayConverter;
import com.fuyuanshen.common.core.utils.StringUtils;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
import com.fuyuanshen.global.mqtt.config.MqttGateway;
import com.fuyuanshen.global.mqtt.constants.LightingCommandTypeConstants;
import com.fuyuanshen.global.mqtt.constants.MqttConstants;
import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants;
import com.fuyuanshen.global.mqtt.listener.domain.FunctionAccessStatus;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.CRC32;
import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY;
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
import static com.fuyuanshen.common.core.utils.ImageToCArrayConverter.convertHexToDecimal;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_BOOT_LOGO_KEY_PREFIX;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 星汉设备开机 LOGO 下发规则:
* <p>
* 1. 设备上行 sta_PicTarns=great! => 仅标记成功<br>
* 2. 设备上行 sta_PicTarns=数字 => 下发第 N 块数据256B/块,带 CRC32
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class XinghanBootLogoRule implements MqttMessageRule {
private final MqttGateway mqttGateway;
private final ObjectMapper objectMapper;
@Override
public String getCommandType() {
return XingHanCommandTypeConstants.XingHan_BOOT_LOGO;
}
@Override
public void execute(MqttRuleContext ctx) {
final String functionAccessKey = FUNCTION_ACCESS_KEY + ctx.getDeviceImei();
try {
MqttXinghanLogoJson payload = objectMapper.convertValue(
ctx.getPayloadDict(), MqttXinghanLogoJson.class);
String respText = payload.getStaPicTrans();
log.warn("设备上报LOGO{}", respText);
// 1. great! —— 成功标记
if ("great!".equalsIgnoreCase(respText)) {
RedisUtils.setCacheObject(functionAccessKey,
FunctionAccessStatus.OK.getCode(), Duration.ofSeconds(20));
log.info("设备 {} 开机 LOGO 写入成功", ctx.getDeviceImei());
return;
}
// 2. 数字 —— 下发数据块
int blockIndex;
try {
blockIndex = Integer.parseInt(respText);
} catch (NumberFormatException ex) {
log.warn("设备 {} LOGO 上报非法块号:{}", ctx.getDeviceImei(), respText);
return;
}
String hexImage = RedisUtils.getCacheObject(
GLOBAL_REDIS_KEY + DEVICE_KEY_PREFIX + ctx.getDeviceImei() + DEVICE_BOOT_LOGO_KEY_PREFIX);
if (StringUtils.isEmpty(hexImage)) {
return;
}
byte[] fullBin = ImageToCArrayConverter.convertStringToByteArray(hexImage);
byte[] chunk = ImageToCArrayConverter.getChunk(fullBin, blockIndex - 1, CHUNK_SIZE);
log.info("设备 {} 第 {} 块数据长度: {} bytes", ctx.getDeviceImei(), blockIndex, chunk.length);
// 组装下发数据
ArrayList<Integer> dataFrame = new ArrayList<>();
dataFrame.add(blockIndex); // 块号
ImageToCArrayConverter.buildArr(convertHexToDecimal(chunk), dataFrame);
dataFrame.addAll(crc32AsList(chunk)); // CRC32
Map<String, Object> pub = new HashMap<>();
pub.put("ins_PicTrans", dataFrame);
String topic = MqttConstants.GLOBAL_PUB_KEY + ctx.getDeviceImei();
String json = JsonUtils.toJsonString(pub);
mqttGateway.sendMsgToMqtt(topic, 1, json);
log.info("下发开机 LOGO 数据 => topic:{}, payload:{}", topic, json);
} catch (Exception e) {
log.error("处理设备 {} 开机 LOGO 失败", ctx.getDeviceImei(), e);
RedisUtils.setCacheObject(functionAccessKey,
FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(20));
}
}
/* ---------- 内部工具 ---------- */
private static final int CHUNK_SIZE = 256;
private static ArrayList<Integer> crc32AsList(byte[] data) {
CRC32 crc = new CRC32();
crc.update(data);
byte[] crcBytes = ByteBuffer.allocate(4)
.order(ByteOrder.BIG_ENDIAN)
.putInt((int) crc.getValue())
.array();
ArrayList<Integer> list = new ArrayList<>(4);
for (byte b : crcBytes) {
list.add(Byte.toUnsignedInt(b));
}
return list;
}
/* ---------- DTO ---------- */
@Data
private static class MqttXinghanLogoJson {
/**
* 设备上行:
* 数字 -> 请求对应块号
* great! -> 写入成功
*/
@JsonProperty("sta_PicTrans")
private String staPicTrans;
}
}

View File

@ -0,0 +1,117 @@
package com.fuyuanshen.global.mqtt.rule.xinghan;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fuyuanshen.common.json.utils.JsonUtils;
import com.fuyuanshen.common.redis.utils.RedisUtils;
import com.fuyuanshen.global.mqtt.base.MqttMessageRule;
import com.fuyuanshen.global.mqtt.base.MqttRuleContext;
import com.fuyuanshen.global.mqtt.config.MqttGateway;
import com.fuyuanshen.global.mqtt.constants.MqttConstants;
import com.fuyuanshen.global.mqtt.constants.XingHanCommandTypeConstants;
import com.fuyuanshen.global.mqtt.listener.domain.FunctionAccessStatus;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.nio.charset.Charset;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.fuyuanshen.common.core.constant.GlobalConstants.FUNCTION_ACCESS_KEY;
import static com.fuyuanshen.common.core.constant.GlobalConstants.GLOBAL_REDIS_KEY;
import static com.fuyuanshen.global.mqtt.constants.DeviceRedisKeyConstants.DEVICE_KEY_PREFIX;
/**
* 星汉设备发送消息 下发规则:
* <p>
* 1. 设备上行 sta_TexTarns=genius! => 仅标记成功<br>
* 2. 设备上行 sta_TexTarns=数字 => GBK编码每行文字为一包一共4包第一字节为包序号
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class XinghanSendMsgRule implements MqttMessageRule {
private final MqttGateway mqttGateway;
private final ObjectMapper objectMapper;
@Override
public String getCommandType() {
return XingHanCommandTypeConstants.XingHan_ESEND_MSG;
}
@Override
public void execute(MqttRuleContext ctx) {
String functionAccess = FUNCTION_ACCESS_KEY + ctx.getDeviceImei();
try {
XinghanSendMsgRule.MqttXinghanMsgJson payload = objectMapper.convertValue(
ctx.getPayloadDict(), XinghanSendMsgRule.MqttXinghanMsgJson.class);
String respText = payload.getStaTexTrans();
log.info("设备上报人员信息: {} ", respText);
// 1. genius! —— 成功标记
if ("genius!".equalsIgnoreCase(respText)) {
RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(20));
log.info("设备 {} 发送消息完成", ctx.getDeviceImei());
return;
}
// 2. 数字 —— 下发数据块
int blockIndex;
try {
blockIndex = Integer.parseInt(respText);
} catch (NumberFormatException ex) {
log.warn("设备 {} 消息上报非法块号:{}", ctx.getDeviceImei(), respText);
return;
}
// 将发送的信息原文本以List<String>形式存储在Redis中
List<String> data = RedisUtils.getCacheList(GLOBAL_REDIS_KEY+ DEVICE_KEY_PREFIX + ctx.getDeviceImei() + ":app_send_message_data");
if (data.isEmpty()) {
return;
}
//
ArrayList<Integer> intData = new ArrayList<>();
intData.add(blockIndex);
// 获取块原内容 转成GBK 再转成无符号十进制整数
String blockTxt = data.get(blockIndex-1);
// 再按 GBK 编码把字符串转成字节数组,并逐个转为无符号十进制整数
for (byte b : blockTxt.getBytes(GBK)) {
intData.add(b & 0xFF); // b & 0xFF 得到 0~255 的整数
}
Map<String, Object> map = new HashMap<>();
map.put("ins_TexTrans", intData);
String topic = MqttConstants.GLOBAL_PUB_KEY + ctx.getDeviceImei();
String json = JsonUtils.toJsonString(map);
mqttGateway.sendMsgToMqtt(topic, 1, json);
log.info("发送设备信息数据到设备消息=>topic:{},payload:{}",
MqttConstants.GLOBAL_PUB_KEY + ctx.getDeviceImei(),
JsonUtils.toJsonString(map));
} catch (Exception e) {
log.error("处理发送设备信息时出错", e);
RedisUtils.setCacheObject(functionAccess, FunctionAccessStatus.FAILED.getCode(), Duration.ofSeconds(20));
}
}
private static final Charset GBK = Charset.forName("GBK");
/* ---------- DTO ---------- */
@Data
private static class MqttXinghanMsgJson {
/**
* 设备上行:
* 数字 -> 请求对应块号
* genius! -> 写入成功
*/
@JsonProperty("sta_TexTrans")
private String staTexTrans;
}
}