feat(iot): 对接 3D11 单目客流计数器
在 IoT Gateway 的 Vert.x Router 上注册 /api/camera/* 专用路由, 桥接 3D11 摄像头的心跳和数据上报到现有消息总线和编解码体系。 - 新建 Camera3D11 DTO(心跳请求、数据上报请求、统一响应) - 新建 IotCamera3D11Codec 编解码器(TYPE=CAMERA_3D11) - 新建 IotCameraUpstreamHandler 处理心跳和数据上报 - productKey 通过 application.yaml 配置,未配置时不注册路由 - 心跳上报间隔设为 1 分钟 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
package com.viewsh.module.iot.gateway.codec.camera3d11;
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import com.viewsh.framework.common.util.json.JsonUtils;
|
||||||
|
import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||||
|
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import com.viewsh.module.iot.gateway.codec.IotDeviceMessageCodec;
|
||||||
|
import com.viewsh.module.iot.gateway.codec.camera3d11.dto.Camera3D11DataUploadReqVO;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D11 单目客流计数器 编解码器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class IotCamera3D11Codec implements IotDeviceMessageCodec {
|
||||||
|
|
||||||
|
public static final String TYPE = "CAMERA_3D11";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IotDeviceMessage decode(byte[] bytes) {
|
||||||
|
// 1. 解析 JSON
|
||||||
|
Camera3D11DataUploadReqVO payload;
|
||||||
|
try {
|
||||||
|
payload = JsonUtils.parseObject(bytes, Camera3D11DataUploadReqVO.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[decode][解析 3D11 数据失败]", e);
|
||||||
|
throw new IllegalArgumentException("JSON 格式错误");
|
||||||
|
}
|
||||||
|
Assert.notNull(payload, "消息内容不能为空");
|
||||||
|
|
||||||
|
// 2. 构建属性参数
|
||||||
|
Map<String, Object> params = MapUtil.newHashMap();
|
||||||
|
params.put("people_in", payload.getPeopleIn());
|
||||||
|
params.put("people_out", payload.getPeopleOut());
|
||||||
|
params.put("stat_start_time", payload.getStartTime());
|
||||||
|
params.put("stat_end_time", payload.getEndTime());
|
||||||
|
|
||||||
|
// 3. 使用 endTime 作为上报时间
|
||||||
|
LocalDateTime reportTime = LocalDateTime.now();
|
||||||
|
if (payload.getEndTime() != null) {
|
||||||
|
reportTime = LocalDateTime.ofInstant(
|
||||||
|
Instant.ofEpochSecond(payload.getEndTime()), ZoneId.systemDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(), params)
|
||||||
|
.setReportTime(reportTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encode(IotDeviceMessage message) {
|
||||||
|
// 3D11 不需要下行指令
|
||||||
|
return new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.viewsh.module.iot.gateway.codec.camera3d11.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D11 单目客流计数器 — 数据上报请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class Camera3D11DataUploadReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备序列号
|
||||||
|
*/
|
||||||
|
private String sn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计开始时间(Unix 秒)
|
||||||
|
*/
|
||||||
|
private Long startTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计结束时间(Unix 秒)
|
||||||
|
*/
|
||||||
|
private Long endTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传时间(Unix 秒)
|
||||||
|
*/
|
||||||
|
private Long time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进客流
|
||||||
|
*/
|
||||||
|
@JsonProperty("in")
|
||||||
|
private Integer peopleIn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出客流
|
||||||
|
*/
|
||||||
|
@JsonProperty("out")
|
||||||
|
private Integer peopleOut;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.viewsh.module.iot.gateway.codec.camera3d11.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D11 单目客流计数器 — 心跳请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class Camera3D11HeartBeatReqVO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备序列号
|
||||||
|
*/
|
||||||
|
private String sn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unix 时间戳(秒)
|
||||||
|
*/
|
||||||
|
private Long time;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.viewsh.module.iot.gateway.codec.camera3d11.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D11 单目客流计数器 — 统一响应
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class Camera3D11Resp {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应码:0=成功 1=SN不存在 2=失败
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
private Object data;
|
||||||
|
|
||||||
|
public static Camera3D11Resp success(Object data) {
|
||||||
|
return new Camera3D11Resp(0, "success", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Camera3D11Resp snNotFound() {
|
||||||
|
return new Camera3D11Resp(1, "sn不存在", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Camera3D11Resp error(String msg) {
|
||||||
|
return new Camera3D11Resp(2, msg, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -118,6 +118,13 @@ public class IotGatewayProperties {
|
|||||||
*/
|
*/
|
||||||
private String sslCertPath;
|
private String sslCertPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D11 单目客流计数器的产品 Key
|
||||||
|
*
|
||||||
|
* 设备通过 /api/camera/* 路径上报,需要配置此值用于设备查找
|
||||||
|
*/
|
||||||
|
private String camera3d11ProductKey;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.viewsh.module.iot.gateway.protocol.http;
|
package com.viewsh.module.iot.gateway.protocol.http;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.viewsh.module.iot.core.util.IotDeviceMessageUtils;
|
import com.viewsh.module.iot.core.util.IotDeviceMessageUtils;
|
||||||
import com.viewsh.module.iot.gateway.config.IotGatewayProperties;
|
import com.viewsh.module.iot.gateway.config.IotGatewayProperties;
|
||||||
|
import com.viewsh.module.iot.gateway.protocol.http.router.IotCameraUpstreamHandler;
|
||||||
import com.viewsh.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
|
import com.viewsh.module.iot.gateway.protocol.http.router.IotHttpAuthHandler;
|
||||||
import com.viewsh.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
|
import com.viewsh.module.iot.gateway.protocol.http.router.IotHttpUpstreamHandler;
|
||||||
import io.vertx.core.AbstractVerticle;
|
import io.vertx.core.AbstractVerticle;
|
||||||
@@ -49,6 +51,13 @@ public class IotHttpUpstreamProtocol extends AbstractVerticle {
|
|||||||
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
|
router.post(IotHttpAuthHandler.PATH).handler(authHandler);
|
||||||
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
|
IotHttpUpstreamHandler upstreamHandler = new IotHttpUpstreamHandler(this);
|
||||||
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
|
router.post(IotHttpUpstreamHandler.PATH).handler(upstreamHandler);
|
||||||
|
// 3D11 单目客流计数器专用路由(仅在配置了 productKey 时启用)
|
||||||
|
String camera3d11ProductKey = httpProperties.getCamera3d11ProductKey();
|
||||||
|
if (StrUtil.isNotBlank(camera3d11ProductKey)) {
|
||||||
|
IotCameraUpstreamHandler cameraHandler = new IotCameraUpstreamHandler(this, camera3d11ProductKey);
|
||||||
|
router.post(IotCameraUpstreamHandler.PATH_HEARTBEAT).handler(cameraHandler);
|
||||||
|
router.post(IotCameraUpstreamHandler.PATH_DATA_UPLOAD).handler(cameraHandler);
|
||||||
|
}
|
||||||
|
|
||||||
// 启动 HTTP 服务器
|
// 启动 HTTP 服务器
|
||||||
HttpServerOptions options = new HttpServerOptions()
|
HttpServerOptions options = new HttpServerOptions()
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.viewsh.module.iot.gateway.protocol.http.router;
|
||||||
|
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import cn.hutool.extra.spring.SpringUtil;
|
||||||
|
import com.viewsh.framework.common.util.json.JsonUtils;
|
||||||
|
import com.viewsh.module.iot.core.biz.dto.IotDeviceRespDTO;
|
||||||
|
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||||
|
import com.viewsh.module.iot.gateway.codec.camera3d11.IotCamera3D11Codec;
|
||||||
|
import com.viewsh.module.iot.gateway.codec.camera3d11.dto.Camera3D11DataUploadReqVO;
|
||||||
|
import com.viewsh.module.iot.gateway.codec.camera3d11.dto.Camera3D11HeartBeatReqVO;
|
||||||
|
import com.viewsh.module.iot.gateway.codec.camera3d11.dto.Camera3D11Resp;
|
||||||
|
import com.viewsh.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol;
|
||||||
|
import com.viewsh.module.iot.gateway.service.device.IotDeviceService;
|
||||||
|
import com.viewsh.module.iot.gateway.service.device.message.IotDeviceMessageService;
|
||||||
|
import io.vertx.core.Handler;
|
||||||
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 3D11 单目客流计数器 — HTTP 上行处理器
|
||||||
|
* <p>
|
||||||
|
* 直接实现 {@link Handler},不继承 {@link IotHttpAbstractHandler},
|
||||||
|
* 因其认证逻辑依赖路径参数 (productKey/deviceName),而 3D11 使用固定路径 + body 中 sn 识别设备。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class IotCameraUpstreamHandler implements Handler<RoutingContext> {
|
||||||
|
|
||||||
|
public static final String PATH_HEARTBEAT = "/api/camera/heartBeat";
|
||||||
|
public static final String PATH_DATA_UPLOAD = "/api/camera/dataUpload";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 心跳响应默认上报间隔(分钟)
|
||||||
|
*/
|
||||||
|
private static final int DEFAULT_UPLOAD_INTERVAL = 1;
|
||||||
|
|
||||||
|
private static final ZoneId ZONE_SHANGHAI = ZoneId.of("Asia/Shanghai");
|
||||||
|
|
||||||
|
private final IotHttpUpstreamProtocol protocol;
|
||||||
|
private final String productKey;
|
||||||
|
private final IotDeviceService deviceService;
|
||||||
|
private final IotDeviceMessageService deviceMessageService;
|
||||||
|
|
||||||
|
public IotCameraUpstreamHandler(IotHttpUpstreamProtocol protocol, String productKey) {
|
||||||
|
this.protocol = protocol;
|
||||||
|
this.productKey = productKey;
|
||||||
|
this.deviceService = SpringUtil.getBean(IotDeviceService.class);
|
||||||
|
this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(RoutingContext context) {
|
||||||
|
try {
|
||||||
|
String path = context.request().path();
|
||||||
|
if (PATH_HEARTBEAT.equals(path)) {
|
||||||
|
handleHeartBeat(context);
|
||||||
|
} else if (PATH_DATA_UPLOAD.equals(path)) {
|
||||||
|
handleDataUpload(context);
|
||||||
|
} else {
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.error("未知路径"));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[handle][3D11 处理异常, path={}]", context.request().path(), e);
|
||||||
|
if (!context.response().ended()) {
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.error("服务器内部错误"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 心跳处理
|
||||||
|
*/
|
||||||
|
private void handleHeartBeat(RoutingContext context) {
|
||||||
|
// 1. 解析请求
|
||||||
|
Camera3D11HeartBeatReqVO req = JsonUtils.parseObject(
|
||||||
|
context.body().buffer().getBytes(), Camera3D11HeartBeatReqVO.class);
|
||||||
|
if (req == null || req.getSn() == null) {
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.error("参数错误"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验设备存在
|
||||||
|
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, req.getSn());
|
||||||
|
if (device == null) {
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.snNotFound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 发送上线消息
|
||||||
|
IotDeviceMessage onlineMsg = IotDeviceMessage.buildStateUpdateOnline();
|
||||||
|
deviceMessageService.sendDeviceMessage(onlineMsg, productKey, req.getSn(), protocol.getServerId());
|
||||||
|
|
||||||
|
// 4. 构建心跳响应
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
long dataStartTime = today.atTime(LocalTime.of(8, 0)).atZone(ZONE_SHANGHAI).toEpochSecond();
|
||||||
|
long dataEndTime = today.atTime(LocalTime.of(23, 0)).atZone(ZONE_SHANGHAI).toEpochSecond();
|
||||||
|
long currentTime = LocalDateTime.now().atZone(ZONE_SHANGHAI).toEpochSecond();
|
||||||
|
|
||||||
|
Map<String, Object> data = MapUtil.newHashMap();
|
||||||
|
data.put("sn", req.getSn());
|
||||||
|
data.put("dataStartTime", dataStartTime);
|
||||||
|
data.put("dataEndTime", dataEndTime);
|
||||||
|
data.put("uploadInterval", DEFAULT_UPLOAD_INTERVAL);
|
||||||
|
data.put("time", currentTime);
|
||||||
|
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.success(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据上报处理
|
||||||
|
*/
|
||||||
|
private void handleDataUpload(RoutingContext context) {
|
||||||
|
// 1. 解析请求
|
||||||
|
byte[] bytes = context.body().buffer().getBytes();
|
||||||
|
Camera3D11DataUploadReqVO req = JsonUtils.parseObject(bytes, Camera3D11DataUploadReqVO.class);
|
||||||
|
if (req == null || req.getSn() == null) {
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.error("参数错误"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 校验设备存在
|
||||||
|
IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, req.getSn());
|
||||||
|
if (device == null) {
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.snNotFound());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 解码并发送消息
|
||||||
|
IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(bytes, IotCamera3D11Codec.TYPE);
|
||||||
|
if (message == null) {
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.error("消息解码失败"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deviceMessageService.sendDeviceMessage(message, productKey, req.getSn(), null);
|
||||||
|
|
||||||
|
// 4. 构建响应
|
||||||
|
long currentTime = LocalDateTime.now().atZone(ZONE_SHANGHAI).toEpochSecond();
|
||||||
|
Map<String, Object> data = MapUtil.newHashMap();
|
||||||
|
data.put("sn", req.getSn());
|
||||||
|
data.put("time", currentTime);
|
||||||
|
|
||||||
|
IotHttpAbstractHandler.writeResponse(context, Camera3D11Resp.success(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ viewsh:
|
|||||||
http:
|
http:
|
||||||
enabled: true
|
enabled: true
|
||||||
server-port: 8092
|
server-port: 8092
|
||||||
|
camera3d11-product-key: camera_3d11 # 3D11 单目客流计数器的产品 Key
|
||||||
# ====================================
|
# ====================================
|
||||||
# 针对引入的 EMQX 组件的配置
|
# 针对引入的 EMQX 组件的配置
|
||||||
# ====================================
|
# ====================================
|
||||||
|
|||||||
Reference in New Issue
Block a user