diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/controller/VideoAlarmController.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/controller/VideoAlarmController.java index 27824edd..07bb40b2 100644 --- a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/controller/VideoAlarmController.java +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/controller/VideoAlarmController.java @@ -32,7 +32,7 @@ public class VideoAlarmController { } @PostMapping("/huawei/callback") - public void huaweiAlarm(@RequestBody String data) { + public void huaweiAlarm(@RequestBody Object data) { log.info("华为上报消息,msg={}", data); } diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/domain/enums/ControlTypeEnum.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/domain/enums/ControlTypeEnum.java index 96b54334..91fdff66 100644 --- a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/domain/enums/ControlTypeEnum.java +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/domain/enums/ControlTypeEnum.java @@ -9,13 +9,13 @@ public enum ControlTypeEnum { */ ACCESS_CONTROL(1), /** - * 远程呼梯 + * 电梯外面面板权限 */ - REMOTE_CALL_ELEVATOR(2), + ELEVATOR_OUT_CONTROL(2), /** - * 梯控 + * 电梯里面的面板 */ - ELEVATOR_CONTROL(3); + ELEVATOR_IN_CONTROL(3); private final Integer code; diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/runner/ElevatorTcpRunner.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/runner/ElevatorTcpRunner.java new file mode 100644 index 00000000..0fdbc566 --- /dev/null +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/runner/ElevatorTcpRunner.java @@ -0,0 +1,40 @@ +package org.dromara.sis.runner; + +import lombok.extern.slf4j.Slf4j; +import org.dromara.sis.sdk.smartDevices.utils.ElevatorControlUtil; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + * @author lsm + * @apiNote ElevatorTcpRunner + * @since 2025/8/8 + */ +@Slf4j +@Component +public class ElevatorTcpRunner implements ApplicationRunner { + + @Override + public void run(ApplicationArguments args) { + // 获取单例实例并启动服务 + ElevatorControlUtil instance = ElevatorControlUtil.getInstance(); + + try{ + // 启动服务 + instance.start(23); + log.info("启动电梯控制服务成功"); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + // 停止服务 + log.info("应用关闭中,停止电梯服务..."); + instance.stop(); + + })); + } catch (IOException e) { + log.info("启动电梯控制服务失败"); + } + } +} diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/ElevatorControlUtil.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/ElevatorControlUtil.java new file mode 100644 index 00000000..1fe653e5 --- /dev/null +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/ElevatorControlUtil.java @@ -0,0 +1,309 @@ +package org.dromara.sis.sdk.smartDevices.utils; + +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @author lsm + * @apiNote ElevatorControlUtil + * @since 2025/8/8 + */ +@Service +public class ElevatorControlUtil { + private static final int FRAME_LENGTH = 10; + private static final byte FIXED_ADDRESS = 0x00; + private static final byte MANUAL_COMMAND = (byte) 0xD1; + private static final byte AUTO_COMMAND = (byte) 0xD2; + private static final byte MANUAL_RESPONSE = (byte) 0xC1; + private static final byte AUTO_RESPONSE = (byte) 0xC2; + private static final byte HEARTBEAT_COMMAND = (byte) 0xD0; + + private static ElevatorControlUtil instance; + + private ServerSocket serverSocket; + private Socket clientSocket; + private InputStream inputStream; + private OutputStream outputStream; + private final Object lock = new Object(); + private ScheduledExecutorService heartbeatScheduler; + private volatile boolean isRunning = false; + + // 私有构造函数 + private ElevatorControlUtil() {} + + /** + * 获取单例实例 + */ + public static synchronized ElevatorControlUtil getInstance() { + if (instance == null) { + instance = new ElevatorControlUtil(); + } + return instance; + } + + /** + * 启动TCP服务 + * @param port 监听端口 + */ + public void start(Integer port) throws IOException { + if (isRunning) { + throw new IllegalStateException("服务已在运行中"); + } + + serverSocket = new ServerSocket(port); + isRunning = true; + + // 启动连接监听线程 + new Thread(this::acceptConnections, "TCP-Acceptor").start(); + System.out.println("电梯控制服务已启动,监听端口: " + port); + } + + /** + * 停止服务 + */ + public void stop() { + isRunning = false; + + // 停止心跳 + if (heartbeatScheduler != null) { + heartbeatScheduler.shutdown(); + } + + // 关闭连接 + closeClientResources(); + + // 关闭服务器 + if (serverSocket != null && !serverSocket.isClosed()) { + try { + serverSocket.close(); + System.out.println("服务已停止"); + } catch (IOException e) { + System.err.println("停止服务时出错: " + e.getMessage()); + } + } + } + + /** + * 接受客户端连接 + */ + private void acceptConnections() { + while (isRunning) { + try { + clientSocket = serverSocket.accept(); + clientSocket.setKeepAlive(true); + inputStream = clientSocket.getInputStream(); + outputStream = clientSocket.getOutputStream(); + + System.out.println("梯控设备已连接: " + clientSocket.getRemoteSocketAddress()); + + // 启动心跳机制 + startHeartbeat(); + + } catch (IOException e) { + if (isRunning) { + System.err.println("接受连接时出错: " + e.getMessage()); + try { + Thread.sleep(5000); // 5秒后重试 + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + } + } + } + + /** + * 启动心跳机制 + */ + private void startHeartbeat() { + if (heartbeatScheduler != null && !heartbeatScheduler.isShutdown()) { + heartbeatScheduler.shutdown(); + } + + heartbeatScheduler = Executors.newSingleThreadScheduledExecutor(); + heartbeatScheduler.scheduleAtFixedRate(() -> { + if (isClientConnected()) { + try { + sendHeartbeat(); + } catch (IOException e) { + System.err.println("心跳包发送失败: " + e.getMessage()); + closeClientResources(); + } + } + }, 30, 30, TimeUnit.SECONDS); + } + + /** + * 发送心跳包 + */ + private void sendHeartbeat() throws IOException { + synchronized (lock) { + if (!isClientConnected()) return; + + byte[] heartbeatData = new byte[7]; + byte[] frame = buildFrame(HEARTBEAT_COMMAND, heartbeatData); + outputStream.write(frame); + outputStream.flush(); + } + } + + /** + * 检查客户端是否连接 + */ + public boolean isClientConnected() { + return clientSocket != null && clientSocket.isConnected() && !clientSocket.isClosed(); + } + + /** + * 发送手动选层命令 + * @param floors 要开放的楼层列表 + * @return 是否成功 + */ + public boolean sendManualCommand(List floors) { + return sendCommand(MANUAL_COMMAND, MANUAL_RESPONSE, generateFloorData(floors)); + } + + /** + * 发送自动选层命令 + * @param floors 要开放的楼层列表 + * @return 是否成功 + */ + public boolean sendAutoCommand(List floors) { + return sendCommand(AUTO_COMMAND, AUTO_RESPONSE, generateFloorData(floors)); + } + + /** + * 发送管理员权限命令(开放所有楼层) + */ + public boolean sendAdminCommand() { + byte[] allFloors = new byte[]{ + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, + (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF + }; + return sendCommand(MANUAL_COMMAND, MANUAL_RESPONSE, allFloors); + } + + private boolean sendCommand(byte command, byte expectedResponse, byte[] floorData) { + if (!isClientConnected()) { + System.err.println("发送命令失败:未连接梯控设备"); + return false; + } + + if (floorData == null || floorData.length != 7) { + System.err.println("发送命令失败:无效的楼层数据"); + return false; + } + + synchronized (lock) { + for (int attempt = 0; attempt < 2; attempt++) { + try { + byte[] frame = buildFrame(command, floorData); + outputStream.write(frame); + outputStream.flush(); + + // 设置100ms超时 + clientSocket.setSoTimeout(100); + + byte[] response = new byte[FRAME_LENGTH]; + int bytesRead = 0; + while (bytesRead < FRAME_LENGTH) { + int read = inputStream.read(response, bytesRead, FRAME_LENGTH - bytesRead); + if (read == -1) { + throw new IOException("连接已关闭"); + } + bytesRead += read; + } + + if (validateResponse(response, expectedResponse)) { + return true; + } + } catch (java.net.SocketTimeoutException e) { + System.err.println("命令响应超时,尝试重试"); + } catch (IOException e) { + System.err.println("发送命令时出错: " + e.getMessage()); + closeClientResources(); + return false; + } + } + return false; + } + } + + /** + * 生成楼层数据 + */ + public byte[] generateFloorData(List floors) { + if (floors == null || floors.isEmpty()) { + return new byte[]{0, 0, 0, 0, 0, 0, 0}; + } + + byte[] data = new byte[7]; + for (int floor : floors) { + if (floor < 1 || floor > 56) continue; + + int index = 56 - floor; + int bytePos = index / 8; + int bitPos = 7 - (index % 8); + + data[bytePos] |= (byte) (1 << bitPos); + } + return data; + } + + private byte[] buildFrame(byte command, byte[] floorData) { + byte[] frame = new byte[FRAME_LENGTH]; + frame[0] = command; + frame[1] = FIXED_ADDRESS; + System.arraycopy(floorData, 0, frame, 2, 7); + + byte checksum = 0; + for (int i = 0; i < 9; i++) { + checksum = (byte) (checksum + frame[i]); + } + frame[9] = checksum; + + return frame; + } + + private boolean validateResponse(byte[] response, byte expectedHeader) { + if (response[0] != expectedHeader) return false; + if (response[1] != FIXED_ADDRESS) return false; + + byte checksum = 0; + for (int i = 0; i < 9; i++) { + checksum = (byte) (checksum + response[i]); + } + + return response[9] == checksum; + } + + /** + * 关闭客户端资源 + */ + private void closeClientResources() { + try { + if (inputStream != null) inputStream.close(); + if (outputStream != null) outputStream.close(); + if (clientSocket != null && !clientSocket.isClosed()) { + clientSocket.close(); + } + System.out.println("客户端连接已关闭"); + } catch (IOException e) { + System.err.println("关闭连接时出错: " + e.getMessage()); + } finally { + clientSocket = null; + inputStream = null; + outputStream = null; + } + } +} diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/SisElevatorInfoServiceImpl.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/SisElevatorInfoServiceImpl.java index b6a2df80..40e58eb1 100644 --- a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/SisElevatorInfoServiceImpl.java +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/SisElevatorInfoServiceImpl.java @@ -70,7 +70,7 @@ public class SisElevatorInfoServiceImpl implements ISisElevatorInfoService { SisElevatorInfoVo.DeviceInfo deviceInfo = new SisElevatorInfoVo.DeviceInfo(); deviceInfo.setDeviceId(item.getDeviceId()); deviceInfo.setDeviceIp(item.getDeviceIp()); - if (Objects.equals(item.getControlType(), ControlTypeEnum.REMOTE_CALL_ELEVATOR.getCode())) { + if (Objects.equals(item.getControlType(), ControlTypeEnum.ELEVATOR_OUT_CONTROL.getCode())) { remoteCallElevatorDeviceId.add(deviceInfo); } else { sisElevatorInfoVo.setElevatorControlDeviceId(deviceInfo); @@ -207,7 +207,7 @@ public class SisElevatorInfoServiceImpl implements ISisElevatorInfoService { ref.setDeviceIp(bo.getElevatorControlDeviceId().getDeviceIp()); ref.setBindId(bo.getElevatorId()); ref.setDeviceFloorId(vo.getFloorId()); - ref.setControlType(ControlTypeEnum.ELEVATOR_CONTROL.getCode()); + ref.setControlType(ControlTypeEnum.ELEVATOR_IN_CONTROL.getCode()); ls.add(ref); } // 远程呼叫 @@ -220,7 +220,7 @@ public class SisElevatorInfoServiceImpl implements ISisElevatorInfoService { ref.setDeviceIp(deviceInfo.getDeviceIp()); ref.setBindId(bo.getElevatorId()); ref.setDeviceFloorId(vo.getFloorId()); - ref.setControlType(ControlTypeEnum.REMOTE_CALL_ELEVATOR.getCode()); + ref.setControlType(ControlTypeEnum.ELEVATOR_OUT_CONTROL.getCode()); ls.add(ref); } } diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/ZeroSensationPassageServiceImpl.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/ZeroSensationPassageServiceImpl.java index e90ed521..b9cc8a98 100644 --- a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/ZeroSensationPassageServiceImpl.java +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/service/impl/ZeroSensationPassageServiceImpl.java @@ -4,10 +4,13 @@ import cn.hutool.core.codec.Base64Encoder; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; +import cn.hutool.core.util.ObjectUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; import org.dromara.property.api.RemoteFloorService; +import org.dromara.property.api.domain.vo.RemoteFloorVo; +import org.dromara.sis.domain.enums.ControlTypeEnum; import org.dromara.sis.domain.enums.RosterTypeEnum; import org.dromara.sis.domain.vo.*; import org.dromara.sis.producer.CleanLiftAuthRocketProducer; @@ -16,9 +19,11 @@ import org.dromara.sis.sdk.e8.domain.accessControl.req.RemoteOpenDoorReq; import org.dromara.sis.sdk.hik.HikApiService; import org.dromara.sis.sdk.huawei.HuaWeiBoxApi; import org.dromara.sis.sdk.huawei.domain.HWResult; +import org.dromara.sis.sdk.smartDevices.utils.ElevatorControlUtil; import org.dromara.sis.service.*; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Objects; @@ -58,7 +63,7 @@ public class ZeroSensationPassageServiceImpl implements IZeroSensationPassageSer if (result.getCode() != 200) { log.info("华为盒子比对失败,msg={}", result.getMessage()); // 产生告警数据 - alarmEventsService.createAlarmRecord(deviceIp, 1, 1, "人脸比对失败", smallImg, bigImg); +// alarmEventsService.createAlarmRecord(deviceIp, 1, 1, "人脸比对失败", smallImg, bigImg); return; } log.info("人脸比对执行完成,耗时:{}ms", interval.intervalMs()); @@ -69,19 +74,19 @@ public class ZeroSensationPassageServiceImpl implements IZeroSensationPassageSer if (authRecord == null) { log.info("人员[{}]没有授权记录,判定为陌生人", person); // 不是内部人员 产生紧急的告警信息 - alarmEventsService.createAlarmRecord(deviceIp, 1, 1, "陌生人员入内", smallImg, bigImg); +// alarmEventsService.createAlarmRecord(deviceIp, 1, 1, "陌生人员入内", smallImg, bigImg); return; - }else { + } else { if (Objects.equals(authRecord.getRosterType(), RosterTypeEnum.BLACK_LIST.getCode())) { log.info("人员[{}]在黑名单中,暂不处理。", person); - alarmEventsService.createAlarmRecord(deviceIp, 3, 1, "黑名单人员入内", smallImg, bigImg); +// alarmEventsService.createAlarmRecord(deviceIp, 3, 1, "黑名单人员入内", smallImg, bigImg); return; } } Date now = new Date(); if (DateUtil.compare(now, authRecord.getEndDate()) > 0) { - alarmEventsService.createAlarmRecord(deviceIp, 3, 1, "人员授权信息已过期", smallImg, bigImg); +// alarmEventsService.createAlarmRecord(deviceIp, 3, 1, "人员授权信息已过期", smallImg, bigImg); log.info("当前人脸已过期,暂不处理。"); return; } @@ -109,12 +114,12 @@ public class ZeroSensationPassageServiceImpl implements IZeroSensationPassageSer return; } // 判断绑定设备类型,走不同的处理方法 - if (item.getControlType() == 1) { // 门禁 + if (Objects.equals(item.getControlType(), ControlTypeEnum.ACCESS_CONTROL.getCode())) { // 门禁 handleAc(item.getDeviceId()); - } else if (item.getControlType() == 2) { // 电梯外面面板权限 - handleEle(item.getDeviceId(), r.getAuthGroupId(), 2, item.getDeviceFloorId()); - } else if (item.getControlType() == 3) { // 电梯里面的面板 - handleEle(item.getDeviceId(), r.getAuthGroupId(), 3, item.getDeviceFloorId()); + } else if (item.getControlType().equals(ControlTypeEnum.ELEVATOR_OUT_CONTROL.getCode())) { // 电梯外面面板权限 + handleEle(item.getDeviceId(), r.getAuthGroupId(), ControlTypeEnum.ELEVATOR_OUT_CONTROL.getCode(), item.getDeviceFloorId()); + } else if (item.getControlType().equals(ControlTypeEnum.ELEVATOR_IN_CONTROL.getCode())) { // 电梯里面的面板 + handleEle(item.getDeviceId(), r.getAuthGroupId(), ControlTypeEnum.ELEVATOR_IN_CONTROL.getCode(), item.getDeviceFloorId()); } else { log.info("设备绑定了未知的控制类型[{}],不处理", item.getControlType()); } @@ -157,13 +162,15 @@ public class ZeroSensationPassageServiceImpl implements IZeroSensationPassageSer // 获取权限组下电梯⇄楼层关联信息 List groupRef = elevatorFloorRefService.queryByAuthGroupId(groupId); + if (ObjectUtil.isEmpty(groupRef)) return; // 取出当前电梯的楼层授权信息 List eleRef = groupRef.stream().filter(o -> Objects.equals(o.getElevatorId(), deviceId)).toList(); + if (ObjectUtil.isEmpty(eleRef)) return; - for (SisElevatorFloorRefVo ref : eleRef){ - if (controlType == 2){ - log.info("开始下发外面版梯控权限...."); + if (Objects.equals(controlType, ControlTypeEnum.ELEVATOR_IN_CONTROL.getCode())) { + log.info("开始下发里面版梯控权限...."); + for (SisElevatorFloorRefVo ref : eleRef) { if (ref.getUpChannel() != null && Objects.equals(ref.getFloorId(), deviceFloorId)) { HikApiService.getInstance().controlGateway(ele.getControlIp(), ref.getUpChannel().intValue(), 2); } @@ -171,12 +178,33 @@ public class ZeroSensationPassageServiceImpl implements IZeroSensationPassageSer if (ref.getDownChannel() != null && Objects.equals(ref.getFloorId(), deviceFloorId)) { HikApiService.getInstance().controlGateway(ele.getControlIp(), ref.getDownChannel().intValue(), 2); } - }else { - log.info("开始下发里面版梯控权限...."); - if (ref.getInChannel() != null && Objects.equals(ref.getFloorId(), deviceFloorId)) { - HikApiService.getInstance().controlGateway(ele.getControlIp(), ref.getInChannel().intValue(), 2); + } + } + + + // 获取当前电梯所在建筑的楼层 + List floorList = remoteFloorService.queryByBuildingId(ele.getBuildingId()); + if (CollUtil.isEmpty(floorList)) return; + + if (Objects.equals(controlType, ControlTypeEnum.ELEVATOR_OUT_CONTROL.getCode())) { + SisElevatorFloorRefVo vo; + List num = new ArrayList<>(); + for (int i = 1; i < floorList.size(); i++) { + int finalI = i; + // 取出权限楼层id与实际楼层id相等的数据 + vo = eleRef.stream().filter(o -> Objects.equals(o.getFloorId(), floorList.get(finalI-1).getId())).findFirst().orElse(null); + // 存在权限楼层,添加到num中,梯控模块从1开始 + if (vo != null) { + num.add(i); } } + if (CollUtil.isEmpty(num)) return; + + if (!ElevatorControlUtil.getInstance().isClientConnected()){ + log.info("梯控模块未连接,请检查梯控模块是否启动"); + return; + } + ElevatorControlUtil.getInstance().sendManualCommand(num); } log.info("梯控下发权限完成");