Commit a2750693ebbb6de7506b9d5e0de39a7c2d3d1bac
1 parent
e765099a
feat(iot):【网关设备:50%】整体初步实现(未测试),基于 gateway-device-topic-design.md 规划
Showing
13 changed files
with
903 additions
and
10 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/ErrorCodeConstants.java
| @@ -35,6 +35,16 @@ public interface ErrorCodeConstants { | @@ -35,6 +35,16 @@ public interface ErrorCodeConstants { | ||
| 35 | ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); | 35 | ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一"); |
| 36 | ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关"); | 36 | ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关"); |
| 37 | ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑"); | 37 | ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑"); |
| 38 | + // 拓扑管理相关错误码 1-050-003-100 | ||
| 39 | + ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效"); | ||
| 40 | + ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效"); | ||
| 41 | + ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败"); | ||
| 42 | + ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关"); | ||
| 43 | + // TODO @AI:这里的错误码校验,要不要使用? | ||
| 44 | + ErrorCode DEVICE_TOPO_SUB_DEVICE_NOT_BOUND = new ErrorCode(1_050_003_104, "子设备【{}/{}】未绑定到任何网关"); | ||
| 45 | + // 子设备注册相关错误码 1-050-003-200 | ||
| 46 | + ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效"); | ||
| 47 | + ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型"); | ||
| 38 | 48 | ||
| 39 | // ========== 产品分类 1-050-004-000 ========== | 49 | // ========== 产品分类 1-050-004-000 ========== |
| 40 | ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); | 50 | ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在"); |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/IotDeviceServiceImpl.java
| @@ -468,6 +468,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { | @@ -468,6 +468,13 @@ public class IotDeviceServiceImpl implements IotDeviceService { | ||
| 468 | log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); | 468 | log.error("[authDevice][设备({}/{}) 密码不正确]", productKey, deviceName); |
| 469 | return false; | 469 | return false; |
| 470 | } | 470 | } |
| 471 | + | ||
| 472 | + // 3. 校验子设备拓扑关系:子设备必须先绑定到某网关才能认证上线 | ||
| 473 | + if (IotProductDeviceTypeEnum.isGatewaySub(device.getDeviceType()) | ||
| 474 | + && device.getGatewayId() != null) { | ||
| 475 | + log.warn("[authDevice][子设备({}/{}) 未绑定到任何网关,认证失败]", productKey, deviceName); | ||
| 476 | + return false; | ||
| 477 | + } | ||
| 471 | return true; | 478 | return true; |
| 472 | } | 479 | } |
| 473 | 480 |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java
| @@ -10,17 +10,30 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; | @@ -10,17 +10,30 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; | ||
| 10 | import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; | 10 | import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; |
| 11 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | 11 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 12 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 12 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 13 | +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.device.IotDeviceSaveReqVO; | ||
| 13 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; | 14 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; |
| 14 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; | 15 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageReqVO; |
| 15 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; | 16 | import cn.iocoder.yudao.module.iot.controller.admin.statistics.vo.IotStatisticsDeviceMessageSummaryByDateRespVO; |
| 17 | +import cn.iocoder.yudao.module.iot.core.biz.dto.*; | ||
| 16 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | 18 | import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; |
| 19 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; | ||
| 17 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 20 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 21 | +import cn.iocoder.yudao.module.iot.core.topic.property.IotDevicePropertyPackPostReqDTO; | ||
| 22 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterReqDTO; | ||
| 23 | +import cn.iocoder.yudao.module.iot.core.topic.auth.IotSubDeviceRegisterRespDTO; | ||
| 24 | +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoAddReqDTO; | ||
| 25 | +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoDeleteReqDTO; | ||
| 26 | +import cn.iocoder.yudao.module.iot.core.topic.topo.IotDeviceTopoRespDTO; | ||
| 27 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; | ||
| 18 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | 28 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; |
| 19 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 29 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 20 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; | 30 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| 21 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; | 31 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; |
| 32 | +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; | ||
| 22 | import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper; | 33 | import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper; |
| 34 | +import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum; | ||
| 23 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | 35 | import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 36 | +import cn.iocoder.yudao.module.iot.service.product.IotProductService; | ||
| 24 | import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; | 37 | import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; |
| 25 | import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; | 38 | import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; |
| 26 | import com.baomidou.mybatisplus.core.metadata.IPage; | 39 | import com.baomidou.mybatisplus.core.metadata.IPage; |
| @@ -35,12 +48,11 @@ import org.springframework.validation.annotation.Validated; | @@ -35,12 +48,11 @@ import org.springframework.validation.annotation.Validated; | ||
| 35 | 48 | ||
| 36 | import java.sql.Timestamp; | 49 | import java.sql.Timestamp; |
| 37 | import java.time.LocalDateTime; | 50 | import java.time.LocalDateTime; |
| 38 | -import java.util.List; | ||
| 39 | -import java.util.Map; | 51 | +import java.util.*; |
| 40 | 52 | ||
| 41 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 53 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 42 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | 54 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 43 | -import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL; | 55 | +import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*; |
| 44 | 56 | ||
| 45 | /** | 57 | /** |
| 46 | * IoT 设备消息 Service 实现类 | 58 | * IoT 设备消息 Service 实现类 |
| @@ -59,6 +71,9 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -59,6 +71,9 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 59 | @Resource | 71 | @Resource |
| 60 | @Lazy // 延迟加载,避免循环依赖 | 72 | @Lazy // 延迟加载,避免循环依赖 |
| 61 | private IotOtaTaskRecordService otaTaskRecordService; | 73 | private IotOtaTaskRecordService otaTaskRecordService; |
| 74 | + @Resource | ||
| 75 | + @Lazy // 延迟加载,避免循环依赖 | ||
| 76 | + private IotProductService productService; | ||
| 62 | 77 | ||
| 63 | @Resource | 78 | @Resource |
| 64 | private IotDeviceMessageMapper deviceMessageMapper; | 79 | private IotDeviceMessageMapper deviceMessageMapper; |
| @@ -168,9 +183,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -168,9 +183,10 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 168 | // 2. 记录消息 | 183 | // 2. 记录消息 |
| 169 | getSelf().createDeviceLogAsync(message); | 184 | getSelf().createDeviceLogAsync(message); |
| 170 | 185 | ||
| 171 | - // 3. 回复消息。前提:非 _reply 消息,并且非禁用回复的消息 | 186 | + // TODO @AI:我在想,是不是批量上传后,还是得 reply 。因为打包上传的时候,只是那条消息的回复。然后,需要单独给每个子消息回复,后续至于怎么使用,是不是得看具体业务了;例如说:1)批量上传属性,默认回复是批量上传的消息;然后,每个属性、事件拆包消息,单独回复,后续网关设备按需回复给子设备。 |
| 187 | + // 3. 回复消息。前提:非 _reply 消息、非禁用回复的消息、非拆包消息 | ||
| 172 | if (IotDeviceMessageUtils.isReplyMessage(message) | 188 | if (IotDeviceMessageUtils.isReplyMessage(message) |
| 173 | - || IotDeviceMessageMethodEnum.isReplyDisabled(message.getMethod()) | 189 | + || !message.needReply() |
| 174 | || StrUtil.isEmpty(message.getServerId())) { | 190 | || StrUtil.isEmpty(message.getServerId())) { |
| 175 | return; | 191 | return; |
| 176 | } | 192 | } |
| @@ -185,15 +201,19 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -185,15 +201,19 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 185 | } | 201 | } |
| 186 | 202 | ||
| 187 | // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器 | 203 | // TODO @芋艿:可优化:未来逻辑复杂后,可以独立拆除 Processor 处理器 |
| 188 | - @SuppressWarnings("SameReturnValue") | ||
| 189 | private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) { | 204 | private Object handleUpstreamDeviceMessage0(IotDeviceMessage message, IotDeviceDO device) { |
| 190 | // 设备上下线 | 205 | // 设备上下线 |
| 191 | if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { | 206 | if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod())) { |
| 192 | String stateStr = IotDeviceMessageUtils.getIdentifier(message); | 207 | String stateStr = IotDeviceMessageUtils.getIdentifier(message); |
| 193 | assert stateStr != null; | 208 | assert stateStr != null; |
| 194 | Assert.notEmpty(stateStr, "设备状态不能为空"); | 209 | Assert.notEmpty(stateStr, "设备状态不能为空"); |
| 195 | - deviceService.updateDeviceState(device, Integer.valueOf(stateStr)); | ||
| 196 | - // TODO 芋艿:子设备的关联 | 210 | + Integer state = Integer.valueOf(stateStr); |
| 211 | + deviceService.updateDeviceState(device, state); | ||
| 212 | + // 特殊:网关设备下线时,网关子设备联动下线 | ||
| 213 | + if (Objects.equal(state, IotDeviceStateEnum.OFFLINE.getState()) | ||
| 214 | + && IotProductDeviceTypeEnum.isGateway(device.getDeviceType())) { | ||
| 215 | + handleGatewayOffline(device, message.getServerId()); | ||
| 216 | + } | ||
| 197 | return null; | 217 | return null; |
| 198 | } | 218 | } |
| 199 | 219 | ||
| @@ -202,6 +222,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -202,6 +222,11 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 202 | devicePropertyService.saveDeviceProperty(device, message); | 222 | devicePropertyService.saveDeviceProperty(device, message); |
| 203 | return null; | 223 | return null; |
| 204 | } | 224 | } |
| 225 | + // 批量上报(属性+事件+子设备) | ||
| 226 | + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.PROPERTY_PACK_POST.getMethod())) { | ||
| 227 | + handlePackMessage(message, device); | ||
| 228 | + return null; | ||
| 229 | + } | ||
| 205 | 230 | ||
| 206 | // OTA 上报升级进度 | 231 | // OTA 上报升级进度 |
| 207 | if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) { | 232 | if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.OTA_PROGRESS.getMethod())) { |
| @@ -209,10 +234,435 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -209,10 +234,435 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 209 | return null; | 234 | return null; |
| 210 | } | 235 | } |
| 211 | 236 | ||
| 212 | - // TODO @芋艿:这里可以按需,添加别的逻辑; | 237 | + // 添加拓扑关系 |
| 238 | + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_ADD.getMethod())) { | ||
| 239 | + return handleTopoAdd(message, device); | ||
| 240 | + } | ||
| 241 | + // 删除拓扑关系 | ||
| 242 | + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_DELETE.getMethod())) { | ||
| 243 | + return handleTopoDelete(message, device); | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + // 获取拓扑关系 | ||
| 247 | + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.TOPO_GET.getMethod())) { | ||
| 248 | + return handleTopoGet(device); | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + // 子设备动态注册 | ||
| 252 | + if (Objects.equal(message.getMethod(), IotDeviceMessageMethodEnum.SUB_DEVICE_REGISTER.getMethod())) { | ||
| 253 | + return handleSubDeviceRegister(message, device); | ||
| 254 | + } | ||
| 255 | + | ||
| 256 | + return null; | ||
| 257 | + } | ||
| 258 | + | ||
| 259 | + // ========== 拓扑管理处理方法 ========== | ||
| 260 | + | ||
| 261 | + // TODO @AI:是不是更适合在 deviceService 里面处理? | ||
| 262 | + /** | ||
| 263 | + * 处理添加拓扑关系请求 | ||
| 264 | + * | ||
| 265 | + * @param message 消息 | ||
| 266 | + * @param gatewayDevice 网关设备 | ||
| 267 | + * @return 响应数据 | ||
| 268 | + */ | ||
| 269 | + private Object handleTopoAdd(IotDeviceMessage message, IotDeviceDO gatewayDevice) { | ||
| 270 | + // TODO @AI:这里是不是 1.1 1.2 1.3 这样?修改前,我们确认下,有没模块是这么写的;iot 消息处理里; | ||
| 271 | + // 1. 校验网关设备类型 | ||
| 272 | + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { | ||
| 273 | + throw exception(DEVICE_NOT_GATEWAY); | ||
| 274 | + } | ||
| 275 | + | ||
| 276 | + // 2. 解析参数 | ||
| 277 | + // TODO @AI:是不是 parseObject 增加一个方法,允许传入 object 类型,避免先转 jsonString 再 parseObject ; | ||
| 278 | + IotDeviceTopoAddReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()), | ||
| 279 | + IotDeviceTopoAddReqDTO.class); | ||
| 280 | + if (params == null) { | ||
| 281 | + throw exception(DEVICE_TOPO_PARAMS_INVALID); | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + // 3. 解析子设备信息 | ||
| 285 | + IotDeviceAuthUtils.DeviceInfo subDeviceInfo = IotDeviceAuthUtils.parseUsername(params.getUsername()); | ||
| 286 | + if (subDeviceInfo == null) { | ||
| 287 | + throw exception(DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID); | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | + // 4. 校验子设备认证信息 | ||
| 291 | + // TODO @AI:链式调用; | ||
| 292 | + IotDeviceAuthReqDTO authReqDTO = new IotDeviceAuthReqDTO(); | ||
| 293 | + authReqDTO.setClientId(params.getClientId()); | ||
| 294 | + authReqDTO.setUsername(params.getUsername()); | ||
| 295 | + authReqDTO.setPassword(params.getPassword()); | ||
| 296 | + if (!deviceService.authDevice(authReqDTO)) { | ||
| 297 | + throw exception(DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED); | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + // 5. 获取子设备 | ||
| 301 | + IotDeviceDO subDevice = deviceService.getDeviceFromCache(subDeviceInfo.getProductKey(), subDeviceInfo.getDeviceName()); | ||
| 302 | + if (subDevice == null) { | ||
| 303 | + throw exception(DEVICE_NOT_EXISTS); | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | + // 6. 校验子设备类型 | ||
| 307 | + if (!IotProductDeviceTypeEnum.isGatewaySub(subDevice.getDeviceType())) { | ||
| 308 | + throw exception(DEVICE_NOT_GATEWAY_SUB, subDevice.getProductKey(), subDevice.getDeviceName()); | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + // 7. 绑定拓扑关系 | ||
| 312 | + // TODO @AI:这里要考虑,校验是不是老设备已经绑定到其他网关了? | ||
| 313 | + deviceService.bindDeviceGateway(Collections.singletonList(subDevice.getId()), gatewayDevice.getId()); | ||
| 314 | + log.info("[handleTopoAdd][网关({}/{}) 绑定子设备({}/{})]", | ||
| 315 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), | ||
| 316 | + subDevice.getProductKey(), subDevice.getDeviceName()); | ||
| 317 | + | ||
| 318 | + // 8. 发送拓扑变更通知 | ||
| 319 | + sendTopoChangeNotify(gatewayDevice, "add", subDevice); | ||
| 213 | return null; | 320 | return null; |
| 214 | } | 321 | } |
| 215 | 322 | ||
| 323 | + // TODO @AI:是不是更适合在 deviceService 里面处理? | ||
| 324 | + | ||
| 325 | + /** | ||
| 326 | + * 处理删除拓扑关系请求 | ||
| 327 | + * | ||
| 328 | + * @param message 消息 | ||
| 329 | + * @param gatewayDevice 网关设备 | ||
| 330 | + * @return 响应数据 | ||
| 331 | + */ | ||
| 332 | + private Object handleTopoDelete(IotDeviceMessage message, IotDeviceDO gatewayDevice) { | ||
| 333 | + // 1. 校验网关设备类型 | ||
| 334 | + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { | ||
| 335 | + throw exception(DEVICE_NOT_GATEWAY); | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + // 2. 解析参数 | ||
| 339 | + IotDeviceTopoDeleteReqDTO params = JsonUtils.parseObject(JsonUtils.toJsonString(message.getParams()), | ||
| 340 | + IotDeviceTopoDeleteReqDTO.class); | ||
| 341 | + if (params == null) { | ||
| 342 | + throw exception(DEVICE_TOPO_PARAMS_INVALID); | ||
| 343 | + } | ||
| 344 | + | ||
| 345 | + // 3. 获取子设备 | ||
| 346 | + IotDeviceDO subDevice = deviceService.getDeviceFromCache(params.getProductKey(), params.getDeviceName()); | ||
| 347 | + if (subDevice == null) { | ||
| 348 | + throw exception(DEVICE_NOT_EXISTS); | ||
| 349 | + } | ||
| 350 | + | ||
| 351 | + // 4. 校验子设备是否绑定到该网关 | ||
| 352 | + if (!Objects.equal(subDevice.getGatewayId(), gatewayDevice.getId())) { | ||
| 353 | + throw exception(DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY, params.getProductKey(), params.getDeviceName()); | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | + // 5. 解绑拓扑关系 | ||
| 357 | + deviceService.unbindDeviceGateway(Collections.singletonList(subDevice.getId())); | ||
| 358 | + log.info("[handleTopoDelete][网关({}/{}) 解绑子设备({}/{})]", | ||
| 359 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), | ||
| 360 | + subDevice.getProductKey(), subDevice.getDeviceName()); | ||
| 361 | + | ||
| 362 | + // 6. 子设备下线 | ||
| 363 | + if (Objects.equal(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) { | ||
| 364 | + deviceService.updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState()); | ||
| 365 | + } | ||
| 366 | + | ||
| 367 | + // 7. 发送拓扑变更通知 | ||
| 368 | + sendTopoChangeNotify(gatewayDevice, "delete", subDevice); | ||
| 369 | + | ||
| 370 | + return null; | ||
| 371 | + } | ||
| 372 | + | ||
| 373 | + /** | ||
| 374 | + * 处理获取拓扑关系请求 | ||
| 375 | + * | ||
| 376 | + * @param gatewayDevice 网关设备 | ||
| 377 | + * @return 子设备列表 | ||
| 378 | + */ | ||
| 379 | + private Object handleTopoGet(IotDeviceDO gatewayDevice) { | ||
| 380 | + // 1. 校验网关设备类型 | ||
| 381 | + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { | ||
| 382 | + throw exception(DEVICE_NOT_GATEWAY); | ||
| 383 | + } | ||
| 384 | + | ||
| 385 | + // 2. 获取子设备列表 | ||
| 386 | + List<IotDeviceDO> subDevices = deviceService.getDeviceListByGatewayId(gatewayDevice.getId()); | ||
| 387 | + | ||
| 388 | + // 3. 转换为响应格式 | ||
| 389 | + return convertList(subDevices, subDevice -> new IotDeviceTopoRespDTO() | ||
| 390 | + .setProductKey(subDevice.getProductKey()) | ||
| 391 | + .setDeviceName(subDevice.getDeviceName())); | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + /** | ||
| 395 | + * 发送拓扑变更通知 | ||
| 396 | + * | ||
| 397 | + * @param gatewayDevice 网关设备 | ||
| 398 | + * @param changeType 变更类型:add/delete | ||
| 399 | + * @param subDevice 子设备 | ||
| 400 | + */ | ||
| 401 | + private void sendTopoChangeNotify(IotDeviceDO gatewayDevice, String changeType, IotDeviceDO subDevice) { | ||
| 402 | + try { | ||
| 403 | + String serverId = devicePropertyService.getDeviceServerId(gatewayDevice.getId()); | ||
| 404 | + if (StrUtil.isEmpty(serverId)) { | ||
| 405 | + log.warn("[sendTopoChangeNotify][网关({}/{}) serverId 为空,无法发送拓扑变更通知]", | ||
| 406 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName()); | ||
| 407 | + return; | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + Map<String, Object> params = MapUtil.builder(new HashMap<String, Object>()) | ||
| 411 | + .put("changeType", changeType) | ||
| 412 | + .put("subDevice", MapUtil.builder(new HashMap<String, Object>()) | ||
| 413 | + .put("productKey", subDevice.getProductKey()) | ||
| 414 | + .put("deviceName", subDevice.getDeviceName()) | ||
| 415 | + .build()) | ||
| 416 | + .build(); | ||
| 417 | + | ||
| 418 | + IotDeviceMessage notifyMessage = IotDeviceMessage.requestOf( | ||
| 419 | + IotDeviceMessageMethodEnum.TOPO_CHANGE.getMethod(), params); | ||
| 420 | + sendDeviceMessage(notifyMessage, gatewayDevice, serverId); | ||
| 421 | + } catch (Exception ex) { | ||
| 422 | + log.error("[sendTopoChangeNotify][发送拓扑变更通知失败,网关({}/{}), 子设备({}/{})]", | ||
| 423 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), | ||
| 424 | + subDevice.getProductKey(), subDevice.getDeviceName(), ex); | ||
| 425 | + } | ||
| 426 | + } | ||
| 427 | + | ||
| 428 | + // ========== 子设备注册处理方法 ========== | ||
| 429 | + | ||
| 430 | + /** | ||
| 431 | + * 处理子设备动态注册请求 | ||
| 432 | + * | ||
| 433 | + * @param message 消息 | ||
| 434 | + * @param gatewayDevice 网关设备 | ||
| 435 | + * @return 注册结果列表 | ||
| 436 | + */ | ||
| 437 | + private Object handleSubDeviceRegister(IotDeviceMessage message, IotDeviceDO gatewayDevice) { | ||
| 438 | + // 1. 校验网关设备类型 | ||
| 439 | + if (!IotProductDeviceTypeEnum.isGateway(gatewayDevice.getDeviceType())) { | ||
| 440 | + throw exception(DEVICE_NOT_GATEWAY); | ||
| 441 | + } | ||
| 442 | + | ||
| 443 | + // 2. 解析参数(数组) | ||
| 444 | + List<IotSubDeviceRegisterReqDTO> paramsList; | ||
| 445 | + if (message.getParams() instanceof List) { | ||
| 446 | + paramsList = JsonUtils.parseArray(JsonUtils.toJsonString(message.getParams()), | ||
| 447 | + IotSubDeviceRegisterReqDTO.class); | ||
| 448 | + } else { | ||
| 449 | + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + if (paramsList == null || paramsList.isEmpty()) { | ||
| 453 | + throw exception(DEVICE_SUB_REGISTER_PARAMS_INVALID); | ||
| 454 | + } | ||
| 455 | + | ||
| 456 | + // 3. 遍历注册每个子设备 | ||
| 457 | + List<IotSubDeviceRegisterRespDTO> results = new ArrayList<>(); | ||
| 458 | + for (IotSubDeviceRegisterReqDTO params : paramsList) { | ||
| 459 | + try { | ||
| 460 | + IotSubDeviceRegisterRespDTO result = registerSubDevice(params, gatewayDevice); | ||
| 461 | + results.add(result); | ||
| 462 | + } catch (Exception ex) { | ||
| 463 | + log.error("[handleSubDeviceRegister][子设备({}/{}) 注册失败]", | ||
| 464 | + params.getProductKey(), params.getDeviceName(), ex); | ||
| 465 | + // 继续处理其他子设备,不影响整体流程 | ||
| 466 | + } | ||
| 467 | + } | ||
| 468 | + | ||
| 469 | + return results; | ||
| 470 | + } | ||
| 471 | + | ||
| 472 | + /** | ||
| 473 | + * 注册单个子设备 | ||
| 474 | + * | ||
| 475 | + * @param params 注册参数 | ||
| 476 | + * @param gatewayDevice 网关设备 | ||
| 477 | + * @return 注册结果 | ||
| 478 | + */ | ||
| 479 | + private IotSubDeviceRegisterRespDTO registerSubDevice(IotSubDeviceRegisterReqDTO params, | ||
| 480 | + IotDeviceDO gatewayDevice) { | ||
| 481 | + // 1. 查找产品 | ||
| 482 | + IotProductDO product = productService.getProductByProductKey(params.getProductKey()); | ||
| 483 | + if (product == null) { | ||
| 484 | + throw exception(PRODUCT_NOT_EXISTS); | ||
| 485 | + } | ||
| 486 | + | ||
| 487 | + // 2. 校验产品是否为网关子设备类型 | ||
| 488 | + if (!IotProductDeviceTypeEnum.isGatewaySub(product.getDeviceType())) { | ||
| 489 | + throw exception(DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB, params.getProductKey()); | ||
| 490 | + } | ||
| 491 | + | ||
| 492 | + // 3. 查找设备是否已存在 | ||
| 493 | + IotDeviceDO existDevice = deviceService.getDeviceFromCache(params.getProductKey(), params.getDeviceName()); | ||
| 494 | + if (existDevice != null) { | ||
| 495 | + // 已存在则返回设备信息 | ||
| 496 | + return new IotSubDeviceRegisterRespDTO() | ||
| 497 | + .setProductKey(existDevice.getProductKey()) | ||
| 498 | + .setDeviceName(existDevice.getDeviceName()) | ||
| 499 | + .setDeviceSecret(existDevice.getDeviceSecret()); | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + // 4. 创建新设备 | ||
| 503 | + IotDeviceSaveReqVO createReqVO = new IotDeviceSaveReqVO() | ||
| 504 | + .setDeviceName(params.getDeviceName()) | ||
| 505 | + .setProductId(product.getId()) | ||
| 506 | + .setGatewayId(gatewayDevice.getId()); | ||
| 507 | + Long deviceId = deviceService.createDevice(createReqVO); | ||
| 508 | + | ||
| 509 | + // 5. 获取新创建的设备信息 | ||
| 510 | + IotDeviceDO newDevice = deviceService.getDevice(deviceId); | ||
| 511 | + log.info("[registerSubDevice][网关({}/{}) 注册子设备({}/{})]", | ||
| 512 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), | ||
| 513 | + newDevice.getProductKey(), newDevice.getDeviceName()); | ||
| 514 | + | ||
| 515 | + return new IotSubDeviceRegisterRespDTO() | ||
| 516 | + .setProductKey(newDevice.getProductKey()) | ||
| 517 | + .setDeviceName(newDevice.getDeviceName()) | ||
| 518 | + .setDeviceSecret(newDevice.getDeviceSecret()); | ||
| 519 | + } | ||
| 520 | + | ||
| 521 | + // ========== 批量上报处理方法 ========== | ||
| 522 | + | ||
| 523 | + /** | ||
| 524 | + * 处理批量上报消息 | ||
| 525 | + * <p> | ||
| 526 | + * 将 pack 消息拆分成多条标准消息,递归处理 | ||
| 527 | + * | ||
| 528 | + * @param packMessage 批量消息 | ||
| 529 | + * @param gatewayDevice 网关设备 | ||
| 530 | + */ | ||
| 531 | + private void handlePackMessage(IotDeviceMessage packMessage, IotDeviceDO gatewayDevice) { | ||
| 532 | + // 1. 解析参数 | ||
| 533 | + IotDevicePropertyPackPostReqDTO params = JsonUtils.parseObject( | ||
| 534 | + JsonUtils.toJsonString(packMessage.getParams()), | ||
| 535 | + IotDevicePropertyPackPostReqDTO.class); | ||
| 536 | + if (params == null) { | ||
| 537 | + log.warn("[handlePackMessage][消息({}) 参数解析失败]", packMessage.getId()); | ||
| 538 | + return; | ||
| 539 | + } | ||
| 540 | + | ||
| 541 | + // 2. 处理网关自身属性 | ||
| 542 | + if (params.getProperties() != null && !params.getProperties().isEmpty()) { | ||
| 543 | + Map<String, Object> gatewayProperties = convertPackProperties(params.getProperties()); | ||
| 544 | + IotDeviceMessage gatewayMsg = IotDeviceMessage.builder() | ||
| 545 | + .id(IotDeviceMessageUtils.generateMessageId()) | ||
| 546 | + .parentMessageId(packMessage.getId()) | ||
| 547 | + .deviceId(gatewayDevice.getId()) | ||
| 548 | + .tenantId(gatewayDevice.getTenantId()) | ||
| 549 | + .serverId(packMessage.getServerId()) | ||
| 550 | + .method(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) | ||
| 551 | + .params(gatewayProperties) | ||
| 552 | + .reportTime(LocalDateTime.now()) | ||
| 553 | + .build(); | ||
| 554 | + // 直接调用处理,不通过消息总线 | ||
| 555 | + try { | ||
| 556 | + devicePropertyService.saveDeviceProperty(gatewayDevice, gatewayMsg); | ||
| 557 | + getSelf().createDeviceLogAsync(gatewayMsg); | ||
| 558 | + } catch (Exception ex) { | ||
| 559 | + log.error("[handlePackMessage][网关({}) 属性处理失败]", gatewayDevice.getId(), ex); | ||
| 560 | + } | ||
| 561 | + } | ||
| 562 | + | ||
| 563 | + // 3. 处理子设备数据 | ||
| 564 | + if (params.getSubDevices() != null) { | ||
| 565 | + for (IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData : params.getSubDevices()) { | ||
| 566 | + try { | ||
| 567 | + handleSubDevicePackData(packMessage, subDeviceData); | ||
| 568 | + } catch (Exception ex) { | ||
| 569 | + log.error("[handlePackMessage][子设备({}/{}) 数据处理失败]", | ||
| 570 | + subDeviceData.getIdentity().getProductKey(), | ||
| 571 | + subDeviceData.getIdentity().getDeviceName(), ex); | ||
| 572 | + } | ||
| 573 | + } | ||
| 574 | + } | ||
| 575 | + } | ||
| 576 | + | ||
| 577 | + /** | ||
| 578 | + * 处理子设备的 pack 数据 | ||
| 579 | + * | ||
| 580 | + * @param packMessage 原始 pack 消息 | ||
| 581 | + * @param subDeviceData 子设备数据 | ||
| 582 | + */ | ||
| 583 | + private void handleSubDevicePackData(IotDeviceMessage packMessage, | ||
| 584 | + IotDevicePropertyPackPostReqDTO.SubDeviceData subDeviceData) { | ||
| 585 | + // 1. 获取子设备 | ||
| 586 | + IotDevicePropertyPackPostReqDTO.DeviceIdentity identity = subDeviceData.getIdentity(); | ||
| 587 | + IotDeviceDO subDevice = deviceService.getDeviceFromCache(identity.getProductKey(), identity.getDeviceName()); | ||
| 588 | + if (subDevice == null) { | ||
| 589 | + log.warn("[handleSubDevicePackData][子设备({}/{}) 不存在]", | ||
| 590 | + identity.getProductKey(), identity.getDeviceName()); | ||
| 591 | + return; | ||
| 592 | + } | ||
| 593 | + | ||
| 594 | + // 2. 处理子设备属性 | ||
| 595 | + if (subDeviceData.getProperties() != null && !subDeviceData.getProperties().isEmpty()) { | ||
| 596 | + Map<String, Object> properties = convertPackProperties(subDeviceData.getProperties()); | ||
| 597 | + IotDeviceMessage subMsg = IotDeviceMessage.builder() | ||
| 598 | + .id(IotDeviceMessageUtils.generateMessageId()) | ||
| 599 | + .parentMessageId(packMessage.getId()) | ||
| 600 | + .deviceId(subDevice.getId()) | ||
| 601 | + .tenantId(subDevice.getTenantId()) | ||
| 602 | + .serverId(packMessage.getServerId()) | ||
| 603 | + .method(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()) | ||
| 604 | + .params(properties) | ||
| 605 | + .reportTime(LocalDateTime.now()) | ||
| 606 | + .build(); | ||
| 607 | + devicePropertyService.saveDeviceProperty(subDevice, subMsg); | ||
| 608 | + getSelf().createDeviceLogAsync(subMsg); | ||
| 609 | + } | ||
| 610 | + | ||
| 611 | + // 3. 处理子设备事件(如果需要) | ||
| 612 | + // TODO: 事件处理可以后续扩展 | ||
| 613 | + } | ||
| 614 | + | ||
| 615 | + /** | ||
| 616 | + * 转换 pack 属性格式为标准属性格式 | ||
| 617 | + * <p> | ||
| 618 | + * pack 格式:{"temperature": {"value": 25.5, "time": 1524448722000}} | ||
| 619 | + * 标准格式:{"temperature": 25.5} | ||
| 620 | + * | ||
| 621 | + * @param packProperties pack 属性 | ||
| 622 | + * @return 标准属性 | ||
| 623 | + */ | ||
| 624 | + private Map<String, Object> convertPackProperties(Map<String, IotDevicePropertyPackPostReqDTO.PropertyValue> packProperties) { | ||
| 625 | + Map<String, Object> result = new HashMap<>(); | ||
| 626 | + for (Map.Entry<String, IotDevicePropertyPackPostReqDTO.PropertyValue> entry : packProperties.entrySet()) { | ||
| 627 | + if (entry.getValue() != null) { | ||
| 628 | + result.put(entry.getKey(), entry.getValue().getValue()); | ||
| 629 | + } | ||
| 630 | + } | ||
| 631 | + return result; | ||
| 632 | + } | ||
| 633 | + | ||
| 634 | + // ========== 网关下线联动处理 ========== | ||
| 635 | + | ||
| 636 | + // TODO @AI:是不是写到 deviceService 里更合适?更解耦。 | ||
| 637 | + /** | ||
| 638 | + * 处理网关下线,联动所有子设备下线 | ||
| 639 | + * | ||
| 640 | + * @param gatewayDevice 网关设备 | ||
| 641 | + * @param serverId 服务标识 | ||
| 642 | + */ | ||
| 643 | + private void handleGatewayOffline(IotDeviceDO gatewayDevice, String serverId) { | ||
| 644 | + // 1. 获取网关下所有子设备 | ||
| 645 | + List<IotDeviceDO> subDevices = deviceService.getDeviceListByGatewayId(gatewayDevice.getId()); | ||
| 646 | + if (subDevices == null || subDevices.isEmpty()) { | ||
| 647 | + return; | ||
| 648 | + } | ||
| 649 | + | ||
| 650 | + // 2. 将在线的子设备设置为下线 | ||
| 651 | + for (IotDeviceDO subDevice : subDevices) { | ||
| 652 | + if (Objects.equal(subDevice.getState(), IotDeviceStateEnum.ONLINE.getState())) { | ||
| 653 | + try { | ||
| 654 | + deviceService.updateDeviceState(subDevice, IotDeviceStateEnum.OFFLINE.getState()); | ||
| 655 | + log.info("[handleGatewayOffline][网关({}/{}) 下线,子设备({}/{}) 联动下线]", | ||
| 656 | + gatewayDevice.getProductKey(), gatewayDevice.getDeviceName(), | ||
| 657 | + subDevice.getProductKey(), subDevice.getDeviceName()); | ||
| 658 | + } catch (Exception ex) { | ||
| 659 | + log.error("[handleGatewayOffline][子设备({}/{}) 下线失败]", | ||
| 660 | + subDevice.getProductKey(), subDevice.getDeviceName(), ex); | ||
| 661 | + } | ||
| 662 | + } | ||
| 663 | + } | ||
| 664 | + } | ||
| 665 | + | ||
| 216 | @Override | 666 | @Override |
| 217 | public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) { | 667 | public PageResult<IotDeviceMessageDO> getDeviceMessagePage(IotDeviceMessagePageReqVO pageReqVO) { |
| 218 | try { | 668 | try { |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
| @@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { | @@ -24,12 +24,28 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { | ||
| 24 | 24 | ||
| 25 | // TODO 芋艿:要不要加个 ping 消息; | 25 | // TODO 芋艿:要不要加个 ping 消息; |
| 26 | 26 | ||
| 27 | + // ========== 拓扑管理 ========== | ||
| 28 | + // 可参考:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships | ||
| 29 | + | ||
| 30 | + TOPO_ADD("thing.topo.add", "添加拓扑关系", true), | ||
| 31 | + TOPO_DELETE("thing.topo.delete", "删除拓扑关系", true), | ||
| 32 | + TOPO_GET("thing.topo.get", "获取拓扑关系", true), | ||
| 33 | + TOPO_CHANGE("thing.topo.change", "拓扑关系变更通知", false), | ||
| 34 | + | ||
| 35 | + // ========== 设备注册 ========== | ||
| 36 | + // 可参考:https://help.aliyun.com/zh/iot/user-guide/register-devices | ||
| 37 | + | ||
| 38 | + SUB_DEVICE_REGISTER("thing.sub.register", "子设备动态注册", true), | ||
| 39 | + | ||
| 27 | // ========== 设备属性 ========== | 40 | // ========== 设备属性 ========== |
| 28 | // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services | 41 | // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services |
| 29 | 42 | ||
| 30 | PROPERTY_POST("thing.property.post", "属性上报", true), | 43 | PROPERTY_POST("thing.property.post", "属性上报", true), |
| 31 | PROPERTY_SET("thing.property.set", "属性设置", false), | 44 | PROPERTY_SET("thing.property.set", "属性设置", false), |
| 32 | 45 | ||
| 46 | + // TODO @AI:改成 thing.property.pack.post | ||
| 47 | + PROPERTY_PACK_POST("thing.event.property.pack.post", "批量上报(属性 + 事件 + 子设备)", true), // 网关独有 | ||
| 48 | + | ||
| 33 | // ========== 设备事件 ========== | 49 | // ========== 设备事件 ========== |
| 34 | // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services | 50 | // 可参考:https://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services |
| 35 | 51 | ||
| @@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { | @@ -50,6 +66,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { | ||
| 50 | 66 | ||
| 51 | OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), | 67 | OTA_UPGRADE("thing.ota.upgrade", "OTA 固定信息推送", false), |
| 52 | OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), | 68 | OTA_PROGRESS("thing.ota.progress", "OTA 升级进度上报", true), |
| 69 | + | ||
| 53 | ; | 70 | ; |
| 54 | 71 | ||
| 55 | public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod) | 72 | public static final String[] ARRAYS = Arrays.stream(values()).map(IotDeviceMessageMethodEnum::getMethod) |
| @@ -60,7 +77,8 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { | @@ -60,7 +77,8 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable<String> { | ||
| 60 | */ | 77 | */ |
| 61 | public static final Set<String> REPLY_DISABLED = SetUtils.asSet( | 78 | public static final Set<String> REPLY_DISABLED = SetUtils.asSet( |
| 62 | STATE_UPDATE.getMethod(), | 79 | STATE_UPDATE.getMethod(), |
| 63 | - OTA_PROGRESS.getMethod() // 参考阿里云,OTA 升级进度上报,不进行回复 | 80 | + OTA_PROGRESS.getMethod(), // 参考阿里云,OTA 升级进度上报,不进行回复 |
| 81 | + TOPO_CHANGE.getMethod() // 拓扑变更通知,下行消息,不需要回复 TODO @AI:看看阿里云的文档,确认下是不是这样的 | ||
| 64 | ); | 82 | ); |
| 65 | 83 | ||
| 66 | private final String method; | 84 | private final String method; |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/mq/message/IotDeviceMessage.java
| @@ -94,6 +94,42 @@ public class IotDeviceMessage { | @@ -94,6 +94,42 @@ public class IotDeviceMessage { | ||
| 94 | */ | 94 | */ |
| 95 | private String msg; | 95 | private String msg; |
| 96 | 96 | ||
| 97 | + /** | ||
| 98 | + * 父消息 ID | ||
| 99 | + * <p> | ||
| 100 | + * - null:原始消息,需要 reply | ||
| 101 | + * - 非 null:从父消息(如 pack)拆分而来,不需要单独 reply | ||
| 102 | + */ | ||
| 103 | + private String parentMessageId; | ||
| 104 | + | ||
| 105 | + // TODO @TODO @AI:抽到工具类里,具体哪个,一起讨论下; | ||
| 106 | + /** | ||
| 107 | + * 判断是否需要发送 reply | ||
| 108 | + * | ||
| 109 | + * @return 是否需要回复 | ||
| 110 | + */ | ||
| 111 | + public boolean needReply() { | ||
| 112 | + // 1. 来自拆包的消息,不单独 reply | ||
| 113 | + if (parentMessageId != null) { | ||
| 114 | + return false; | ||
| 115 | + } | ||
| 116 | + // 2. 某些方法本身不需要 reply(如 STATE_UPDATE) | ||
| 117 | + if (IotDeviceMessageMethodEnum.isReplyDisabled(method)) { | ||
| 118 | + return false; | ||
| 119 | + } | ||
| 120 | + return true; | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + // TODO @AI:没用的字段,删除删除 | ||
| 124 | + /** | ||
| 125 | + * 获取原始消息 ID(用于日志追踪) | ||
| 126 | + * | ||
| 127 | + * @return 原始消息 ID | ||
| 128 | + */ | ||
| 129 | + public String getOriginMessageId() { | ||
| 130 | + return parentMessageId != null ? parentMessageId : id; | ||
| 131 | + } | ||
| 132 | + | ||
| 97 | // ========== 基础方法:只传递"codec(编解码)字段" ========== | 133 | // ========== 基础方法:只传递"codec(编解码)字段" ========== |
| 98 | 134 | ||
| 99 | public static IotDeviceMessage requestOf(String method) { | 135 | public static IotDeviceMessage requestOf(String method) { |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.auth; | ||
| 2 | + | ||
| 3 | +import jakarta.validation.constraints.NotEmpty; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +// TODO @AI:不用带 device 关键字; | ||
| 7 | +// TODO @AI:挂个阿里云的链接,https://help.aliyun.com/zh/iot/user-guide/register-devices 的「子设备的 MQTT 动态注册」小节 | ||
| 8 | +/** | ||
| 9 | + * IoT 子设备动态注册 Request DTO | ||
| 10 | + * <p> | ||
| 11 | + * 用于 thing.sub.register 消息的 params 数组元素 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@Data | ||
| 16 | +public class IotSubDeviceRegisterReqDTO { | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 子设备 ProductKey | ||
| 20 | + */ | ||
| 21 | + @NotEmpty(message = "产品标识不能为空") | ||
| 22 | + private String productKey; | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 子设备 DeviceName | ||
| 26 | + */ | ||
| 27 | + @NotEmpty(message = "设备名称不能为空") | ||
| 28 | + private String deviceName; | ||
| 29 | + | ||
| 30 | +} |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterRespDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.auth; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +// TODO @AI:修复建议,参考 /Users/yunai/Java/ruoyi-vue-pro-jdk25/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/auth/IotSubDeviceRegisterReqDTO.java | ||
| 6 | +/** | ||
| 7 | + * IoT 子设备动态注册 Response DTO | ||
| 8 | + * <p> | ||
| 9 | + * 用于 thing.sub.register 响应的设备信息 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +public class IotSubDeviceRegisterRespDTO { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 子设备 ProductKey | ||
| 18 | + */ | ||
| 19 | + private String productKey; | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 子设备 DeviceName | ||
| 23 | + */ | ||
| 24 | + private String deviceName; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 分配的 DeviceSecret | ||
| 28 | + */ | ||
| 29 | + private String deviceSecret; | ||
| 30 | + | ||
| 31 | +} |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/package-info.java
0 → 100644
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/property/IotDevicePropertyPackPostReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.property; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | +import lombok.experimental.Accessors; | ||
| 5 | + | ||
| 6 | +import java.util.List; | ||
| 7 | +import java.util.Map; | ||
| 8 | + | ||
| 9 | +// TODO @AI:挂个阿里云的链接,http://help.aliyun.com/zh/iot/user-guide/device-properties-events-and-services 的「设备批量上报属性、事件」小节 | ||
| 10 | +/** | ||
| 11 | + * IoT 设备属性批量上报 Request DTO | ||
| 12 | + * <p> | ||
| 13 | + * 用于 thing.event.property.pack.post 消息的 params 参数 | ||
| 14 | + * 参考阿里云 Alink 协议 | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +@Data | ||
| 19 | +public class IotDevicePropertyPackPostReqDTO { | ||
| 20 | + | ||
| 21 | + // TODO @AI:去掉里面的 time,直接平铺值(可能就是直接的 map);例如说 "Power":value, 而不是 PropertyValue "properties": { | ||
| 22 | + // "Power": [ | ||
| 23 | + // { | ||
| 24 | + // "value": "on", | ||
| 25 | + // "time": 1524448722000 | ||
| 26 | + // }, | ||
| 27 | + // { | ||
| 28 | + // "value": "off", | ||
| 29 | + // "time": 1524448722001 | ||
| 30 | + // } | ||
| 31 | + // ], | ||
| 32 | + // "WF": [ | ||
| 33 | + // { | ||
| 34 | + // "value": 3, | ||
| 35 | + // "time": 1524448722000 | ||
| 36 | + // }, | ||
| 37 | + // { | ||
| 38 | + // "value": 4, | ||
| 39 | + // "time": 1524448722009 | ||
| 40 | + // } | ||
| 41 | + // ] | ||
| 42 | + // } | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 网关自身属性 | ||
| 46 | + * <p> | ||
| 47 | + * key: 属性标识符 | ||
| 48 | + * value: 属性值对象(包含 value 和 time) | ||
| 49 | + */ | ||
| 50 | + private Map<String, PropertyValue> properties; | ||
| 51 | + | ||
| 52 | + // TODO @AI:EventValue { | ||
| 53 | + // | ||
| 54 | + // "method": "thing.event.post", | ||
| 55 | + // | ||
| 56 | + // "version": "1.0", | ||
| 57 | + // | ||
| 58 | + // "params": { | ||
| 59 | + // | ||
| 60 | + // "identifier": "eat", | ||
| 61 | + // | ||
| 62 | + // "params": { | ||
| 63 | + // | ||
| 64 | + // "rice": 100 | ||
| 65 | + // | ||
| 66 | + // } | ||
| 67 | + // | ||
| 68 | + // } | ||
| 69 | + // | ||
| 70 | + //} | ||
| 71 | + | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 网关自身事件 | ||
| 75 | + * <p> | ||
| 76 | + * key: 事件标识符 | ||
| 77 | + * value: 事件值对象(包含 value 和 time) | ||
| 78 | + */ | ||
| 79 | + private Map<String, EventValue> events; | ||
| 80 | + | ||
| 81 | + /** | ||
| 82 | + * 子设备数据列表 | ||
| 83 | + */ | ||
| 84 | + private List<SubDeviceData> subDevices; | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 属性值对象 | ||
| 88 | + */ | ||
| 89 | + @Data | ||
| 90 | + public static class PropertyValue { | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * 属性值 | ||
| 94 | + */ | ||
| 95 | + private Object value; | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * 上报时间(毫秒时间戳) | ||
| 99 | + */ | ||
| 100 | + private Long time; | ||
| 101 | + | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 事件值对象 | ||
| 106 | + */ | ||
| 107 | + @Data | ||
| 108 | + public static class EventValue { | ||
| 109 | + | ||
| 110 | + /** | ||
| 111 | + * 事件参数 | ||
| 112 | + */ | ||
| 113 | + private Object value; | ||
| 114 | + | ||
| 115 | + /** | ||
| 116 | + * 上报时间(毫秒时间戳) | ||
| 117 | + */ | ||
| 118 | + private Long time; | ||
| 119 | + | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * 子设备数据 | ||
| 124 | + */ | ||
| 125 | + @Data | ||
| 126 | + public static class SubDeviceData { | ||
| 127 | + | ||
| 128 | + /** | ||
| 129 | + * 子设备标识 | ||
| 130 | + */ | ||
| 131 | + private DeviceIdentity identity; | ||
| 132 | + | ||
| 133 | + /** | ||
| 134 | + * 子设备属性 | ||
| 135 | + */ | ||
| 136 | + private Map<String, PropertyValue> properties; | ||
| 137 | + | ||
| 138 | + /** | ||
| 139 | + * 子设备事件 | ||
| 140 | + */ | ||
| 141 | + private Map<String, EventValue> events; | ||
| 142 | + | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + /** | ||
| 146 | + * 设备标识 | ||
| 147 | + */ | ||
| 148 | + @Data | ||
| 149 | + @Accessors(chain = true) | ||
| 150 | + public static class DeviceIdentity { | ||
| 151 | + | ||
| 152 | + /** | ||
| 153 | + * 产品标识 | ||
| 154 | + */ | ||
| 155 | + private String productKey; | ||
| 156 | + | ||
| 157 | + /** | ||
| 158 | + * 设备名称 | ||
| 159 | + */ | ||
| 160 | + private String deviceName; | ||
| 161 | + | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | +} |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoAddReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.topo; | ||
| 2 | + | ||
| 3 | +import jakarta.validation.constraints.NotEmpty; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +// TODO @AI:得一起讨论下,到底使用什么后缀合适:1)一方面要体现出请求、响应;2)一方面体现出上下行(设备 to server,还是 server to 设备),可以一起讨论? | ||
| 7 | +// TODO @AI:文档地址:https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships?spm=a2c4g.11186623.help-menu-30520.d_2_2_7_3_2.2e983f47Z2iGbo&scm=20140722.H_89299._.OR_help-T_cn~zh-V_1#section-w33-vyg-12b | ||
| 8 | +/** | ||
| 9 | + * IoT 设备拓扑添加 Request DTO | ||
| 10 | + * <p> | ||
| 11 | + * 用于 thing.topo.add 消息的 params 参数 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@Data | ||
| 16 | +public class IotDeviceTopoAddReqDTO { | ||
| 17 | + | ||
| 18 | + // TODO @AI:是个数组; | ||
| 19 | + // TODO @AI:有响应结果的; | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 子设备客户端 ID | ||
| 23 | + */ | ||
| 24 | + @NotEmpty(message = "客户端 ID 不能为空") | ||
| 25 | + private String clientId; | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 子设备用户名 | ||
| 29 | + */ | ||
| 30 | + @NotEmpty(message = "用户名不能为空") | ||
| 31 | + private String username; | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 子设备认证密码 | ||
| 35 | + */ | ||
| 36 | + @NotEmpty(message = "密码不能为空") | ||
| 37 | + private String password; | ||
| 38 | + | ||
| 39 | +} |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoDeleteReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.topo; | ||
| 2 | + | ||
| 3 | +import jakarta.validation.constraints.NotEmpty; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +// TODO @AI:是个数组;https://help.aliyun.com/zh/iot/user-guide/manage-topological-relationships?spm=a2c4g.11186623.help-menu-30520.d_2_2_7_3_2.540c390beZSDOG&scm=20140722.H_89299._.OR_help-T_cn~zh-V_1#section-rb1-wzw-y2b | ||
| 7 | + | ||
| 8 | +// todo @AI:删除完,需要有个 reply 响应; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * IoT 设备拓扑删除 Request DTO | ||
| 12 | + * <p> | ||
| 13 | + * 用于 thing.topo.delete 消息的 params 参数 | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@Data | ||
| 18 | +public class IotDeviceTopoDeleteReqDTO { | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * 子设备 ProductKey | ||
| 22 | + */ | ||
| 23 | + @NotEmpty(message = "产品标识不能为空") | ||
| 24 | + private String productKey; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 子设备 DeviceName | ||
| 28 | + */ | ||
| 29 | + @NotEmpty(message = "设备名称不能为空") | ||
| 30 | + private String deviceName; | ||
| 31 | + | ||
| 32 | +} |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/topo/IotDeviceTopoRespDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.core.topic.topo; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +// TODO @AI:是不是改成 IotDeviceTopoGetRespDTO | ||
| 6 | +/** | ||
| 7 | + * IoT 设备拓扑关系 Response DTO | ||
| 8 | + * <p> | ||
| 9 | + * 用于 thing.topo.get 响应的子设备信息 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +public class IotDeviceTopoRespDTO { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 子设备 ProductKey | ||
| 18 | + */ | ||
| 19 | + private String productKey; | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 子设备 DeviceName | ||
| 23 | + */ | ||
| 24 | + private String deviceName; | ||
| 25 | + | ||
| 26 | +} |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
| @@ -63,4 +63,50 @@ public final class IotMqttTopicUtils { | @@ -63,4 +63,50 @@ public final class IotMqttTopicUtils { | ||
| 63 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; | 63 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; |
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | + /** | ||
| 67 | + * 构建拓扑管理 Topic | ||
| 68 | + * <p> | ||
| 69 | + * 拓扑管理类 Topic 使用网关设备的 productKey/deviceName | ||
| 70 | + * | ||
| 71 | + * @param method 方法,如 thing.topo.add | ||
| 72 | + * @param gatewayProductKey 网关 ProductKey | ||
| 73 | + * @param gatewayDeviceName 网关 DeviceName | ||
| 74 | + * @param isReply 是否为响应 | ||
| 75 | + * @return Topic | ||
| 76 | + */ | ||
| 77 | + public static String buildTopoTopic(String method, String gatewayProductKey, | ||
| 78 | + String gatewayDeviceName, boolean isReply) { | ||
| 79 | + return buildTopicByMethod(method, gatewayProductKey, gatewayDeviceName, isReply); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + /** | ||
| 83 | + * 判断是否为拓扑管理 Topic(通过 method 判断) | ||
| 84 | + * | ||
| 85 | + * @param method 消息方法 | ||
| 86 | + * @return 是否为拓扑管理 Topic | ||
| 87 | + */ | ||
| 88 | + public static boolean isTopoMethod(String method) { | ||
| 89 | + return method != null && method.startsWith("thing.topo."); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + /** | ||
| 93 | + * 判断是否为子设备注册 Topic | ||
| 94 | + * | ||
| 95 | + * @param method 消息方法 | ||
| 96 | + * @return 是否为子设备注册 Topic | ||
| 97 | + */ | ||
| 98 | + public static boolean isSubDeviceRegisterMethod(String method) { | ||
| 99 | + return "thing.sub.register".equals(method); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * 判断是否为批量上报 Topic | ||
| 104 | + * | ||
| 105 | + * @param method 消息方法 | ||
| 106 | + * @return 是否为批量上报 Topic | ||
| 107 | + */ | ||
| 108 | + public static boolean isPackPostMethod(String method) { | ||
| 109 | + return "thing.event.property.pack.post".equals(method); | ||
| 110 | + } | ||
| 111 | + | ||
| 66 | } | 112 | } |
| 67 | \ No newline at end of file | 113 | \ No newline at end of file |