From 7b816d10f001801cc1cdaa2bc8d443ad191a24cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8C=AB=E7=8C=ABguowen?= <9290821+guowen-the-cat@user.noreply.gitee.com> Date: Fri, 19 Dec 2025 14:16:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=AD=AA=E7=94=9F=E5=90=8E?= =?UTF-8?q?=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/TcpServerController.java | 27 +++ .../com/ruoyi/schedule/ScheduleThread.java | 4 +- .../java/com/ruoyi/tcp/GetDeviceAlarm.java | 147 ++++++++++++++ .../java/com/ruoyi/tcp/GetDeviceSrgvMap.java | 140 +++++++++++++ .../java/com/ruoyi/tcp/GetDeviceSsjMap.java | 188 ++++++++++++++++++ .../java/com/ruoyi/tcp/GetDeviceTsjMap.java | 151 ++++++++++++++ .../main/java/com/ruoyi/tcp/TcpServer.java | 149 ++++++++++++++ .../java/com/ruoyi/tcp/TcpServerManager.java | 87 ++++++++ .../com/ruoyi/tcp/TcpServerProperties.java | 42 ++++ .../ruoyi/tcp/Wcs2wmsgetEmptyCargoSpace.java | 129 ++++++++++++ .../com/ruoyi/tcp/Wcs2wmsgetStockList.java | 58 ++++++ .../src/main/resources/application.yml | 22 ++ ruoyi-ui/src/views/device/ssj/index.vue | 8 +- 13 files changed, 1144 insertions(+), 8 deletions(-) create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/api/controller/TcpServerController.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceAlarm.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSrgvMap.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSsjMap.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceTsjMap.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServer.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerManager.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerProperties.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetEmptyCargoSpace.java create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetStockList.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/api/controller/TcpServerController.java b/ruoyi-admin/src/main/java/com/ruoyi/api/controller/TcpServerController.java new file mode 100644 index 0000000..fafe05a --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/api/controller/TcpServerController.java @@ -0,0 +1,27 @@ +package com.ruoyi.api.controller; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.tcp.TcpServerManager; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/tcp") +public class TcpServerController { + private final TcpServerManager manager; + + public TcpServerController(TcpServerManager manager) { + this.manager = manager; + } + + @GetMapping("/status") + public AjaxResult status() { + return AjaxResult.success(manager.status()); + } + + @PostMapping("/send/{channel}") + public AjaxResult send(@PathVariable String channel, @RequestBody String message) { + boolean ok = manager.broadcast(channel, message); + if (ok) return AjaxResult.success(); + return AjaxResult.error("channel not found"); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java b/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java index dba4b09..ee3fc63 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/schedule/ScheduleThread.java @@ -107,7 +107,7 @@ public class ScheduleThread { ssjList.put(deviceSsj.getDeviceCode(), deviceSsj); } //plc输送机读取线程 - PlcSsjRead plcSsjRead = new PlcSsjRead(ssjList, new PlcConnectInfo("127.0.0.1", 1502)); + PlcSsjRead plcSsjRead = new PlcSsjRead(ssjList, new PlcConnectInfo("172.16.100.9", 1502)); Thread plcSsjReadThread = new Thread(plcSsjRead); plcSsjReadCustomThread = new CustomThread(plcSsjReadThread, plcSsjRead); @@ -118,7 +118,7 @@ public class ScheduleThread { tsjList.put(t.getName(), t); } //plc提升机读取线程 - PlcTsjRead plcTsjRead = new PlcTsjRead(tsjList, new PlcConnectInfo("127.0.0.1", 1502)); + PlcTsjRead plcTsjRead = new PlcTsjRead(tsjList, new PlcConnectInfo("172.16.100.9", 1502)); Thread plcTsjReadThread = new Thread(plcTsjRead); plcTsjReadCustomThread = new CustomThread(plcTsjReadThread, plcTsjRead); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceAlarm.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceAlarm.java new file mode 100644 index 0000000..621b0a8 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceAlarm.java @@ -0,0 +1,147 @@ +package com.ruoyi.tcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.device.srgv.domain.DeviceSrgv; +import com.ruoyi.device.ssj.domain.DeviceSsj; +import com.ruoyi.device.tsj.domain.DeviceTsj; +import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead; +import com.ruoyi.schedule.plcRead.ssj.PlcSsjRead; +import com.ruoyi.schedule.plcRead.tsj.PlcTsjRead; +import lombok.Data; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class GetDeviceAlarm { + private final TcpServerManager manager; + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 构造函数:注入 TCP 管理器,用于管理 5555 端口客户端与广播 + */ + public GetDeviceAlarm(TcpServerManager manager) { + this.manager = manager; + } + + /** + * 初始化:注册 5555 端口的客户端连接监听,客户端第一次连接时立即发送报警数据 + */ + @PostConstruct + public void init() { + manager.setClientListener("port5555", this::handleConnect); + } + + /** + * 客户端连接处理:构建报警数据并写入到 Socket 输出流 + */ + private void handleConnect(Socket socket) { + try { + String payload = buildAlarmPayload(); + OutputStream os = socket.getOutputStream(); + os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8)); + os.flush(); + } catch (Exception ignored) { + } + } + + + /** + * 周期发送:每 2 秒检查 5555 端口是否有连接,有则广播报警数据,无连接不发送 + */ + @Scheduled(fixedRate = 2000) + public void send() { + Integer count = manager.status().getOrDefault("port5555", 0); + if (count != null && count > 0) { + String payload = buildAlarmPayload(); + manager.broadcast("port5555", payload == null ? "" : payload); + } + } + + /** + * 构建报警数据:汇总四向车、输送机、提升机报警 + * - 四向车:报警代码 > 1,字典类型 srgv_alarm + * - 输送机:报警代码 > 2,字典类型 ssj_ex + * - 提升机:报警代码 > 1000,字典类型 tsj_alarm + * 返回 JSON 字符串格式:{"alarm":[{id,name,alarm,alarmMsg}]} + */ + private String buildAlarmPayload() { + List alarms = new ArrayList<>(); + + Map srgvMap = PlcSrgvRead.deviceSrgvMap; + if (srgvMap != null) { + for (DeviceSrgv d : srgvMap.values()) { + Integer code = d.getAlarm(); + if (code != null && code > 1) { + AlarmDto dto = new AlarmDto(); + dto.setId(d.getDeviceId() == null ? "" : String.valueOf(d.getDeviceId())); + dto.setName("四向车"); + dto.setAlarm(code == null ? "" : String.valueOf(code)); + dto.setAlarmMsg(DictUtils.getDictLabel("srgv_alarm", code == null ? "" : String.valueOf(code))); + alarms.add(dto); + } + } + } + + Map ssjMap = PlcSsjRead.deviceSsjMap; + if (ssjMap != null) { + for (DeviceSsj d : ssjMap.values()) { + Integer code = d.getException(); + if (code != null && code > 2) { + AlarmDto dto = new AlarmDto(); + dto.setId(d.getDeviceCode() == null ? "" : d.getDeviceCode()); + dto.setName("输送机"); + dto.setAlarm(String.valueOf(code)); + dto.setAlarmMsg(DictUtils.getDictLabel("ssj_ex", String.valueOf(code))); + alarms.add(dto); + } + } + } + + Map tsjMap = PlcTsjRead.deviceTsjMap; + if (tsjMap != null) { + for (Map.Entry entry : tsjMap.entrySet()) { + String codeId = entry.getKey(); + DeviceTsj d = entry.getValue(); + Integer code = d.getSbzt(); + if (code != null && code > 1000) { + AlarmDto dto = new AlarmDto(); + dto.setId(codeId == null ? "" : codeId); + dto.setName("提升机"); + dto.setAlarm(String.valueOf(code)); + dto.setAlarmMsg(DictUtils.getDictLabel("tsj_alarm", String.valueOf(code))); + alarms.add(dto); + } + } + } + + try { + Wrapper w = new Wrapper(); + w.setAlarm(alarms); + return objectMapper.writeValueAsString(w); + } catch (Exception e) { + return "{\"alarm\":[]}"; + } + } + + @Data + private static class Wrapper { + private List alarm; + } + + @Data + private static class AlarmDto { + private String id; + private String name; + private String alarm; + private String alarmMsg; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSrgvMap.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSrgvMap.java new file mode 100644 index 0000000..c1e6ef4 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSrgvMap.java @@ -0,0 +1,140 @@ +package com.ruoyi.tcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.device.srgv.domain.DeviceSrgv; +import com.ruoyi.schedule.plcRead.srgv.PlcSrgvRead; +import com.ruoyi.job.service.IMydoJobService; +import com.ruoyi.job.domain.MydoJob; +import com.ruoyi.common.utils.DictUtils; +import lombok.Data; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class GetDeviceSrgvMap { + private final TcpServerManager manager; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final IMydoJobService mydoJobService; + + /** + * 构造函数:注入 TCP 管理器与作业服务,用于管理 2222 端口广播并根据任务号补全物料信息 + */ + public GetDeviceSrgvMap(TcpServerManager manager, IMydoJobService mydoJobService) { + this.manager = manager; + this.mydoJobService = mydoJobService; + } + + /** + * 初始化:注册 2222 端口的客户端连接监听,客户端第一次连接时立即发送 AGV 数据 + */ + @PostConstruct + public void init() { + manager.setClientListener("port2222", this::handleConnect); + } + + /** + * 客户端连接处理:构建 AGV 数据并写入到 Socket 输出流 + */ + private void handleConnect(Socket socket) { + try { + String payload = buildAgvPayload(); + OutputStream os = socket.getOutputStream(); + os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8)); + os.flush(); + } catch (Exception ignored) { + } + } + + /** + * 周期发送:每 2 秒检查 2222 端口是否有连接,有则广播 AGV 数据,无连接不发送 + */ + @Scheduled(fixedRate = 2000) + public void send() { + Integer count = manager.status().getOrDefault("port2222", 0); + if (count != null && count > 0) { + String payload = buildAgvPayload(); + manager.broadcast("port2222", payload == null ? "" : payload); + } + } + + /** + * 构建 AGV 数据:从 PLC 缓存读取四向车状态并组装 JSON + * - 计算在位与状态(基于执行阶段 workPhase) + * - 报警码与文案:使用字典 srgv_alarm + * - 当任务号有效时,查询 MydoJob 补齐托盘与物料信息 + * 返回 JSON 字符串格式:{"AGV":[{...}]} + */ + private String buildAgvPayload() { + Map map = PlcSrgvRead.deviceSrgvMap; + List list = new ArrayList<>(); + if (map != null) { + for (DeviceSrgv d : map.values()) { + AgvDto dto = new AgvDto(); + dto.setId(d.getDeviceId() == null ? "" : String.format("%04d", d.getDeviceId())); + dto.setTaskNo(d.getTaskNo() == null ? "" : String.valueOf(d.getTaskNo())); + dto.setIsThere((d.getWorkPhase() >= 3 && d.getWorkPhase() < 6 ? "1" : "0")); + Integer wp = d.getWorkPhase(); + String status = "0"; + if (wp != null) { + if (wp == 3) status = "1"; + else if (wp == 6) status = "2"; + else if (wp == 24) status = "3"; + } + dto.setStatus(status); + dto.setLocation(d.getCurrSite() == null ? "" : d.getCurrSite()); + dto.setAlarm(d.getAlarm() == null ? "" : String.valueOf(d.getAlarm())); + dto.setAlarmMsg(DictUtils.getDictLabel("srgv_alarm", d.getAlarm() == null ? "" : String.valueOf(d.getAlarm()))); + if (d.getTaskNo() != null && d.getTaskNo() > 0) { + try { + MydoJob job = mydoJobService.selectMydoJobByTaskNo(Long.valueOf(d.getTaskNo())); + dto.setTrayCode(job == null || job.getTrayCode() == null ? "" : job.getTrayCode()); + dto.setProductCode(job == null || job.getProductId() == null ? "" : job.getProductId()); + dto.setProductName(job == null || job.getProductName() == null ? "" : job.getProductName()); + } catch (Exception e) { + dto.setTrayCode(""); + dto.setProductCode(""); + dto.setProductName(""); + } + } else { + dto.setTrayCode(""); + dto.setProductCode(""); + dto.setProductName(""); + } + list.add(dto); + } + } + try { + Wrapper w = new Wrapper(); + w.setAGV(list); + return objectMapper.writeValueAsString(w); + } catch (Exception e) { + return "{\"AGV\":[]}"; + } + } + + @Data + private static class Wrapper { + private List AGV; + } + + @Data + private static class AgvDto { + private String id; + private String taskNo; + private String isThere; + private String status; + private String location; + private String alarm; + private String alarmMsg; + private String trayCode; + private String productCode; + private String productName; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSsjMap.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSsjMap.java new file mode 100644 index 0000000..43231e0 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceSsjMap.java @@ -0,0 +1,188 @@ +package com.ruoyi.tcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.device.ssj.domain.DeviceSsj; +import com.ruoyi.schedule.plcRead.ssj.PlcSsjRead; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import com.ruoyi.job.service.IMydoJobService; +import com.ruoyi.job.domain.MydoJob; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import javax.annotation.PostConstruct; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class GetDeviceSsjMap { + private final TcpServerManager manager; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final IMydoJobService mydoJobService; + + /** + * 构造函数:注入 TCP 管理器与作业服务,用于管理 3333 端口广播并根据任务号补全物料信息 + */ + public GetDeviceSsjMap(TcpServerManager manager, IMydoJobService mydoJobService) { + this.manager = manager; + this.mydoJobService = mydoJobService; + } + + /** + * 初始化:注册 3333 端口的客户端连接监听,客户端第一次连接时立即发送输送机数据 + */ + @PostConstruct + public void init() { + manager.setClientListener("port3333", this::handleConnect); + } + + /** + * 客户端连接处理:构建输送机数据并写入到 Socket 输出流 + */ + private void handleConnect(Socket socket) { + try { + String payload = readConveyorsFromCache(); + OutputStream os = socket.getOutputStream(); + os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8)); + os.flush(); + } catch (Exception ignored) { + } + } + + /** + * 周期发送:每 2 秒检查 3333 端口是否有连接,有则广播输送机数据,无连接不发送 + */ + @Scheduled(fixedRate = 2000) + public void send() { + Integer count = manager.status().getOrDefault("port3333", 0); + if (count != null && count > 0) { + String payload = readConveyorsFromCache(); + manager.broadcast("port3333", payload == null ? "" : payload); + } + } + + /** + * 从 PLC 缓存读取输送机状态并组装 JSON + * - 工作模式转换为中文标签 + * - 报警码与文案:使用字典 ssj_ex + * - 当任务号有效时,查询 MydoJob 补齐托盘与物料信息 + * 返回 JSON 字符串格式:{"ssj":[{...}]} + */ + private String readConveyorsFromCache() { + Map map = PlcSsjRead.deviceSsjMap; + List list = new ArrayList<>(); + if (map == null || map.isEmpty()) { + return toJson(list); + } + for (DeviceSsj d : map.values()) { + SsjDto dto = new SsjDto(); + dto.setId(d.getDeviceCode()); + Integer cf = d.getModel(); + String mode = "0"; + if (cf != null) { + if (cf == 1) { + mode = "手动"; + }else if (cf == 2) { + mode = "自动"; + }else { + mode = String.valueOf(cf); + } + } + dto.setWorkingMode(mode); + dto.setTaskNo(String.valueOf(d.getTaskNo())); + dto.setRotationDirection(calcRotationDirection(d.getTaskNo())); + dto.setAlarm(String.valueOf(d.getException())); + dto.setAlarmMsg(DictUtils.getDictLabel("ssj_ex", String.valueOf(d.getException()))); + dto.setDW(String.valueOf(d.getDw())); + dto.setKX(String.valueOf(d.getKx())); + if (d.getTaskNo() > 0) { + try { + MydoJob job = mydoJobService.selectMydoJobByTaskNo(Long.valueOf(d.getTaskNo())); + dto.setTrayCode(job == null || job.getTrayCode() == null ? "" : job.getTrayCode()); + dto.setProductCode(job == null || job.getProductId() == null ? "" : job.getProductId()); + dto.setProductName(job == null || job.getProductName() == null ? "" : job.getProductName()); + } catch (Exception e) { + dto.setTrayCode(""); + dto.setProductCode(""); + dto.setProductName(""); + } + } else { + dto.setTrayCode(""); + dto.setProductCode(""); + dto.setProductName(""); + } + dto.setRunning(""); + list.add(dto); + } + return toJson(list); + } + + /** + * 计算输送机旋转方向:依据作业 inOrOut 字段,>=20 返回 "2",<20 返回 "1" + */ + private String calcRotationDirection(int taskNo) { + try { + if (taskNo <= 0) return "0"; + MydoJob job = mydoJobService.selectMydoJobByTaskNo((long) taskNo); + if (job == null || job.getInOrOut() == null) return "0"; + int val; + try { + val = Integer.parseInt(job.getInOrOut()); + } catch (NumberFormatException e) { + return "0"; + } + return val >= 20 ? "2" : "1"; + } catch (Exception e) { + return "0"; + } + } + + /** + * 列表对象序列化为 JSON 字符串 + */ + private String toJson(List list) { + try { + Wrapper w = new Wrapper(); + w.setSsj(list); + return objectMapper.writeValueAsString(w); + } catch (Exception e) { + return "{\"ssj\":[]}"; + } + } + + public static class Wrapper { + private List ssj; + /** + * 获取输送机列表 + */ + public List getSsj() { return ssj; } + /** + * 设置输送机列表 + */ + public void setSsj(List ssj) { this.ssj = ssj; } + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class SsjDto { + private String id; + private String workingMode; + private String taskNo; + private String rotationDirection; + private String alarm; + private String alarmMsg; + private String DW; + private String KX; + private String running; + private String trayCode; + private String productCode; + private String productName; + + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceTsjMap.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceTsjMap.java new file mode 100644 index 0000000..6e35e04 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/GetDeviceTsjMap.java @@ -0,0 +1,151 @@ +package com.ruoyi.tcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.utils.DictUtils; +import com.ruoyi.device.tsj.domain.DeviceTsj; +import com.ruoyi.job.domain.MydoJob; +import com.ruoyi.job.service.IMydoJobService; +import com.ruoyi.schedule.plcRead.tsj.PlcTsjRead; +import lombok.Data; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class GetDeviceTsjMap { + private final TcpServerManager manager; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final IMydoJobService mydoJobService; + + /** + * 构造函数:注入 TCP 管理器与作业服务,用于管理 4444 端口广播并根据任务号补全物料信息 + */ + public GetDeviceTsjMap(TcpServerManager manager, IMydoJobService mydoJobService) { + this.manager = manager; + this.mydoJobService = mydoJobService; + } + + /** + * 初始化:注册 4444 端口的客户端连接监听,客户端第一次连接时立即发送提升机数据 + */ + @PostConstruct + public void init() { + manager.setClientListener("port4444", this::handleConnect); + } + + /** + * 客户端连接处理:构建提升机数据并写入到 Socket 输出流 + */ + private void handleConnect(Socket socket) { + try { + String payload = buildTsjPayload(); + OutputStream os = socket.getOutputStream(); + os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8)); + os.flush(); + } catch (Exception ignored) { + } + } + + /** + * 周期发送:每 2 秒检查 4444 端口是否有连接,有则广播提升机数据,无连接不发送 + */ + @Scheduled(fixedRate = 2000) + public void send() { + Integer count = manager.status().getOrDefault("port4444", 0); + if (count != null && count > 0) { + String payload = buildTsjPayload(); + manager.broadcast("port4444", payload == null ? "" : payload); + } + } + + /** + * 构建提升机数据:从 PLC 缓存读取提升机状态并组装 JSON + * - 工作模式转换为中文标签 + * - 报警码与文案:使用字典 tsj_alarm + * - 当任务号有效时,查询 MydoJob 补齐托盘与物料信息 + * 返回 JSON 字符串格式:{"tsj":[{...}]} + */ + private String buildTsjPayload() { + Map map = PlcTsjRead.deviceTsjMap; + List list = new ArrayList<>(); + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + String code = entry.getKey(); + DeviceTsj d = entry.getValue(); + TsjDto dto = new TsjDto(); + dto.setId(String.valueOf(d.getId())); + dto.setLayer(d.getDqcs() == null ? "" : String.valueOf(d.getDqcs())); + dto.setTaskNo(d.getRwh() == null ? "" : String.valueOf(d.getRwh())); + dto.setZxjd(d.getJdbz() == null ? "" : String.valueOf(d.getJdbz())); + Integer cf = d.getCzfs(); + String mode = "0"; + if (cf != null) { + if (cf == 1) { + mode = "手动"; + }else if (cf == 2) { + mode = "自动"; + }else { + mode = String.valueOf(cf); + } + } + dto.setWorkingMode(mode); + dto.setSourceLayer(d.getYch() == null ? "" : String.valueOf(d.getYch())); + dto.setTargetLayer(d.getMbch() == null ? "" : String.valueOf(d.getMbch())); + dto.setAlarm(d.getSbzt() == null ? "" : String.valueOf(d.getSbzt())); + dto.setAlarmMsg(DictUtils.getDictLabel("tsj_alarm", d.getSbzt() == null ? "" : String.valueOf(d.getSbzt()))); + if (d.getRwh() != null && d.getRwh() > 0) { + try { + MydoJob job = mydoJobService.selectMydoJobByTaskNo(Long.valueOf(d.getRwh())); + dto.setTrayCode(job == null || job.getTrayCode() == null ? "" : job.getTrayCode()); + dto.setProductCode(job == null || job.getProductId() == null ? "" : job.getProductId()); + dto.setProductName(job == null || job.getProductName() == null ? "" : job.getProductName()); + } catch (Exception e) { + dto.setTrayCode(""); + dto.setProductCode(""); + dto.setProductName(""); + } + } else { + dto.setTrayCode(""); + dto.setProductCode(""); + dto.setProductName(""); + } + list.add(dto); + } + } + try { + Wrapper w = new Wrapper(); + w.setTsj(list); + return objectMapper.writeValueAsString(w); + } catch (Exception e) { + return "{\"tsj\":[]}"; + } + } + + @Data + private static class Wrapper { + private List tsj; + } + + @Data + private static class TsjDto { + private String id; + private String taskNo; + private String zxjd; + private String workingMode; + private String sourceLayer; + private String targetLayer; + private String alarm; + private String alarmMsg; + private String trayCode; + private String productCode; + private String productName; + private String layer; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServer.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServer.java new file mode 100644 index 0000000..142bbc8 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServer.java @@ -0,0 +1,149 @@ +package com.ruoyi.tcp; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +@Slf4j +public class TcpServer implements Runnable { + @Getter + private final String name; + @Getter + private final int port; + private final AtomicBoolean running = new AtomicBoolean(false); + private ServerSocket serverSocket; + private final Set clients = ConcurrentHashMap.newKeySet(); + private final ExecutorService clientPool = Executors.newCachedThreadPool(); + @lombok.Setter + private volatile java.util.function.Consumer clientListener; + + public TcpServer(String name, int port) { + this.name = name; + this.port = port; + } + + /** + * 启动服务器:在守护线程中运行 ServerSocket 接受客户端连接 + */ + public void start() { + if (running.compareAndSet(false, true)) { + Thread t = new Thread(this, "TcpServer-" + name + "-" + port); + t.setDaemon(true); + t.start(); + } + } + + /** + * 停止服务器:关闭 ServerSocket 与所有客户端连接,终止客户端线程池 + */ + public void stop() { + running.set(false); + try { + if (serverSocket != null) serverSocket.close(); + } catch (IOException ignored) { + } + for (Socket s : clients) { + try { + s.close(); + } catch (IOException ignored) { + } + } + clients.clear(); + clientPool.shutdownNow(); + } + + /** + * 获取当前已连接的客户端数量 + */ + public int clientCount() { + return clients.size(); + } + + /** + * 广播消息到所有已连接客户端;写入失败的客户端将被移除 + */ + public void broadcast(String message) { + byte[] bytes = (message + "\n").getBytes(StandardCharsets.UTF_8); + for (Socket s : clients) { + try { + OutputStream os = s.getOutputStream(); + os.write(bytes); + os.flush(); + } catch (IOException e) { + try { + s.close(); + } catch (IOException ignored) { + } + clients.remove(s); + } + } + } + + @Override + /** + * 服务器主循环:绑定端口,阻塞等待新连接并交由客户端处理线程执行 + */ + public void run() { + try { + serverSocket = new ServerSocket(port); + log.info("TCP server {} started on {}", name, port); + while (running.get()) { + Socket socket = serverSocket.accept(); + clients.add(socket); + clientPool.execute(() -> handleClient(socket)); + } + } catch (IOException e) { + if (running.get()) { + log.error("TCP server {} failed on {} {}", name, port, e.getMessage()); + } + } finally { + stop(); + } + } + + /** + * 单个客户端处理: + * - 调用已注册的客户端监听器进行初次处理 + * - 读取客户端输入,支持 ping/pong 心跳应答 + * - 连接结束时关闭资源并移除客户端 + */ + private void handleClient(Socket socket) { + if (clientListener != null) { + try { + clientListener.accept(socket); + } catch (Exception e) { + log.error("Error in client listener for {} on port {}", name, port, e); + } + } + try { + BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null && running.get()) { + if ("ping".equalsIgnoreCase(line.trim())) { + OutputStream os = socket.getOutputStream(); + os.write("pong\n".getBytes(StandardCharsets.UTF_8)); + os.flush(); + } + } + } catch (IOException ignored) { + } finally { + try { + socket.close(); + } catch (IOException ignored) { + } + clients.remove(socket); + } + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerManager.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerManager.java new file mode 100644 index 0000000..08f3f66 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerManager.java @@ -0,0 +1,87 @@ +package com.ruoyi.tcp; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +public class TcpServerManager { + private final TcpServerProperties properties; + private final Map servers = new HashMap<>(); + + /** + * 构造函数:注入 TCP 服务器配置(启用标志与端口映射) + */ + public TcpServerManager(TcpServerProperties properties) { + this.properties = properties; + } + + /** + * 初始化:按配置创建并启动各端口的 TCP 服务器 + */ + @PostConstruct + public void init() { + if (!properties.isEnabled()) return; + for (Map.Entry e : properties.getChannels().entrySet()) { + TcpServer s = new TcpServer(e.getKey(), e.getValue()); + servers.put(e.getKey(), s); + s.start(); + } + log.info("TCP servers initialized {}", properties.getChannels()); + } + + /** + * 销毁:停止所有 TCP 服务器并清理缓存 + */ + @PreDestroy + public void destroy() { + for (TcpServer s : servers.values()) { + s.stop(); + } + servers.clear(); + } + + /** + * 状态查询:返回每个通道当前已连接客户端数量 + */ + public Map status() { + Map map = new HashMap<>(); + for (Map.Entry e : servers.entrySet()) { + map.put(e.getKey(), e.getValue().clientCount()); + } + return map; + } + + /** + * 广播消息:向指定通道的所有客户端发送文本消息 + */ + public boolean broadcast(String channel, String message) { + TcpServer s = servers.get(channel); + if (s == null) return false; + s.broadcast(message); + return true; + } + + /** + * 设置客户端监听器:为指定通道注册客户端连接回调 + */ + public void setClientListener(String channel, java.util.function.Consumer listener) { + TcpServer s = servers.get(channel); + if (s != null) { + s.setClientListener(listener); + } + } + + /** + * 获取服务器实例集合(只读视图) + */ + public Map getServers() { + return Collections.unmodifiableMap(servers); + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerProperties.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerProperties.java new file mode 100644 index 0000000..8c956ba --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/TcpServerProperties.java @@ -0,0 +1,42 @@ +package com.ruoyi.tcp; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.Map; + +@Component +@ConfigurationProperties(prefix = "tcpserver") +public class TcpServerProperties { + private boolean enabled = false; + private Map channels = new LinkedHashMap<>(); + + /** + * 获取是否启用 TCP 服务器功能 + */ + public boolean isEnabled() { + return enabled; + } + + /** + * 设置是否启用 TCP 服务器功能 + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * 获取通道端口映射(通道名 -> 端口号) + */ + public Map getChannels() { + return channels; + } + + /** + * 设置通道端口映射(通道名 -> 端口号) + */ + public void setChannels(Map channels) { + this.channels = channels; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetEmptyCargoSpace.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetEmptyCargoSpace.java new file mode 100644 index 0000000..3f599cc --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetEmptyCargoSpace.java @@ -0,0 +1,129 @@ +package com.ruoyi.tcp; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.common.Constant; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +@Component +@Slf4j +public class Wcs2wmsgetEmptyCargoSpace { + private final TcpServerManager manager; + private final ObjectMapper objectMapper = new ObjectMapper(); + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 构造函数:注入 TCP 管理器,用于在 1111 端口管理客户端与数据发送 + */ + public Wcs2wmsgetEmptyCargoSpace(TcpServerManager manager) { + this.manager = manager; + } + + /** + * 初始化:注册 1111 端口客户端监听,客户端首次连接时触发数据发送 + */ + @PostConstruct + public void init() { + manager.setClientListener("port1111", this::handleConnect); + } + + /** + * 客户端连接处理:调用 WMS 接口获取空货位数据,转换为统一 JSON 并写入到 Socket + */ + private void handleConnect(Socket socket) { + try { + String url = Constant.WMS_BASE_URL + "twin/getEmptyCargoSpace"; + log.info("Fetching WMS data from {}", url); + String wmsResponse = restTemplate.getForObject(url, String.class); + + JsonNode root = objectMapper.readTree(wmsResponse); + List cargoList = new ArrayList<>(); + + // Handle if root is array or wrapped in "data" or "rows" + JsonNode arrayNode = null; + if (root.isArray()) { + arrayNode = root; + } else if (root.has("data") && root.get("data").isArray()) { + arrayNode = root.get("data"); + } else if (root.has("rows") && root.get("rows").isArray()) { + arrayNode = root.get("rows"); + } + + if (arrayNode != null) { + for (JsonNode node : arrayNode) { + cargoList.add(mapToDto(node)); + } + } else { + // If it's a single object, maybe treat as one item? + // Or maybe the format is different. For now, log warning if empty. + log.warn("WMS response is not an array: {}", wmsResponse); + } + + CargoResponse response = new CargoResponse(); + response.setCargo(cargoList); + String json = objectMapper.writeValueAsString(response); + + OutputStream os = socket.getOutputStream(); + os.write((json + "\n").getBytes(StandardCharsets.UTF_8)); + os.flush(); + log.info("Sent WMS data to client on port 1111"); + + } catch (Exception e) { + log.error("Failed to fetch WMS data or send to client", e); + } + } + + /** + * 映射单条 WMS 返回记录为 DTO:优先取目标字段名,兼容 BasCargoSpace 字段名 + */ + private CargoDto mapToDto(JsonNode node) { + CargoDto dto = new CargoDto(); + // Mapping strategy: try user's target keys first, then BasCargoSpace keys + dto.setRow(getText(node, "row", "thePlatoon")); + dto.setColumn(getText(node, "column", "theColumn")); + dto.setLayer(getText(node, "layer", "theFloor")); + dto.setStatus(getText(node, "status")); + dto.setTrayCode(getText(node, "trayCode")); + dto.setProductCode(getText(node, "productCode")); + dto.setProductName(getText(node, "productName")); + return dto; + } + + /** + * 读取节点文本:按给定字段名顺序查找首个存在且非空的值 + */ + private String getText(JsonNode node, String... keys) { + for (String key : keys) { + if (node.has(key) && !node.get(key).isNull()) { + return node.get(key).asText(); + } + } + return ""; + } + + @Data + private static class CargoResponse { + private List cargo; + } + + @Data + private static class CargoDto { + private String row; + private String column; + private String layer; + private String status; + private String trayCode; + private String productCode; + private String productName; + } +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetStockList.java b/ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetStockList.java new file mode 100644 index 0000000..98bb368 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/tcp/Wcs2wmsgetStockList.java @@ -0,0 +1,58 @@ +package com.ruoyi.tcp; + +import com.ruoyi.common.Constant; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import javax.annotation.PostConstruct; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +@Component +public class Wcs2wmsgetStockList { + private final TcpServerManager manager; + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 构造函数:注入 TCP 管理器,用于在 6666 端口管理客户端与数据发送 + */ + public Wcs2wmsgetStockList(TcpServerManager manager) { + this.manager = manager; + } + + /** + * 初始化:注册 6666 端口客户端监听,客户端首次连接时触发库存数据发送 + */ + @PostConstruct + public void init() { + manager.setClientListener("port6666", this::handleConnect); + } + + /** + * 客户端连接处理:调用 WMS 接口获取库存列表并写入到 Socket 输出流 + */ + private void handleConnect(Socket socket) { + try { + String url = Constant.WMS_BASE_URL + "twin/getStockList"; + String payload = restTemplate.getForObject(url, String.class); + OutputStream os = socket.getOutputStream(); + os.write(((payload == null ? "" : payload) + "\n").getBytes(StandardCharsets.UTF_8)); + os.flush(); + } catch (Exception ignored) { + } + } + + /** + * 周期发送:每 10 秒检查 6666 端口是否有连接,有则广播库存数据,无连接不发送 + */ + @Scheduled(fixedRate = 10000) + public void send() { + Integer count = manager.status().getOrDefault("port6666", 0); + if (count != null && count > 0) { + String url = Constant.WMS_BASE_URL + "twin/getStockList"; + String payload = restTemplate.getForObject(url, String.class); + manager.broadcast("port6666", payload == null ? "" : payload); + } + } +} diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index cc6e3c0..4e90c86 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -127,3 +127,25 @@ xss: excludes: /system/notice # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/* + +# TCP服务器配置 +tcpserver: +# 是否开启TCP服务器 + enabled: true + # 通道配置 + channels: + status: 9000 + events: 9001 + alarms: 9002 + # 货位与库存查询端口 + port1111: 1111 + # 数据分析端口 + port6666: 6666 + # Srgv端口 + port2222: 2222 + # ssj端口 + port3333: 3333 + # tsj端口 + port4444: 4444 + # 报警端口 + port5555: 5555 diff --git a/ruoyi-ui/src/views/device/ssj/index.vue b/ruoyi-ui/src/views/device/ssj/index.vue index 6812631..2a37b00 100644 --- a/ruoyi-ui/src/views/device/ssj/index.vue +++ b/ruoyi-ui/src/views/device/ssj/index.vue @@ -94,7 +94,7 @@ import { selectDictLabel } from "@/utils/ruoyi" export default { name: "deviceMonitor", - dicts: ['srgv_work_phase','srgv_work_mode','srgv_status','srgv_err_msg','srgv_alarm','srgv_task_type','ssj_dw','ssj_kx','ssj_ex','tsj_zxjd'], + dicts: ['srgv_work_phase','srgv_work_mode','srgv_status','srgv_err_msg','srgv_alarm','srgv_task_type','ssj_dw','ssj_kx','ssj_ex','tsj_zxjd','tsj_alarm'], data() { return { imageUrl1: ssjImg, @@ -178,15 +178,11 @@ export default { { key: 'rwh', label: '任务号', value: d.rwh }, { key: 'yxxf', label: '允许下发', value: d.yxxf }, { key: 'dqcs', label: '当前层数', value: d.dqcs }, - { key: 'yph', label: '原排', value: d.yph }, - { key: 'ylh', label: '原列', value: d.ylh }, { key: 'ych', label: '原层', value: d.ych }, - { key: 'mbph', label: '目标排', value: d.mbph }, - { key: 'mblh', label: '目标列', value: d.mblh }, { key: 'mbch', label: '目标层', value: d.mbch }, { key: 'hczt', label: '载货状态', value: d.hczt }, { key: 'xtxh', label: '心跳信号', value: d.xtxh }, - { key: 'sbzt', label: '设备状态', value: d.sbzt }, + { key: 'sbzt', label: '设备状态', value: d.sbzt, dict: 'tsj_alarm' }, { key: 'jdbz', label: '执行阶段', value: d.jdbz, dict: 'tsj_zxjd' } ] }