#
Junjie
4 天以前 2f9849905dbb8d65faa28628a40084708a0386ef
#
4个文件已添加
25个文件已修改
816 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/BasDualCrnpController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasDualCrnpErrController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasDualCrnpErrLogController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasDualCrnpOptController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasMapController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/ConsoleController.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/DualCrnController.java 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/param/DualCrnCommandParam.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/domain/vo/DualCrnStateTableVo.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/cache/MessageQueue.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/model/protocol/CrnProtocol.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/model/protocol/DualCrnProtocol.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/model/protocol/RgvProtocol.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/model/protocol/StationProtocol.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/ZyStationConnectDriver.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/api/ZyStationConnectApi.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyDualCrnFakeConnect.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/fake/ZyStationFakeConnect.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/network/real/ZyStationRealConnect.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/StationThread.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/core/thread/impl/ZyStationThread.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/DevpCard.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/MapCanvas.js 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/WatchCrnCard.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/WatchDualCrnCard.js 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/components/WatchRgvCard.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/watch/console.html 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/BasDualCrnpController.java
@@ -78,8 +78,8 @@
    @RequestMapping(value = "/basDualCrnp/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam(value="ids[]") Long[] ids){
         for (Long id : ids){
    public R delete(@RequestParam(value="ids[]") Integer[] ids){
         for (Integer id : ids){
            basDualCrnpService.deleteById(id);
        }
        return R.ok();
src/main/java/com/zy/asrs/controller/BasDualCrnpErrController.java
@@ -78,8 +78,8 @@
    @RequestMapping(value = "/basDualCrnpErr/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam(value="ids[]") Long[] ids){
         for (Long id : ids){
    public R delete(@RequestParam(value="ids[]") Integer[] ids){
         for (Integer id : ids){
            basDualCrnpErrService.deleteById(id);
        }
        return R.ok();
src/main/java/com/zy/asrs/controller/BasDualCrnpErrLogController.java
@@ -78,8 +78,8 @@
    @RequestMapping(value = "/basDualCrnpErrLog/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam(value="ids[]") Long[] ids){
         for (Long id : ids){
    public R delete(@RequestParam(value="ids[]") Integer[] ids){
         for (Integer id : ids){
            basDualCrnpErrLogService.deleteById(id);
        }
        return R.ok();
src/main/java/com/zy/asrs/controller/BasDualCrnpOptController.java
@@ -78,8 +78,8 @@
    @RequestMapping(value = "/basDualCrnpOpt/delete/auth")
    @ManagerAuth
    public R delete(@RequestParam(value="ids[]") Long[] ids){
         for (Long id : ids){
    public R delete(@RequestParam(value="ids[]") Integer[] ids){
         for (Integer id : ids){
            basDualCrnpOptService.deleteById(id);
        }
        return R.ok();
src/main/java/com/zy/asrs/controller/BasMapController.java
@@ -198,6 +198,9 @@
                    }else if (nodeType.equals("RGB(255,192,0)")) {
                        //堆垛机
                        nodeData.put("type", "crn");
                    }else if (nodeType.equals("RGB(255,255,0)")) {
                        //双工位堆垛机
                        nodeData.put("type", "dualCrn");
                    }else if (nodeType.equals("RGB(0,112,192)")) {
                        //输送线
                        nodeData.put("type", "devp");
src/main/java/com/zy/asrs/controller/ConsoleController.java
@@ -26,9 +26,11 @@
import com.zy.core.enums.SlaveType;
import com.zy.core.enums.WrkIoType;
import com.zy.core.model.protocol.CrnProtocol;
import com.zy.core.model.protocol.DualCrnProtocol;
import com.zy.core.model.protocol.StationProtocol;
import com.zy.core.properties.SystemProperties;
import com.zy.core.thread.CrnThread;
import com.zy.core.thread.DualCrnThread;
import com.zy.core.thread.StationThread;
import com.zy.core.thread.RgvThread;
import com.zy.core.model.protocol.RgvProtocol;
@@ -205,6 +207,39 @@
        return R.ok().add(vos);
    }
    @PostMapping("/latest/data/dualcrn")
    @ManagerAuth(memo = "双工位堆垛机实时数据")
    public R dualCrnLatestData() {
        List<CrnLatestDataVo> vos = new ArrayList<>();
        List<DeviceConfig> dualCrnList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>()
                .eq("device_type", String.valueOf(SlaveType.DualCrn)));
        for (DeviceConfig deviceConfig : dualCrnList) {
            DualCrnThread crnThread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, deviceConfig.getDeviceNo());
            if (crnThread == null) { continue; }
            DualCrnProtocol p = crnThread.getStatus();
            if (p == null) { continue; }
            CrnLatestDataVo vo = new CrnLatestDataVo();
            vo.setCrnId(p.getCrnNo());
            Integer bay = p.getBay() != null ? p.getBay() : p.getBayTwo();
            vo.setOffset(bay == null ? 0.0 : Double.valueOf(bay));
            vo.setBay(bay);
            Integer taskNo = (p.getTaskNo() != null && p.getTaskNo() > 0) ? p.getTaskNo()
                    : ((p.getTaskNoTwo() != null && p.getTaskNoTwo() > 0) ? p.getTaskNoTwo() : 0);
            vo.setTaskNo(taskNo);
            if (p.getAlarm() != null && p.getAlarm() > 0) {
                vo.setCrnStatus(CrnStatusType.MACHINE_ERROR);
            } else {
                if (taskNo != null && taskNo > 0) {
                    vo.setCrnStatus(p.getModeType() == CrnModeType.AUTO ? CrnStatusType.MACHINE_AUTO : CrnStatusType.MACHINE_UN_AUTO);
                } else {
                    vo.setCrnStatus(p.getModeType() == CrnModeType.AUTO ? CrnStatusType.MACHINE_AUTO : CrnStatusType.MACHINE_UN_AUTO);
                }
            }
            vos.add(vo);
        }
        return R.ok().add(vos);
    }
    @PostMapping("/latest/data/rgv")
    @ManagerAuth(memo = "RGV实时数据")
    public R rgvLatestData(){
src/main/java/com/zy/asrs/controller/DualCrnController.java
New file
@@ -0,0 +1,145 @@
package com.zy.asrs.controller;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.annotations.ManagerAuth;
import com.core.common.Cools;
import com.core.common.R;
import com.zy.asrs.domain.param.DualCrnCommandParam;
import com.zy.asrs.domain.vo.DualCrnStateTableVo;
import com.zy.asrs.entity.BasDualCrnp;
import com.zy.asrs.service.BasDualCrnpService;
import com.zy.core.cache.MessageQueue;
import com.zy.core.cache.SlaveConnection;
import com.zy.core.enums.SlaveType;
import com.zy.core.model.Task;
import com.zy.core.model.command.DualCrnCommand;
import com.zy.core.model.protocol.DualCrnProtocol;
import com.zy.core.thread.DualCrnThread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestController
public class DualCrnController {
    @Autowired
    private BasDualCrnpService basDualCrnpService;
    @PostMapping("/dualcrn/table/crn/state")
    @ManagerAuth(memo = "双工位堆垛机信息表")
    public R dualCrnStateTable() {
        List<DualCrnStateTableVo> list = new ArrayList<>();
        List<BasDualCrnp> crnps = basDualCrnpService.selectList(new EntityWrapper<BasDualCrnp>().orderBy("crn_no"));
        for (BasDualCrnp basDualCrnp : crnps) {
            DualCrnStateTableVo vo = new DualCrnStateTableVo();
            vo.setCrnNo(basDualCrnp.getCrnNo());
            list.add(vo);
            DualCrnThread crnThread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, basDualCrnp.getCrnNo());
            if (crnThread == null) {
                continue;
            }
            DualCrnProtocol p = crnThread.getStatus();
            if (p == null) {
                continue;
            }
            vo.setTaskNo(p.getTaskNo());
            vo.setTaskNoTwo(p.getTaskNoTwo());
            vo.setMode(p.getModeType() == null ? "-" : p.getModeType().desc);
            vo.setStatus(p.getStatusType() == null ? "-" : p.getStatusType().desc);
            vo.setStatusTwo(p.getStatusTypeTwo() == null ? "-" : p.getStatusTypeTwo().desc);
            vo.setLoading(p.getLoaded() != null && p.getLoaded() == 1 ? "有物" : "无物");
            vo.setLoadingTwo(p.getLoadedTwo() != null && p.getLoadedTwo() == 1 ? "有物" : "无物");
            vo.setBay(p.getBay());
            vo.setBayTwo(p.getBayTwo());
            vo.setLev(p.getLevel());
            vo.setLevTwo(p.getLevelTwo());
            vo.setForkOffset(p.getForkPosType() == null ? "-" : p.getForkPosType().desc);
            vo.setForkOffsetTwo(p.getForkPosTypeTwo() == null ? "-" : p.getForkPosTypeTwo().desc);
            vo.setLiftPos(p.getLiftPosType() == null ? "-" : p.getLiftPosType().desc);
            vo.setLiftPosTwo(p.getLiftPosTypeTwo() == null ? "-" : p.getLiftPosTypeTwo().desc);
            vo.setWalkPos(p.getWalkPos() != null && p.getWalkPos() == 0 ? "在定位" : "不在定位");
            vo.setWalkPosTwo(p.getWalkPosTwo() != null && p.getWalkPosTwo() == 0 ? "在定位" : "不在定位");
            vo.setXspeed(p.getXSpeed());
            vo.setYspeed(p.getYSpeed());
            vo.setZspeed(p.getZSpeed());
            vo.setXdistance(p.getXDistance());
            vo.setYdistance(p.getYDistance());
            vo.setXduration(p.getXDuration());
            vo.setYduration(p.getYDuration());
            vo.setWarnCode(p.getAlarm() == null ? "-" : String.valueOf(p.getAlarm()));
            if (p.getAlarm() != null && p.getAlarm() > 0) {
                vo.setDeviceStatus("ERROR");
            } else if (p.getTaskNo() != null && p.getTaskNo() > 0 || p.getTaskNoTwo() != null && p.getTaskNoTwo() > 0) {
                vo.setDeviceStatus("WORKING");
            } else if (p.getModeType() != null && "AUTO".equals(p.getModeType().name())) {
                vo.setDeviceStatus("AUTO");
            } else {
                vo.setDeviceStatus("OFFLINE");
            }
        }
        return R.ok().add(list);
    }
    @PostMapping("/dualcrn/command/take")
    @ManagerAuth(memo = "双工位堆垛机取放货命令")
    public R dualCrnCommandTake(@RequestBody DualCrnCommandParam param) {
        if (Cools.isEmpty(param)) {
            return R.error("缺少参数");
        }
        Integer crnNo = param.getCrnNo();
        String sourceLocNo = param.getSourceLocNo();
        String targetLocNo = param.getTargetLocNo();
        Integer station = param.getStation();
        DualCrnThread crnThread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, crnNo);
        if (crnThread == null) {
            return R.error("线程不存在");
        }
        DualCrnCommand command = crnThread.getPickAndPutCommand(sourceLocNo, targetLocNo, 9999, crnNo, station);
        MessageQueue.offer(SlaveType.DualCrn, crnNo, new Task(2, command));
        return R.ok();
    }
    @PostMapping("/dualcrn/command/move")
    @ManagerAuth(memo = "双工位堆垛机移动命令")
    public R dualCrnCommandMove(@RequestBody DualCrnCommandParam param) {
        if (Cools.isEmpty(param)) {
            return R.error("缺少参数");
        }
        Integer crnNo = param.getCrnNo();
        String targetLocNo = param.getTargetLocNo();
        Integer station = param.getStation();
        DualCrnThread crnThread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, crnNo);
        if (crnThread == null) {
            return R.error("线程不存在");
        }
        DualCrnCommand command = crnThread.getMoveCommand(targetLocNo, 9999, crnNo);
        if (station != null) {
            command.setStation(station.shortValue());
        }
        MessageQueue.offer(SlaveType.DualCrn, crnNo, new Task(2, command));
        return R.ok();
    }
    @PostMapping("/dualcrn/command/taskComplete")
    @ManagerAuth(memo = "双工位堆垛机任务完成确认")
    public R dualCrnCommandTaskComplete(@RequestBody DualCrnCommandParam param) {
        if (Cools.isEmpty(param)) {
            return R.error("缺少参数");
        }
        Integer crnNo = param.getCrnNo();
        Integer station = param.getStation();
        DualCrnThread crnThread = (DualCrnThread) SlaveConnection.get(SlaveType.DualCrn, crnNo);
        if (crnThread == null) {
            return R.error("线程不存在");
        }
        DualCrnCommand command = crnThread.getResetCommand(crnNo, station);
        MessageQueue.offer(SlaveType.DualCrn, crnNo, new Task(2, command));
        return R.ok();
    }
}
src/main/java/com/zy/asrs/domain/param/DualCrnCommandParam.java
New file
@@ -0,0 +1,11 @@
package com.zy.asrs.domain.param;
import lombok.Data;
@Data
public class DualCrnCommandParam {
    private Integer crnNo;
    private String sourceLocNo;
    private String targetLocNo;
    private Integer station;
}
src/main/java/com/zy/asrs/domain/vo/DualCrnStateTableVo.java
New file
@@ -0,0 +1,61 @@
package com.zy.asrs.domain.vo;
import lombok.Data;
@Data
public class DualCrnStateTableVo {
    private Integer crnNo;
    private String mode = "-";
    private String warnCode = "-";
    private Integer taskNo = 0;
    private Integer taskNoTwo = 0;
    private String status = "-";
    private String statusTwo = "-";
    private String loading = "-";
    private String loadingTwo = "-";
    private Integer bay;
    private Integer bayTwo;
    private Integer lev;
    private Integer levTwo;
    private String forkOffset = "-";
    private String forkOffsetTwo = "-";
    private String liftPos = "-";
    private String liftPosTwo = "-";
    private String walkPos = "-";
    private String walkPosTwo = "-";
    private Integer xspeed = 0;
    private Integer yspeed = 0;
    private Integer zspeed = 0;
    private Integer xdistance = 0;
    private Integer ydistance = 0;
    private Integer xduration = 0;
    private Integer yduration = 0;
    private String deviceStatus = "OFFLINE";
}
src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
@@ -103,10 +103,15 @@
            } else if ("/console/latest/data/rgv".equals(url)) {
                ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
                resObj = consoleController.rgvLatestData();
            } else if ("/console/latest/data/dualcrn".equals(url)) {
                ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
                resObj = consoleController.dualCrnLatestData();
            } else if ("/crn/table/crn/state".equals(url)) {
                resObj = SpringUtils.getBean(CrnController.class).crnStateTable();
            } else if ("/rgv/table/rgv/state".equals(url)) {
                resObj = SpringUtils.getBean(RgvController.class).rgvStateTable();
            } else if ("/dualcrn/table/crn/state".equals(url)) {
                resObj = SpringUtils.getBean(com.zy.asrs.controller.DualCrnController.class).dualCrnStateTable();
            }
        } catch (Exception e) {
            R err = R.error(e.getMessage());
src/main/java/com/zy/core/cache/MessageQueue.java
@@ -16,6 +16,8 @@
    // 堆垛机mq交换机
    private static final Map<Integer, LinkedBlockingQueue<Task>> CRN_EXCHANGE = new ConcurrentHashMap<>();
    // 双工位堆垛机mq交换机
    private static final Map<Integer, LinkedBlockingQueue<Task>> DUAL_CRN_EXCHANGE = new ConcurrentHashMap<>();
    // 输送线mq交换机
    private static final Map<Integer, ConcurrentLinkedQueue<Task>> DEVP_EXCHANGE = new ConcurrentHashMap<>();
    // 条码扫描仪mq交换机
@@ -34,6 +36,9 @@
        switch (type) {
            case Crn:
                CRN_EXCHANGE.put(id, new LinkedBlockingQueue<>(1));
                break;
            case DualCrn:
                DUAL_CRN_EXCHANGE.put(id, new LinkedBlockingQueue<>(1));
                break;
            case Rgv:
                RGV_EXCHANGE.put(id, new LinkedBlockingQueue<>(1));
@@ -63,6 +68,8 @@
        switch (type) {
            case Crn:
                return CRN_EXCHANGE.get(id).offer(task);
            case DualCrn:
                return DUAL_CRN_EXCHANGE.get(id).offer(task);
            case Rgv:
                return RGV_EXCHANGE.get(id).offer(task);
            case Devp:
@@ -86,6 +93,8 @@
        switch (type) {
            case Crn:
                return CRN_EXCHANGE.get(id).poll();
            case DualCrn:
                return DUAL_CRN_EXCHANGE.get(id).poll();
            case Rgv:
                return RGV_EXCHANGE.get(id).poll();
            case Devp:
@@ -108,6 +117,8 @@
        switch (type) {
            case Crn:
                return CRN_EXCHANGE.get(id).peek();
            case DualCrn:
                return DUAL_CRN_EXCHANGE.get(id).peek();
            case Rgv:
                return RGV_EXCHANGE.get(id).peek();
            case Devp:
@@ -128,6 +139,9 @@
            case Crn:
                CRN_EXCHANGE.get(id).clear();
                break;
            case DualCrn:
                DUAL_CRN_EXCHANGE.get(id).clear();
                break;
            case Rgv:
                RGV_EXCHANGE.get(id).clear();
                break;
src/main/java/com/zy/core/model/protocol/CrnProtocol.java
@@ -6,6 +6,8 @@
import com.zy.core.enums.CrnStatusType;
import lombok.Data;
import java.util.Map;
@Data
public class CrnProtocol {
@@ -147,6 +149,16 @@
    private Integer crnLane = 1;
    /**
     * 称重数据
     */
    private Double weight;
    /**
     * 条码数据
     */
    private String barcode;
    /**
     * 日志采集时间
     */
    private Long deviceDataLog = System.currentTimeMillis();
@@ -156,6 +168,11 @@
     */
    private Long lastCommandTime = System.currentTimeMillis();
    /**
     * 扩展数据
     */
    private Map<String, Object> extend;
    public void setMode(Integer mode) {
        this.mode = mode;
        this.modeType = CrnModeType.get(mode);
src/main/java/com/zy/core/model/protocol/DualCrnProtocol.java
@@ -6,6 +6,8 @@
import com.zy.core.enums.CrnStatusType;
import lombok.Data;
import java.util.Map;
@Data
public class DualCrnProtocol {
@@ -199,6 +201,11 @@
     */
    private Long lastCommandTime = System.currentTimeMillis();
    /**
     * 扩展数据
     */
    private Map<String, Object> extend;
    public void setMode(Integer mode) {
        this.mode = mode;
        this.modeType = CrnModeType.get(mode);
@@ -214,9 +221,19 @@
        this.forkPosType = CrnForkPosType.get(forkPos);
    }
    public void setForkPosTwo(Integer forkPosTwo) {
        this.forkPosTwo = forkPosTwo;
        this.forkPosTypeTwo = CrnForkPosType.get(forkPosTwo);
    }
    public void setForkPos(CrnForkPosType type) {
        this.forkPosType = type;
        this.forkPos = CrnForkPosType.get(type).id;
    }
    public void setForkPosTwo(CrnForkPosType type) {
        this.forkPosTypeTwo = type;
        this.forkPosTwo = CrnForkPosType.get(type).id;
    }
    public void setLiftPos(Integer liftPos) {
@@ -224,9 +241,19 @@
        this.liftPosType = CrnLiftPosType.get(liftPos);
    }
    public void setLiftPosTwo(Integer liftPosTwo) {
        this.liftPosTwo = liftPosTwo;
        this.liftPosTypeTwo = CrnLiftPosType.get(liftPosTwo);
    }
    public void setLiftPos(CrnLiftPosType type) {
        this.liftPosType = type;
        this.liftPos = CrnLiftPosType.get(type).id;
    }
    public void setLiftPosTwo(CrnLiftPosType type) {
        this.liftPosTypeTwo = type;
        this.liftPosTwo = CrnLiftPosType.get(type).id;
    }
    public void setStatus(Integer status){
@@ -239,4 +266,14 @@
        this.status = CrnStatusType.get(type).id;
    }
    public void setStatusTwo(Integer statusTwo){
        this.statusTwo = statusTwo;
        this.statusTypeTwo = CrnStatusType.get(statusTwo);
    }
    public void setStatusTwo(CrnStatusType type){
        this.statusTypeTwo = type;
        this.statusTwo = CrnStatusType.get(type).id;
    }
}
src/main/java/com/zy/core/model/protocol/RgvProtocol.java
@@ -2,6 +2,8 @@
import lombok.Data;
import java.util.Map;
@Data
public class RgvProtocol {
@@ -22,4 +24,9 @@
    private Long deviceDataLog = System.currentTimeMillis();
    
    private Long lastCommandTime = System.currentTimeMillis();
    /**
     * 扩展数据
     */
    private Map<String, Object> extend;
}
src/main/java/com/zy/core/model/protocol/StationProtocol.java
@@ -2,6 +2,8 @@
import lombok.Data;
import java.util.Map;
@Data
public class StationProtocol {
@@ -50,6 +52,11 @@
    //重量
    private Double weight;
   /**
    * 扩展数据
    */
   private Map<String, Object> extend;
    @Override
    public StationProtocol clone() {
        try {
src/main/java/com/zy/core/network/ZyStationConnectDriver.java
@@ -104,4 +104,12 @@
    public CommandResponse sendCommand(StationCommand command) {
        return zyStationConnectApi.sendCommand(deviceConfig.getDeviceNo(), command);
    }
    public CommandResponse sendOriginCommand(String address, short[] data) {
        return zyStationConnectApi.sendOriginCommand(address, data);
    }
    public byte[] readOriginCommand(String address, int length) {
        return zyStationConnectApi.readOriginCommand(address, length);
    }
}
src/main/java/com/zy/core/network/api/ZyStationConnectApi.java
@@ -16,4 +16,7 @@
    CommandResponse sendCommand(Integer deviceNo, StationCommand command);//下发命令
    CommandResponse sendOriginCommand(String address, short[] data);//原始命令
    byte[] readOriginCommand(String address, int length);//读取原始数据
}
src/main/java/com/zy/core/network/fake/ZyDualCrnFakeConnect.java
@@ -57,8 +57,13 @@
    }
    private void commandTaskComplete(DualCrnCommand command) {
        if(command.getStation() == 1) {
        this.crnStatus.setTaskNo(0);
        this.crnStatus.setStatus(CrnStatusType.IDLE.id);
        }else {
            this.crnStatus.setTaskNoTwo(0);
            this.crnStatus.setStatusTwo(CrnStatusType.IDLE.id);
        }
    }
    private void commandMove(DualCrnCommand command) {
src/main/java/com/zy/core/network/fake/ZyStationFakeConnect.java
@@ -101,6 +101,16 @@
        return new CommandResponse(true, "命令已受理(异步执行)");
    }
    @Override
    public CommandResponse sendOriginCommand(String address, short[] data) {
        return new CommandResponse(true, "原始命令已受理(异步执行)");
    }
    @Override
    public byte[] readOriginCommand(String address, int length) {
        return new byte[0];
    }
    private void handleCommand(Integer deviceNo, StationCommand command) {
        News.info("[WCS Debug] 站点仿真模拟已启动,命令数据={}", JSON.toJSONString(command));
        Integer taskNo = command.getTaskNo();
src/main/java/com/zy/core/network/real/ZyStationRealConnect.java
@@ -186,6 +186,34 @@
        return commandResponse;
    }
    @Override
    public CommandResponse sendOriginCommand(String address, short[] data) {
        CommandResponse commandResponse = new CommandResponse(false);
        if (null == data || data.length == 0) {
            commandResponse.setMessage("数据为空");
            return commandResponse;
        }
        OperateResult write = siemensNet.Write(address, data);
        if (write.IsSuccess) {
            log.info("写入原始命令成功。地址={},数据={}", address, JSON.toJSON(data));
            commandResponse.setResult(true);
        } else {
            log.error("写入原始命令失败。地址={},数据={}", address, JSON.toJSON(data));
            commandResponse.setResult(false);
        }
        return commandResponse;
    }
    @Override
    public byte[] readOriginCommand(String address, int length) {
        OperateResultExOne<byte[]> result = siemensNet.Read(address, (short) length);
        if (result.IsSuccess) {
            return result.Content;
        }
        return new byte[0];
    }
    private ZyStationStatusEntity findStatusEntity(Integer stationId) {
        for (ZyStationStatusEntity statusEntity : statusList) {
            if (statusEntity.getStationId().equals(stationId)) {
src/main/java/com/zy/core/thread/StationThread.java
@@ -17,4 +17,8 @@
    CommandResponse sendCommand(StationCommand command);
    CommandResponse sendOriginCommand(String address, short[] data);
    byte[] readOriginCommand(String address, int length);
}
src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java
@@ -129,6 +129,7 @@
        crnProtocol.setForkPos(-1);
        crnProtocol.setLoaded(0);
        crnProtocol.setWalkPos(0);
        crnProtocol.setLiftPos(0);
        //工位2
        crnProtocol.setTaskNoTwo(0);
@@ -138,6 +139,7 @@
        crnProtocol.setForkPosTwo(-1);
        crnProtocol.setLoadedTwo(0);
        crnProtocol.setWalkPosTwo(0);
        crnProtocol.setLiftPosTwo(0);
        crnProtocol.setAlarm(0);
        crnProtocol.setXSpeed(0);
@@ -177,6 +179,7 @@
        crnProtocol.setForkPos(crnStatus.getForkPos());
        crnProtocol.setLoaded(crnStatus.getLoaded());
        crnProtocol.setWalkPos(crnStatus.getWalkPos());
        crnProtocol.setLiftPos(crnStatus.getLiftPos());
        //工位2
        crnProtocol.setTaskNoTwo(crnStatus.getTaskNoTwo());
@@ -186,6 +189,7 @@
        crnProtocol.setForkPosTwo(crnStatus.getForkPosTwo());
        crnProtocol.setLoadedTwo(crnStatus.getLoadedTwo());
        crnProtocol.setWalkPosTwo(crnStatus.getWalkPosTwo());
        crnProtocol.setLiftPosTwo(crnStatus.getLiftPosTwo());
        crnProtocol.setAlarm(crnStatus.getAlarm());
        crnProtocol.setTemp1(crnStatus.getTemp1());
src/main/java/com/zy/core/thread/impl/ZyStationThread.java
@@ -266,4 +266,14 @@
        }
        return map;
    }
    @Override
    public CommandResponse sendOriginCommand(String address, short[] data) {
        return zyStationConnectDriver.sendOriginCommand(address, data);
    }
    @Override
    public byte[] readOriginCommand(String address, int length) {
        return zyStationConnectDriver.readOriginCommand(address, length);
    }
}
src/main/webapp/components/DevpCard.js
@@ -43,6 +43,7 @@
                <el-descriptions-item label="托盘高度">{{ item.palletHeight }}</el-descriptions-item>
                <el-descriptions-item label="条码">{{ item.barcode }}</el-descriptions-item>
                <el-descriptions-item label="故障代码">{{ item.error }}</el-descriptions-item>
                <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item>
            </el-descriptions>
            </el-collapse-item>
          </el-collapse>
src/main/webapp/components/MapCanvas.js
@@ -18,6 +18,7 @@
      pixiStageList: [],
      pixiStaMap: new Map(),
      pixiCrnMap: new Map(),
      pixiDualCrnMap: new Map(),
      pixiRgvMap: new Map(),
      pixiShelfMap: new Map(),
      pixiTrackMap: new Map(),
@@ -27,6 +28,7 @@
      pixiCrnColorTextureMap: new Map(),
      pixiRgvColorTextureMap: new Map(),
      crnList: [],
      dualCrnList: [],
      rgvList: [],
      objectsContainer: null,
      objectsContainer2: null,
@@ -56,6 +58,7 @@
    this.timer = setInterval(() => {
      this.getCrnInfo();
      this.getDualCrnInfo();
      this.getSiteInfo();
      this.getRgvInfo();
    }, 1000);
@@ -208,8 +211,10 @@
      if (this.tracksContainer) { this.tracksContainer.removeChildren(); }
      if (this.shelvesContainer) { this.shelvesContainer.removeChildren(); }
      this.crnList = [];
      this.dualCrnList = [];
      this.rgvList = [];
      this.pixiCrnMap = new Map();
      this.pixiDualCrnMap = new Map();
      this.pixiRgvMap = new Map();
      this.pixiStaMap = new Map();
      this.pixiStageList = [];
@@ -362,6 +367,39 @@
        this.objectsContainer2.addChild(sprite);
      });
      
      this.dualCrnList.forEach((item) => {
        if (this.graphicsCrn == null) { this.graphicsCrn = this.createCrnTexture(item.width * 0.9, item.height * 0.9); }
        let sprite = new PIXI.Sprite(this.graphicsCrn);
        const deviceNo = this.getDeviceNo(item.value);
        const taskNo = this.getTaskNo(item.value);
        const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 12, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 });
        const txt = taskNo > 0 ? (deviceNo + "(" + taskNo + ")") : String(deviceNo);
        const text = new PIXI.Text(txt, style);
        text.anchor.set(0.5);
        text.position.set(sprite.width / 2, sprite.height / 2);
        sprite.addChild(text);
        sprite.textObj = text;
        sprite.position.set(item.posX, item.posY);
        sprite.interactive = true;
        sprite.buttonMode = true;
        sprite.on('pointerdown', () => {
          if (window.gsap) { window.gsap.killTweensOf(sprite); }
          sprite.alpha = 1;
          const id = parseInt(deviceNo, 10);
          this.$emit('dual-crn-click', id);
        });
        let rowIndexForCrn = 0;
        for (let r = 0; r < map.length; r++) {
          if (map[r].length > 0) {
            const rowY = map[r][0].posY;
            if (Math.abs(rowY - item.posY) < 0.5) { rowIndexForCrn = r; break; }
          }
        }
        sprite.rowIndex = rowIndexForCrn;
        this.pixiDualCrnMap.set(parseInt(deviceNo), sprite);
        this.objectsContainer2.addChild(sprite);
      });
      this.rgvList.forEach((item) => {
        if (this.graphicsRgv == null) { this.graphicsRgv = this.createRgvTexture(item.width * 0.9, item.height * 0.9); }
        let sprite = new PIXI.Sprite(this.graphicsRgv);
@@ -491,6 +529,10 @@
      if (this.isSwitchingFloor) { return; }
      this.sendWs(JSON.stringify({ url: "/console/latest/data/crn", data: {} }));
    },
    getDualCrnInfo() {
      if (this.isSwitchingFloor) { return; }
      this.sendWs(JSON.stringify({ url: "/console/latest/data/dualcrn", data: {} }));
    },
    getSiteInfo() {
      if (this.isSwitchingFloor) { return; }
      this.sendWs(JSON.stringify({ url: "/console/latest/data/station", data: {} }));
@@ -533,6 +575,60 @@
          for (let c = this.map[rowIndex].length - 1; c >= 0; c--) {
            const cell = this.map[rowIndex][c];
            if (cell && cell.type === 'crn') { targetCell = cell; break; }
          }
        }
        if (!targetCell) { continue; }
        const targetX = targetCell.posX + (targetCell.width - sprite.width) / 2;
        const dx = Math.abs(targetX - sprite.x);
        if (dx < 1) {
        } else if (dx < 5) {
          sprite.x = targetX;
        } else if (window.gsap) {
          window.gsap.killTweensOf(sprite);
          window.gsap.to(sprite, { x: targetX, duration: 0.3, ease: "power1.inOut" });
        } else {
          sprite.x = targetX;
        }
      }
      this.scheduleAdjustLabels();
    },
    setDualCrnInfo(res) {
      let crns = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
      if (!crns) { return; }
      for (var i = 0; i < crns.length; i++) {
        const id = parseInt(crns[i].crnId);
        const sprite = this.pixiDualCrnMap.get(id);
        if (!sprite) { continue; }
        const taskNo = crns[i].taskNo;
        if (taskNo != null && taskNo > 0) { sprite.textObj.text = id + "(" + taskNo + ")"; } else { sprite.textObj.text = String(id); }
        const status = crns[i].crnStatus;
        const statusColor = this.getCrnStatusColor(status);
        this.updateCrnTextureColor(sprite, statusColor);
        let bay = parseInt(crns[i].bay, 10);
        if (isNaN(bay) || bay < 1 || bay === -2) { bay = 1; }
        let rowIndex = (sprite.rowIndex != null) ? sprite.rowIndex : -1;
        if (rowIndex === -1) {
          for (let r = 0; r < this.map.length; r++) {
            if (this.map[r].length > 0) {
              const rowY = this.map[r][0].posY;
              if (Math.abs(rowY - sprite.y) < 0.5) { rowIndex = r; break; }
            }
          }
          if (rowIndex === -1) { rowIndex = 0; }
        }
        let targetCell = null;
        let crnCount = 0;
        for (let c = 0; c < this.map[rowIndex].length; c++) {
          const cell = this.map[rowIndex][c];
          if (cell && (cell.type === 'dualCrn' || cell.type === 'dualcrn')) {
            crnCount++;
            if (crnCount === bay) { targetCell = cell; break; }
          }
        }
        if (!targetCell) {
          for (let c = this.map[rowIndex].length - 1; c >= 0; c--) {
            const cell = this.map[rowIndex][c];
            if (cell && (cell.type === 'dualCrn' || cell.type === 'dualcrn')) { targetCell = cell; break; }
          }
        }
        if (!targetCell) { continue; }
@@ -605,6 +701,8 @@
        this.setSiteInfo(JSON.parse(result.data));
      } else if (result.url === "/console/latest/data/crn") {
        this.setCrnInfo(JSON.parse(result.data));
      } else if (result.url === "/console/latest/data/dualcrn") {
        this.setDualCrnInfo(JSON.parse(result.data));
      } else if (result.url === "/console/latest/data/rgv") {
        this.setRgvInfo(JSON.parse(result.data));
      } else if (typeof result.url === "string" && result.url.indexOf("/basMap/lev/") === 0) {
@@ -901,6 +999,10 @@
        sprite = this.createTrackSprite(item.width, item.height);
        sprite._kind = 'crn-track';
        if (this.getDeviceNo(value) > 0) { this.crnList.push(item); }
      } else if (item.type == 'dualCrn') {
        sprite = this.createTrackSprite(item.width, item.height);
        sprite._kind = 'crn-track';
        if (this.getDeviceNo(value) > 0) { this.dualCrnList.push(item); }
      } else if (item.type == 'rgv') {
        sprite = this.createTrackSprite(item.width, item.height);
        sprite._kind = 'rgv-track';
@@ -983,6 +1085,20 @@
        const on = sx >= -margin && sy >= -margin && sx <= vw + margin && sy <= vh + margin;
        textObj.visible = (s >= 0.25) && on;
      });
      this.pixiDualCrnMap && this.pixiDualCrnMap.forEach((sprite) => {
        const textObj = sprite && sprite.textObj;
        if (!textObj) { return; }
        const base = (textObj.style && textObj.style.fontSize) ? textObj.style.fontSize : 12;
        let scale = minPx / (base * s);
        if (!isFinite(scale)) { scale = 1; }
        scale = Math.max(0.8, Math.min(scale, 3));
        textObj.scale.set(scale);
        textObj.position.set(sprite.width / 2, sprite.height / 2);
        const sx = pos.x + sprite.x * s;
        const sy = pos.y + sprite.y * s;
        const on = sx >= -margin && sy >= -margin && sx <= vw + margin && sy <= vh + margin;
        textObj.visible = (s >= 0.25) && on;
      });
      this.pixiRgvMap && this.pixiRgvMap.forEach((sprite) => {
        const textObj = sprite && sprite.textObj;
        if (!textObj) { return; }
src/main/webapp/components/WatchCrnCard.js
@@ -53,8 +53,11 @@
                <el-descriptions-item label="升降距离(Km)">{{ item.ydistance }}</el-descriptions-item>
                <el-descriptions-item label="走行时长(H)">{{ item.xDuration }}</el-descriptions-item>
                <el-descriptions-item label="升降时长(H)">{{ item.yduration }}</el-descriptions-item>
                <el-descriptions-item label="称重数据">{{ item.weight }}</el-descriptions-item>
                <el-descriptions-item label="条码数据">{{ item.barcode }}</el-descriptions-item>
                <el-descriptions-item label="故障代码">{{ item.warnCode }}</el-descriptions-item>
                <el-descriptions-item label="故障描述">{{ item.alarm }}</el-descriptions-item>
                <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item>
            </el-descriptions>
            </el-collapse-item>
          </el-collapse>
src/main/webapp/components/WatchDualCrnCard.js
New file
@@ -0,0 +1,245 @@
Vue.component("watch-dual-crn-card", {
  template: `
    <div>
        <div style="display: flex;margin-bottom: 10px;">
            <div style="width: 100%;">双工位堆垛机监控</div>
            <div style="width: 100%;text-align: right;display: flex;">
              <el-input size="mini" v-model="searchCrnNo" placeholder="请输入堆垛机号"></el-input>
              <el-button @click="getDualCrnStateInfo" size="mini">查询</el-button>
            </div>
        </div>
        <div style="margin-bottom: 10px;">
          <div style="margin-bottom: 5px;">
            <el-button v-if="showControl" @click="openControl" size="mini">关闭控制中心</el-button>
            <el-button v-else @click="openControl" size="mini">打开控制中心</el-button>
          </div>
          <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;">
            <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.crnNo" placeholder="堆垛机号"></el-input></div>
            <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourceLocNo" placeholder="源点"></el-input></div>
            <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetLocNo" placeholder="目标点"></el-input></div>
            <div style="margin-bottom: 10px;width: 33%;">
              <el-select size="mini" v-model="controlParam.station" placeholder="工位">
                <el-option :label="'工位1'" :value="1"></el-option>
                <el-option :label="'工位2'" :value="2"></el-option>
              </el-select>
            </div>
            <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">取放货</el-button></div>
            <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">移动</el-button></div>
            <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">任务完成</el-button></div>
          </div>
        </div>
        <div style="max-height: 55vh; overflow:auto;">
          <el-collapse v-model="activeNames" accordion>
            <el-collapse-item v-for="(item) in displayCrnList" :name="item.crnNo">
              <template slot="title">
                <div style="width: 100%;display: flex;">
                  <div style="width: 50%;">{{ item.crnNo }}号双工位堆垛机</div>
                  <div style="width: 50%;text-align: right;">
                    <el-tag v-if="item.deviceStatus == 'AUTO'" type="success" size="small">自动</el-tag>
                    <el-tag v-else-if="item.deviceStatus == 'WORKING'" size="small">作业中</el-tag>
                    <el-tag v-else-if="item.deviceStatus == 'ERROR'" type="danger" size="small">故障</el-tag>
                    <el-tag v-else type="warning" size="small">离线</el-tag>
                  </div>
                </div>
              </template>
              <el-descriptions border direction="vertical">
                <el-descriptions-item label="模式">{{ item.mode }}</el-descriptions-item>
                <el-descriptions-item label="异常码">{{ item.warnCode }}</el-descriptions-item>
                <el-descriptions-item label="工位1任务号">{{ item.taskNo }}</el-descriptions-item>
                <el-descriptions-item label="工位2任务号">{{ item.taskNoTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1状态">{{ item.status }}</el-descriptions-item>
                <el-descriptions-item label="工位2状态">{{ item.statusTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1是否有物">{{ item.loading }}</el-descriptions-item>
                <el-descriptions-item label="工位2是否有物">{{ item.loadingTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1列">{{ item.bay }}</el-descriptions-item>
                <el-descriptions-item label="工位2列">{{ item.bayTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1层">{{ item.lev }}</el-descriptions-item>
                <el-descriptions-item label="工位2层">{{ item.levTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1货叉定位">{{ item.forkOffset }}</el-descriptions-item>
                <el-descriptions-item label="工位2货叉定位">{{ item.forkOffsetTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1载货台定位">{{ item.liftPos }}</el-descriptions-item>
                <el-descriptions-item label="工位2载货台定位">{{ item.liftPosTwo }}</el-descriptions-item>
                <el-descriptions-item label="工位1走行在定位">{{ item.walkPos }}</el-descriptions-item>
                <el-descriptions-item label="工位2走行在定位">{{ item.walkPosTwo }}</el-descriptions-item>
                <el-descriptions-item label="走行速度(m/min)">{{ item.xspeed }}</el-descriptions-item>
                <el-descriptions-item label="升降速度(m/min)">{{ item.yspeed }}</el-descriptions-item>
                <el-descriptions-item label="叉牙速度(m/min)">{{ item.zspeed }}</el-descriptions-item>
                <el-descriptions-item label="走行距离(Km)">{{ item.xdistance }}</el-descriptions-item>
                <el-descriptions-item label="升降距离(Km)">{{ item.ydistance }}</el-descriptions-item>
                <el-descriptions-item label="走行时长(H)">{{ item.xduration }}</el-descriptions-item>
                <el-descriptions-item label="升降时长(H)">{{ item.yduration }}</el-descriptions-item>
                <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item>
              </el-descriptions>
            </el-collapse-item>
          </el-collapse>
        </div>
        <div style="display:flex; justify-content:flex-end; margin-top:8px;">
          <el-pagination
            @current-change="handlePageChange"
            @size-change="handleSizeChange"
            :current-page="currentPage"
            :page-size="pageSize"
            :page-sizes="[10,20,50,100]"
            layout="total, prev, pager, next"
            :total="crnList.length">
          </el-pagination>
        </div>
    </div>
  `,
  props: ["param"],
  data() {
    return {
      crnList: [],
      activeNames: "",
      searchCrnNo: "",
      showControl: false,
      controlParam: {
        crnNo: "",
        sourceLocNo: "",
        targetLocNo: "",
        station: 1,
      },
      pageSize: 25,
      currentPage: 1,
    };
  },
  created() {
    setInterval(() => {
      this.getDualCrnStateInfo();
    }, 1000);
  },
  computed: {
    displayCrnList() {
      const start = (this.currentPage - 1) * this.pageSize;
      const end = start + this.pageSize;
      return this.crnList.slice(start, end);
    }
  },
  watch: {
    param: {
      handler(newVal) {
        if (newVal.crnNo != 0) {
          this.activeNames = newVal.crnNo;
          this.searchCrnNo = newVal.crnNo;
          const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo);
          if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; }
        }
      },
      deep: true,
      immediate: true,
    },
  },
  methods: {
    handlePageChange(page) {
      this.currentPage = page;
    },
    handleSizeChange(size) {
      this.pageSize = size;
      this.currentPage = 1;
    },
    openControl() {
      this.showControl = !this.showControl;
    },
    getDualCrnStateInfo() {
      if (this.$root.sendWs) {
        this.$root.sendWs(JSON.stringify({
          "url": "/dualcrn/table/crn/state",
          "data": {}
        }));
      }
    },
    controlCommandTransport() {
      let that = this;
      $.ajax({
        url: baseUrl + "/dualcrn/command/take",
        headers: {
          token: localStorage.getItem("token"),
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify(that.controlParam),
        success: (res) => {
          if (res.code == 200) {
            that.$message({
              message: res.msg,
              type: "success",
            });
          } else {
            that.$message({
              message: res.msg,
              type: "warning",
            });
          }
        },
      });
    },
    controlCommandMove() {
      let that = this;
      $.ajax({
        url: baseUrl + "/dualcrn/command/move",
        headers: {
          token: localStorage.getItem("token"),
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify(that.controlParam),
        success: (res) => {
          if (res.code == 200) {
            that.$message({
              message: res.msg,
              type: "success",
            });
          } else {
            that.$message({
              message: res.msg,
              type: "warning",
            });
          }
        },
      });
    },
    controlCommandTaskComplete() {
      let that = this;
      $.ajax({
        url: baseUrl + "/dualcrn/command/taskComplete",
        headers: {
          token: localStorage.getItem("token"),
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify(that.controlParam),
        success: (res) => {
          if (res.code == 200) {
            that.$message({
              message: res.msg,
              type: "success",
            });
          } else {
            that.$message({
              message: res.msg,
              type: "warning",
            });
          }
        },
      });
    },
    setDualCrnList(res) {
      let that = this;
      if (res.code == 200) {
        let list = res.data;
        if (that.searchCrnNo == "") {
          that.crnList = list;
        } else {
          let tmp = [];
          list.forEach((item) => {
            if (item.crnNo == that.searchCrnNo) {
              tmp.push(item);
            }
          });
          that.crnList = tmp;
          that.currentPage = 1;
        }
      }
    },
  },
});
src/main/webapp/components/WatchRgvCard.js
@@ -45,6 +45,7 @@
                <el-descriptions-item label="是否有物">{{ item.loading }}</el-descriptions-item>
                <el-descriptions-item label="故障代码">{{ item.warnCode }}</el-descriptions-item>
                <el-descriptions-item label="故障描述">{{ item.alarm }}</el-descriptions-item>
                <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item>
            </el-descriptions>
            </el-collapse-item>
          </el-collapse>
src/main/webapp/views/watch/console.html
@@ -23,6 +23,9 @@
                        <el-tab-pane label="堆垛机" name="crn">
                            <watch-crn-card ref="watchCrnCard" :param="crnParam"></watch-crn-card>
                        </el-tab-pane>
                        <el-tab-pane label="双工位堆垛机" name="dualCrn">
                            <watch-dual-crn-card ref="watchDualCrnCard" :param="dualCrnParam"></watch-dual-crn-card>
                        </el-tab-pane>
                        <el-tab-pane label="输送站" name="devp">
                            <devp-card ref="devpCard" :param="devpParam"></devp-card>
                        </el-tab-pane>
@@ -35,7 +38,7 @@
                    </el-tabs>
                </div>
                <map-canvas :lev="currentLev" :crn-param="crnParam" :rgv-param="rgvParam" :devp-param="devpParam" @crn-click="openCrn" @station-click="openSite" style="width: 80%; height: 100vh;"></map-canvas>
                <map-canvas :lev="currentLev" :crn-param="crnParam" :rgv-param="rgvParam" :devp-param="devpParam" @crn-click="openCrn" @dual-crn-click="openDualCrn" @station-click="openSite" style="width: 80%; height: 100vh;"></map-canvas>
                <div style="position: absolute;top: 15px;left: 50%;display: flex;">
                    <div v-if="levList.length > 1" v-for="(lev,index) in levList" :key="index" style="margin-right: 10px;">
@@ -47,6 +50,7 @@
        </div>
        <script src="../../components/WatchCrnCard.js"></script>
        <script src="../../components/WatchDualCrnCard.js"></script>
        <script src="../../components/DevpCard.js"></script>
        <script src="../../components/MapSettingCard.js"></script>
        <script src="../../components/WatchRgvCard.js"></script>
@@ -64,6 +68,9 @@
                    rgvPosition: [],
                    activateCard: 'crn',
                    crnParam: {
                        crnNo: 0
                    },
                    dualCrnParam: {
                        crnNo: 0
                    },
                    mapSettingParam: {
@@ -105,6 +112,10 @@
                        if (result.url == "/crn/table/crn/state") {
                             if(this.$refs.watchCrnCard) {
                                 this.$refs.watchCrnCard.setCrnList(JSON.parse(result.data));
                             }
                        } else if (result.url == "/dualcrn/table/crn/state") {
                             if(this.$refs.watchDualCrnCard) {
                                 this.$refs.watchDualCrnCard.setDualCrnList(JSON.parse(result.data));
                             }
                        } else if (result.url == "/console/latest/data/station") {
                             if(this.$refs.devpCard) {
@@ -161,6 +172,13 @@
                    openCrn(id) {
                        this.crnParam.crnNo = id;
                        this.activateCard = 'crn';
                        console.log(id);
                    },
                    openDualCrn(id) {
                        this.dualCrnParam.crnNo = id;
                        this.activateCard = 'dualCrn';
                        console.log(id);
                    },
                    openRgv(id) {
                        this.rgvParam.rgvNo = id;