Commit 45b8172a617a15f5a7b34347ed1da208097a2339
1 parent
b4540698
【代码新增】IoT:实现 device 下行属性获取、设置的下行消息
Showing
6 changed files
with
237 additions
and
15 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceController.http
| 1 | -### 请求 /iot/device/simulation-downstream 接口 => 成功 | |
| 1 | +### 请求 /iot/device/simulation-downstream 接口(服务调用) => 成功 | |
| 2 | 2 | POST {{baseUrl}}/iot/device/simulation-downstream |
| 3 | 3 | Content-Type: application/json |
| 4 | 4 | tenant-id: {{adminTenentId}} |
| ... | ... | @@ -11,4 +11,32 @@ Authorization: Bearer {{token}} |
| 11 | 11 | "data": { |
| 12 | 12 | "xx": "yy" |
| 13 | 13 | } |
| 14 | +} | |
| 15 | + | |
| 16 | +### 请求 /iot/device/simulation-downstream 接口(属性设置) => 成功 | |
| 17 | +POST {{baseUrl}}/iot/device/simulation-downstream | |
| 18 | +Content-Type: application/json | |
| 19 | +tenant-id: {{adminTenentId}} | |
| 20 | +Authorization: Bearer {{token}} | |
| 21 | + | |
| 22 | +{ | |
| 23 | + "id": 25, | |
| 24 | + "type": "property", | |
| 25 | + "identifier": "set", | |
| 26 | + "data": { | |
| 27 | + "xx": "yy" | |
| 28 | + } | |
| 29 | +} | |
| 30 | + | |
| 31 | +### 请求 /iot/device/simulation-downstream 接口(属性获取) => 成功 | |
| 32 | +POST {{baseUrl}}/iot/device/simulation-downstream | |
| 33 | +Content-Type: application/json | |
| 34 | +tenant-id: {{adminTenentId}} | |
| 35 | +Authorization: Bearer {{token}} | |
| 36 | + | |
| 37 | +{ | |
| 38 | + "id": 25, | |
| 39 | + "type": "property", | |
| 40 | + "identifier": "get", | |
| 41 | + "data": ["xx", "yy"] | |
| 14 | 42 | } |
| 15 | 43 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/control/IotDeviceDownstreamServiceImpl.java
| ... | ... | @@ -6,6 +6,8 @@ import cn.hutool.core.util.IdUtil; |
| 6 | 6 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 7 | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceDownstreamAbstractReqDTO; |
| 9 | +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertyGetReqDTO; | |
| 10 | +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertySetReqDTO; | |
| 9 | 11 | import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDeviceServiceInvokeReqDTO; |
| 10 | 12 | import cn.iocoder.yudao.module.iot.controller.admin.device.vo.control.IotDeviceSimulationDownstreamReqVO; |
| 11 | 13 | import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; |
| ... | ... | @@ -24,6 +26,7 @@ import org.springframework.validation.annotation.Validated; |
| 24 | 26 | import org.springframework.web.client.RestTemplate; |
| 25 | 27 | |
| 26 | 28 | import java.time.LocalDateTime; |
| 29 | +import java.util.List; | |
| 27 | 30 | import java.util.Map; |
| 28 | 31 | import java.util.Objects; |
| 29 | 32 | |
| ... | ... | @@ -91,11 +94,12 @@ public class IotDeviceDownstreamServiceImpl implements IotDeviceDownstreamServic |
| 91 | 94 | */ |
| 92 | 95 | @SuppressWarnings("unchecked") |
| 93 | 96 | private void invokeDeviceService(IotDeviceSimulationDownstreamReqVO downstreamReqVO, |
| 94 | - IotDeviceDO device, IotDeviceDO parentDevice) { | |
| 97 | + IotDeviceDO device, IotDeviceDO parentDevice) { | |
| 95 | 98 | // 1. 参数校验 |
| 96 | 99 | if (!(downstreamReqVO.getData() instanceof Map<?, ?>)) { |
| 97 | 100 | throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); |
| 98 | 101 | } |
| 102 | + // TODO @super:【可优化】过滤掉不合法的服务 | |
| 99 | 103 | |
| 100 | 104 | // 2. 发送请求 |
| 101 | 105 | String url = String.format( "sys/%s/%s/thing/service/%s", |
| ... | ... | @@ -125,16 +129,73 @@ public class IotDeviceDownstreamServiceImpl implements IotDeviceDownstreamServic |
| 125 | 129 | * @param device 设备 |
| 126 | 130 | * @param parentDevice 父设备 |
| 127 | 131 | */ |
| 132 | + @SuppressWarnings("unchecked") | |
| 128 | 133 | private void setDeviceProperty(IotDeviceSimulationDownstreamReqVO downstreamReqVO, |
| 129 | - IotDeviceDO device, IotDeviceDO parentDevice) { | |
| 130 | - // TODO 1. 请求 | |
| 134 | + IotDeviceDO device, IotDeviceDO parentDevice) { | |
| 135 | + // 1. 参数校验 | |
| 136 | + if (!(downstreamReqVO.getData() instanceof Map<?, ?>)) { | |
| 137 | + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 Map 类型"); | |
| 138 | + } | |
| 139 | + // TODO @super:【可优化】过滤掉不合法的属性 | |
| 140 | + | |
| 141 | + // 2. 发送请求 | |
| 142 | + String url = String.format( "sys/%s/%s/thing/service/property/set", | |
| 143 | + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); | |
| 144 | + IotDevicePropertySetReqDTO reqDTO = new IotDevicePropertySetReqDTO() | |
| 145 | + .setProperties((Map<String, Object>) downstreamReqVO.getData()); | |
| 146 | + CommonResult<Boolean> result = requestPlugin(url, reqDTO, device); | |
| 147 | + | |
| 148 | + // 3. 发送设备消息 | |
| 149 | + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) | |
| 150 | + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) | |
| 151 | + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) | |
| 152 | + .setData(reqDTO.getProperties()); | |
| 153 | + sendDeviceMessage(message, device, result.getCode()); | |
| 131 | 154 | |
| 132 | - // TODO 2. 发送消息 | |
| 155 | + // 4. 如果不成功,抛出异常,提示用户 | |
| 156 | + if (result.isError()) { | |
| 157 | + log.error("[setDeviceProperty][设备({})属性设置失败,请求参数:({}),响应结果:({})]", | |
| 158 | + device.getDeviceKey(), reqDTO, result); | |
| 159 | + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); | |
| 160 | + } | |
| 133 | 161 | } |
| 134 | 162 | |
| 163 | + /** | |
| 164 | + * 获取设备属性 | |
| 165 | + * | |
| 166 | + * @param downstreamReqVO 下行请求 | |
| 167 | + * @param device 设备 | |
| 168 | + * @param parentDevice 父设备 | |
| 169 | + */ | |
| 170 | + @SuppressWarnings("unchecked") | |
| 135 | 171 | private void getDeviceProperty(IotDeviceSimulationDownstreamReqVO downstreamReqVO, |
| 136 | - IotDeviceDO device, IotDeviceDO parentDevice) { | |
| 137 | - // TODO 芋艿:这里需要获取设备属性 | |
| 172 | + IotDeviceDO device, IotDeviceDO parentDevice) { | |
| 173 | + // 1. 参数校验 | |
| 174 | + if (!(downstreamReqVO.getData() instanceof List<?>)) { | |
| 175 | + throw new ServiceException(BAD_REQUEST.getCode(), "data 不是 List 类型"); | |
| 176 | + } | |
| 177 | + // TODO @super:【可优化】过滤掉不合法的属性 | |
| 178 | + | |
| 179 | + // 2. 发送请求 | |
| 180 | + String url = String.format( "sys/%s/%s/thing/service/property/get", | |
| 181 | + getProductKey(device, parentDevice), getDeviceName(device, parentDevice)); | |
| 182 | + IotDevicePropertyGetReqDTO reqDTO = new IotDevicePropertyGetReqDTO() | |
| 183 | + .setIdentifiers((List<String>) downstreamReqVO.getData()); | |
| 184 | + CommonResult<Boolean> result = requestPlugin(url, reqDTO, device); | |
| 185 | + | |
| 186 | + // 3. 发送设备消息 | |
| 187 | + IotDeviceMessage message = new IotDeviceMessage().setRequestId(reqDTO.getRequestId()) | |
| 188 | + .setType(IotDeviceMessageTypeEnum.PROPERTY.getType()) | |
| 189 | + .setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_SET.getIdentifier()) | |
| 190 | + .setData(reqDTO.getIdentifiers()); | |
| 191 | + sendDeviceMessage(message, device, result.getCode()); | |
| 192 | + | |
| 193 | + // 4. 如果不成功,抛出异常,提示用户 | |
| 194 | + if (result.isError()) { | |
| 195 | + log.error("[getDeviceProperty][设备({})属性获取失败,请求参数:({}),响应结果:({})]", | |
| 196 | + device.getDeviceKey(), reqDTO, result); | |
| 197 | + throw exception(DEVICE_DOWNSTREAM_FAILED, result.getMsg()); | |
| 198 | + } | |
| 138 | 199 | } |
| 139 | 200 | |
| 140 | 201 | /** | ... | ... |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/IotDeviceDownstreamServer.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.plugin.common.downstream; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.iot.plugin.common.downstream.router.IotDevicePropertyGetVertxHandler; | |
| 4 | +import cn.iocoder.yudao.module.iot.plugin.common.downstream.router.IotDevicePropertySetVertxHandler; | |
| 3 | 5 | import cn.iocoder.yudao.module.iot.plugin.common.downstream.router.IotDeviceServiceInvokeVertxHandler; |
| 4 | 6 | import io.vertx.core.Vertx; |
| 5 | 7 | import io.vertx.core.http.HttpServer; |
| ... | ... | @@ -24,8 +26,12 @@ public class IotDeviceDownstreamServer { |
| 24 | 26 | // 创建 Router 实例 |
| 25 | 27 | Router router = Router.router(vertx); |
| 26 | 28 | router.route().handler(BodyHandler.create()); // 处理 Body |
| 27 | - router.post(IotDeviceServiceInvokeVertxHandler.PATH).handler( | |
| 28 | - new IotDeviceServiceInvokeVertxHandler(deviceDownstreamHandler)); // 处理 Service Invoke | |
| 29 | + router.post(IotDeviceServiceInvokeVertxHandler.PATH) | |
| 30 | + .handler(new IotDeviceServiceInvokeVertxHandler(deviceDownstreamHandler)); | |
| 31 | + router.post(IotDevicePropertySetVertxHandler.PATH) | |
| 32 | + .handler(new IotDevicePropertySetVertxHandler(deviceDownstreamHandler)); | |
| 33 | + router.post(IotDevicePropertyGetVertxHandler.PATH) | |
| 34 | + .handler(new IotDevicePropertyGetVertxHandler(deviceDownstreamHandler)); | |
| 29 | 35 | // 创建 HttpServer 实例 |
| 30 | 36 | this.server = vertx.createHttpServer().requestHandler(router); |
| 31 | 37 | } | ... | ... |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertyGetVertxHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 4 | +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertyGetReqDTO; | |
| 5 | +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; | |
| 6 | +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; | |
| 7 | +import io.vertx.core.Handler; | |
| 8 | +import io.vertx.core.json.JsonObject; | |
| 9 | +import io.vertx.ext.web.RoutingContext; | |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | + | |
| 13 | +import java.util.List; | |
| 14 | + | |
| 15 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | |
| 16 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * IOT 设备服务获取 Vertx Handler | |
| 20 | + * | |
| 21 | + * 芋道源码 | |
| 22 | + */ | |
| 23 | +@Slf4j | |
| 24 | +@RequiredArgsConstructor | |
| 25 | +public class IotDevicePropertyGetVertxHandler implements Handler<RoutingContext> { | |
| 26 | + | |
| 27 | + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/get"; | |
| 28 | + | |
| 29 | + private final IotDeviceDownstreamHandler deviceDownstreamHandler; | |
| 30 | + | |
| 31 | + @Override | |
| 32 | + @SuppressWarnings("unchecked") | |
| 33 | + public void handle(RoutingContext routingContext) { | |
| 34 | + // 1. 解析参数 | |
| 35 | + IotDevicePropertyGetReqDTO reqDTO; | |
| 36 | + try { | |
| 37 | + String productKey = routingContext.pathParam("productKey"); | |
| 38 | + String deviceName = routingContext.pathParam("deviceName"); | |
| 39 | + JsonObject body = routingContext.body().asJsonObject(); | |
| 40 | + String requestId = body.getString("requestId"); | |
| 41 | + List<String> identifiers = (List<String>) body.getMap().get("identifiers"); | |
| 42 | + reqDTO = ((IotDevicePropertyGetReqDTO) new IotDevicePropertyGetReqDTO() | |
| 43 | + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) | |
| 44 | + .setIdentifiers(identifiers); | |
| 45 | + } catch (Exception e) { | |
| 46 | + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); | |
| 47 | + IotPluginCommonUtils.writeJson(routingContext, CommonResult.error(BAD_REQUEST)); | |
| 48 | + return; | |
| 49 | + } | |
| 50 | + | |
| 51 | + // 2. 调用处理器 | |
| 52 | + try { | |
| 53 | + CommonResult<Boolean> result = deviceDownstreamHandler.getDeviceProperty(reqDTO); | |
| 54 | + IotPluginCommonUtils.writeJson(routingContext, result); | |
| 55 | + } catch (Exception e) { | |
| 56 | + log.error("[handle][请求参数({}) 属性获取异常]", reqDTO, e); | |
| 57 | + IotPluginCommonUtils.writeJson(routingContext, CommonResult.error(INTERNAL_SERVER_ERROR)); | |
| 58 | + } | |
| 59 | + } | |
| 60 | + | |
| 61 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDevicePropertySetVertxHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.plugin.common.downstream.router; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 4 | +import cn.iocoder.yudao.module.iot.api.device.dto.control.downstream.IotDevicePropertySetReqDTO; | |
| 5 | +import cn.iocoder.yudao.module.iot.plugin.common.downstream.IotDeviceDownstreamHandler; | |
| 6 | +import cn.iocoder.yudao.module.iot.plugin.common.util.IotPluginCommonUtils; | |
| 7 | +import io.vertx.core.Handler; | |
| 8 | +import io.vertx.core.json.JsonObject; | |
| 9 | +import io.vertx.ext.web.RoutingContext; | |
| 10 | +import lombok.RequiredArgsConstructor; | |
| 11 | +import lombok.extern.slf4j.Slf4j; | |
| 12 | + | |
| 13 | +import java.util.Map; | |
| 14 | + | |
| 15 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | |
| 16 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 17 | + | |
| 18 | +/** | |
| 19 | + * IOT 设备服务设置 Vertx Handler | |
| 20 | + * | |
| 21 | + * 芋道源码 | |
| 22 | + */ | |
| 23 | +@Slf4j | |
| 24 | +@RequiredArgsConstructor | |
| 25 | +public class IotDevicePropertySetVertxHandler implements Handler<RoutingContext> { | |
| 26 | + | |
| 27 | + public static final String PATH = "/sys/:productKey/:deviceName/thing/service/property/set"; | |
| 28 | + | |
| 29 | + private final IotDeviceDownstreamHandler deviceDownstreamHandler; | |
| 30 | + | |
| 31 | + @Override | |
| 32 | + @SuppressWarnings("unchecked") | |
| 33 | + public void handle(RoutingContext routingContext) { | |
| 34 | + // 1. 解析参数 | |
| 35 | + IotDevicePropertySetReqDTO reqDTO; | |
| 36 | + try { | |
| 37 | + String productKey = routingContext.pathParam("productKey"); | |
| 38 | + String deviceName = routingContext.pathParam("deviceName"); | |
| 39 | + JsonObject body = routingContext.body().asJsonObject(); | |
| 40 | + String requestId = body.getString("requestId"); | |
| 41 | + Map<String, Object> properties = (Map<String, Object>) body.getMap().get("properties"); | |
| 42 | + reqDTO = ((IotDevicePropertySetReqDTO) new IotDevicePropertySetReqDTO() | |
| 43 | + .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) | |
| 44 | + .setProperties(properties); | |
| 45 | + } catch (Exception e) { | |
| 46 | + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); | |
| 47 | + IotPluginCommonUtils.writeJson(routingContext, CommonResult.error(BAD_REQUEST)); | |
| 48 | + return; | |
| 49 | + } | |
| 50 | + | |
| 51 | + // 2. 调用处理器 | |
| 52 | + try { | |
| 53 | + CommonResult<Boolean> result = deviceDownstreamHandler.setDeviceProperty(reqDTO); | |
| 54 | + IotPluginCommonUtils.writeJson(routingContext, result); | |
| 55 | + } catch (Exception e) { | |
| 56 | + log.error("[handle][请求参数({}) 属性设置异常]", reqDTO, e); | |
| 57 | + IotPluginCommonUtils.writeJson(routingContext, CommonResult.error(INTERNAL_SERVER_ERROR)); | |
| 58 | + } | |
| 59 | + } | |
| 60 | + | |
| 61 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-plugins/yudao-module-iot-plugin-common/src/main/java/cn/iocoder/yudao/module/iot/plugin/common/downstream/router/IotDeviceServiceInvokeVertxHandler.java
| ... | ... | @@ -14,6 +14,11 @@ import java.util.Map; |
| 14 | 14 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; |
| 15 | 15 | import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; |
| 16 | 16 | |
| 17 | +/** | |
| 18 | + * IOT 设备服务调用 Vertx Handler | |
| 19 | + * | |
| 20 | + * 芋道源码 | |
| 21 | + */ | |
| 17 | 22 | @Slf4j |
| 18 | 23 | @RequiredArgsConstructor |
| 19 | 24 | public class IotDeviceServiceInvokeVertxHandler implements Handler<RoutingContext> { |
| ... | ... | @@ -26,7 +31,7 @@ public class IotDeviceServiceInvokeVertxHandler implements Handler<RoutingContex |
| 26 | 31 | @SuppressWarnings("unchecked") |
| 27 | 32 | public void handle(RoutingContext routingContext) { |
| 28 | 33 | // 1. 解析参数 |
| 29 | - IotDeviceServiceInvokeReqDTO invokeReqDTO; | |
| 34 | + IotDeviceServiceInvokeReqDTO reqDTO; | |
| 30 | 35 | try { |
| 31 | 36 | String productKey = routingContext.pathParam("productKey"); |
| 32 | 37 | String deviceName = routingContext.pathParam("deviceName"); |
| ... | ... | @@ -34,21 +39,21 @@ public class IotDeviceServiceInvokeVertxHandler implements Handler<RoutingContex |
| 34 | 39 | JsonObject body = routingContext.body().asJsonObject(); |
| 35 | 40 | String requestId = body.getString("requestId"); |
| 36 | 41 | Map<String, Object> params = (Map<String, Object>) body.getMap().get("params"); |
| 37 | - invokeReqDTO = ((IotDeviceServiceInvokeReqDTO) new IotDeviceServiceInvokeReqDTO() | |
| 42 | + reqDTO = ((IotDeviceServiceInvokeReqDTO) new IotDeviceServiceInvokeReqDTO() | |
| 38 | 43 | .setRequestId(requestId).setProductKey(productKey).setDeviceName(deviceName)) |
| 39 | 44 | .setIdentifier(identifier).setParams(params); |
| 40 | 45 | } catch (Exception e) { |
| 41 | - log.error("[handle][解析参数失败]", e); | |
| 46 | + log.error("[handle][路径参数({}) 解析参数失败]", routingContext.pathParams(), e); | |
| 42 | 47 | IotPluginCommonUtils.writeJson(routingContext, CommonResult.error(BAD_REQUEST)); |
| 43 | 48 | return; |
| 44 | 49 | } |
| 45 | 50 | |
| 46 | - // 2. 调用下游处理器 | |
| 51 | + // 2. 调用处理器 | |
| 47 | 52 | try { |
| 48 | - CommonResult<Boolean> result = deviceDownstreamHandler.invokeDeviceService(invokeReqDTO); | |
| 53 | + CommonResult<Boolean> result = deviceDownstreamHandler.invokeDeviceService(reqDTO); | |
| 49 | 54 | IotPluginCommonUtils.writeJson(routingContext, result); |
| 50 | 55 | } catch (Exception e) { |
| 51 | - log.error("[handle][请求参数({}) 服务调用异常]", invokeReqDTO, e); | |
| 56 | + log.error("[handle][请求参数({}) 服务调用异常]", reqDTO, e); | |
| 52 | 57 | IotPluginCommonUtils.writeJson(routingContext, CommonResult.error(INTERNAL_SERVER_ERROR)); |
| 53 | 58 | } |
| 54 | 59 | } | ... | ... |