diff --git a/config/web/css/style.css b/config/web/css/style.css index 20556ab..00c71b8 100644 --- a/config/web/css/style.css +++ b/config/web/css/style.css @@ -358,7 +358,7 @@ a { color: var(--accent); text-decoration: none; } /* ============================================================ PLC Debug ============================================================ */ -.plc-debug-grid { display: grid; grid-template-columns: 1fr 1.2fr 1fr; gap: 16px; } +.plc-debug-grid { display: grid; grid-template-columns: 1fr 1.2fr 1fr; gap: 8px; } .plc-col { min-width: 0; } .plc-sig-col { border-left: 1px solid var(--border); border-right: 1px solid var(--border); padding: 0 8px; } .plc-sig-list { max-height: 65vh; overflow-y: auto; } @@ -419,7 +419,7 @@ a { color: var(--accent); text-decoration: none; } .plc-debug-grid { display: grid; grid-template-columns: 1fr 1.2fr 1fr; - gap: 12px; + gap: 8px; height: calc(100vh - 200px); min-height: 500px; } @@ -446,6 +446,14 @@ a { color: var(--accent); text-decoration: none; } background: var(--bg-surface); position: sticky; top: 0; z-index: 1; } +.plc-block { display: flex; flex-direction: column; flex: 1; min-height: 0; } +.plc-block-header { + padding: 8px 12px; font-size: 12px; font-weight: 700; + color: var(--text-primary); border-bottom: 1px solid var(--border); + background: var(--bg-surface); display: flex; align-items: center; + position: sticky; top: 0; z-index: 1; +} + .plc-empty { text-align: center; color: var(--text-muted); padding: 30px 10px; font-size: 12px; } .plc-item { @@ -475,7 +483,7 @@ a { color: var(--accent); text-decoration: none; } .plc-item-val { margin-left: 4px; font-weight: 600; font-size: 10px; } /* ---- PLC Signal List ---- */ -.plc-arrow-row { display: flex; gap: 6px; padding: 8px 12px; border-bottom: 1px solid var(--border); } +.plc-arrow-row { display: flex; gap: 4px; padding: 4px 8px; } .plc-sig-list { flex: 1; overflow-y: auto; } .plc-sig-check-item { display: flex; align-items: center; gap: 6px; diff --git a/config/web/js/pages/plc_config.js b/config/web/js/pages/plc_config.js index ba065fd..da61f04 100644 --- a/config/web/js/pages/plc_config.js +++ b/config/web/js/pages/plc_config.js @@ -31,7 +31,7 @@ var PlcConfigPage = (function() { /* ========== 节点类型常量 ========== */ var NT_OUTPUT=0, NT_INPUT=1, NT_OR=2, NT_AND=3, NT_NOT=4; - var NT_62P=5, NT_62D=6, NT_RISING=7, NT_SR=8, NT_RS=9, NT_FALLING=10; + var NT_62P=5, NT_62D=6, NT_RISING=7, NT_SR=8, NT_RS=9, NT_FALLING=10, NT_TOGGLE=11, NT_MX_ADD=50, NT_MX_SUB=51, NT_MX_MAX=52, NT_MX_MIN=53, NT_MX_MUL=54, NT_MX_DIV=55, NT_MX_ABS=56, NT_MX_CMP=57; var NODE_INFO = { 0: { name:'输出点', icon:'📤', color:'#4CAF50', inPins:1, outPins:0, inMax:1, outMax:0 }, @@ -44,7 +44,16 @@ var PlcConfigPage = (function() { 7: { name:'上升沿', icon:'⬆️', color:'#E91E63', inPins:1, outPins:1, inMax:1, outMax:10 }, 8: { name:'SR触发器', icon:'🔒', color:'#795548', inPins:2, outPins:1, inMax:2, outMax:10 }, 9: { name:'RS触发器', icon:'🔒', color:'#607D8B', inPins:2, outPins:1, inMax:2, outMax:10 }, - 10: { name:'下降沿', icon:'⬇️', color:'#FF5722', inPins:1, outPins:1, inMax:1, outMax:10 } + 10: { name:'下降沿', icon:'⬇️', color:'#FF5722', inPins:1, outPins:1, inMax:1, outMax:10 }, + 11: { name:'翻转元件', icon:'🔄', color:'#CDDC39', inPins:1, outPins:1, inMax:1, outMax:10 }, + 50: { name:'加法元件', icon:'➕', color:'#E040FB', inPins:2, outPins:1, inMax:2, outMax:10 }, + 51: { name:'减法元件', icon:'➖', color:'#00E5FF', inPins:2, outPins:1, inMax:2, outMax:10 }, + 52: { name:'最大值元件', icon:'⬆️', color:'#FFD600', inPins:2, outPins:1, inMax:2, outMax:10 }, + 53: { name:'最小值元件', icon:'⬇️', color:'#76FF03', inPins:2, outPins:1, inMax:2, outMax:10 }, + 54: { name:'乘法元件', icon:'✖️', color:'#FF6D00', inPins:2, outPins:1, inMax:2, outMax:10 }, + 55: { name:'除法元件', icon:'➗', color:'#D50000', inPins:2, outPins:1, inMax:2, outMax:10 }, + 56: { name:'绝对值元件', icon:'📏', color:'#00C853', inPins:1, outPins:1, inMax:1, outMax:10 }, + 57: { name:'比较元件', icon:'⚖️', color:'#FF1744', inPins:2, outPins:1, inMax:2, outMax:10 } }; /* ========== 全局状态 ========== */ @@ -121,7 +130,7 @@ var PlcConfigPage = (function() { * 信号面板 * ================================================================ */ function renderSigPanel() { - return '
' + return '
' + '
📋 信号 ('+dcOutSignals.length+')' + '
' + '' @@ -129,6 +138,13 @@ var PlcConfigPage = (function() { + '
'; } + function renderOpPanel() { + var ops=[{t:NT_AND,n:'与门',i:'🔷',c:'#9C27B0'},{t:NT_OR,n:'或门',i:'🔶',c:'#FF9800'},{t:NT_NOT,n:'非门',i:'🔺',c:'#f44336'},{t:NT_62P,n:'62P',i:'⏱️',c:'#00BCD4'},{t:NT_62D,n:'62D',i:'⏱️',c:'#009688'},{t:NT_RISING,n:'上升沿',i:'⬆️',c:'#E91E63'},{t:NT_FALLING,n:'下降沿',i:'⬇️',c:'#FF5722'},{t:NT_TOGGLE,n:'翻转',i:'🔄',c:'#CDDC39'},{t:NT_SR,n:'SR',i:'🔒',c:'#795548'},{t:NT_RS,n:'RS',i:'🔒',c:'#607D8B'},{t:NT_MX_ADD,n:'加法',i:'➕',c:'#E040FB'},{t:NT_MX_SUB,n:'减法',i:'➖',c:'#00E5FF'},{t:NT_MX_MAX,n:'最大值',i:'⬆️',c:'#FFD600'},{t:NT_MX_MIN,n:'最小值',i:'⬇️',c:'#76FF03'},{t:NT_MX_MUL,n:'乘法',i:'✖️',c:'#FF6D00'},{t:NT_MX_DIV,n:'除法',i:'➗',c:'#D50000'},{t:NT_MX_ABS,n:'绝对值',i:'📏',c:'#00C853'},{t:NT_MX_CMP,n:'比较',i:'⚖️',c:'#FF1744'}]; + var h='
🧮 运算符
'; + for(var oi=0;oi'+op.i+''+op.n+'
';} + h+='
'; + return h; + } function loadDcOutSignals(cb) { requestFile('config/SYSTEM/datacenter_out.xml', function(content) { if(!content){showToast('加载 datacenter_out.xml 失败','err');return;} @@ -148,9 +164,9 @@ var PlcConfigPage = (function() { var data=dcOutSignals; if(search){var q=search.toLowerCase();data=data.filter(function(s){return s.saddr.toLowerCase().indexOf(q)!==-1||s.desc.toLowerCase().indexOf(q)!==-1||String(s.no).indexOf(q)!==-1;});} var h=''; - for(var i=0;i#'+s.no+''+escHtml(s.saddr)+''+escHtml(s.desc)+'';} + for(var i=0;i#'+s.no+''+escHtml(s.saddr)+''+escHtml(s.desc)+'';if(mode==='edit'){h+='';}h+='';} list.innerHTML=h||'
无匹配信号
'; - [].forEach.call(list.querySelectorAll('.plc-sig-row'),function(row){row.onclick=function(){if(mode!=='edit')return;var no=parseInt(this.dataset.no),saddr=this.dataset.saddr,desc=this.dataset.desc||'';editNodes.push({type:NT_INPUT,id:no,x:0,y:0,_lx:-260,_ly:editNodes.filter(function(n){return n.type===NT_INPUT;}).length*110,saddr:saddr,desc:desc,no:no});drawEditor();showToast('已添加输入: '+saddr,'ok');};}); + [].forEach.call(list.querySelectorAll('.plc-sig-row'),function(row){}); } /* ================================================================ @@ -202,6 +218,7 @@ var PlcConfigPage = (function() { editWiring=null;editDragNode=null;editPanX=0;editPanY=0;editScale=1.0;editNextGateIdx={}; main.innerHTML = renderSigPanel() + + renderOpPanel() + '
' + '
✏️ 编辑配置' + '' @@ -217,8 +234,8 @@ var PlcConfigPage = (function() { + '' + '' + '' - + '' - + '' + + '' + + '' + '' + '' + '' @@ -243,6 +260,19 @@ var PlcConfigPage = (function() { document.getElementById('plc-edit-modify').onclick=modifySelectedNode; document.getElementById('plc-edit-generate').onclick=generateAndSaveConfig; setupEditorCanvas();drawEditor(); + /* 运算符面板点击事件 */ + var opList=document.getElementById('plc-op-list'); + if(opList){[].forEach.call(opList.querySelectorAll('.plc-op-row'),function(row){row.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt);PlcConfigPage._addOp(gt);};});} + /* 运算符面板点击事件 */ + var opList=document.getElementById('plc-op-list'); + if(opList){[].forEach.call(opList.querySelectorAll('.plc-op-row'),function(row){row.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt);PlcConfigPage._addOp(gt);};});} + /* 重试之前未成功的文件请求(同页切换时 onEnter 不触发) */ + if (PlcConfigPage._retryList && PlcConfigPage._retryList.length > 0) { + var _rty = PlcConfigPage._retryList.slice(); + PlcConfigPage._retryList = []; + for (var _ri = 0; _ri < _rty.length; _ri++) + requestFile(_rty[_ri], (PlcConfigPage._pending || {})[_rty[_ri]] || function(){}); + } if(dcOutSignals.length===0){loadDcOutSignals(function(){refreshSigPanel();autoLoadEditor();});}else{refreshSigPanel();autoLoadEditor();} } @@ -250,22 +280,22 @@ var PlcConfigPage = (function() { function autoLoadEditor(){if(editNodes.length>0)return;loadCurrentLogic(function(){if(logicGraphs.length>0)loadGraphIntoEditor('Reclose_logic.xml');else resetEditor();});} - function loadGraphIntoEditor(srcName){var g=null;for(var gi=0;gi=0){g=logicGraphs[gi];break;}}if(!g&&logicGraphs.length>0)g=logicGraphs[0];if(!g){resetEditor();return;}editNodes=[];editLinks=[];editKey=g.key;editName=g.name||'';for(var i=0;i=0&&di>=0){var el={srcIdx:si,dstIdx:di};if(l.dstPin!==undefined)el.dstPin=l.dstPin;editLinks.push(el);}}var mx={};for(var di=0;di=(mx[dn.type]||-1))mx[dn.type]=idx;}for(var t in mx)editNextGateIdx[t]=mx[t]+1;var seen={};for(var di=0;di0)g=logicGraphs[0];if(!g){resetEditor();return;}editNodes=[];editLinks=[];editKey=g.key;editName=g.name||'';for(var i=0;i=0&&di>=0){var el={srcIdx:si,dstIdx:di};if(l.dstPin!==undefined)el.dstPin=l.dstPin;editLinks.push(el);}}var mx={};for(var di=0;di=(mx[dn.type]||-1))mx[dn.type]=idx;}for(var t in mx)editNextGateIdx[t]=mx[t]+1;var seen={};for(var di=0;di1&&(n.id&0xFF)===(id&0xFF))return i;}}return -1;} /* ---- 编辑器 Canvas ---- */ - function setupEditorCanvas(){var wrap=document.getElementById('plc-edit-canvas-wrap'),cnv=document.getElementById('plc-edit-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=Math.max(500,wrap.clientHeight-4);editScale=0.7;editPanX=cnv.width/2;editPanY=cnv.height/2;drawEditor();}setTimeout(resize,50);window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;var ph=hitTestPin(mx,my);if(ph){if(editWiring){if(editWiring.fromIdx!==ph.nodeIdx){if(editWiring.fromSide==='out'&&ph.side==='in'){var si=NODE_INFO[editNodes[editWiring.fromIdx].type]||{},om=si.outMax||99,oc=0;for(var lc=0;lc=om){showToast('输出上限('+om+')','err');editWiring=null;editSelectedNode=-1;drawEditor();return;}var lk={srcIdx:editWiring.fromIdx,dstIdx:ph.nodeIdx,dstPin:ph.pinIdx};var dn=editNodes[ph.nodeIdx];if(dn.type===NT_SR||dn.type===NT_RS){for(var lc2=0;lc2=0){editSelectedLink=lh;editSelectedNode=-1;editDragNode=null;if(editWiring)editWiring=null;drawEditor();return;}var nh=hitTestNode(mx,my);if(nh>=0){editSelectedNode=nh;editSelectedLink=-1;editDragNode=nh;editDragOffX=mx-editNodes[nh]._lx;editDragOffY=my-editNodes[nh]._ly;drawEditor();return;}editSelectedNode=-1;editSelectedLink=-1;if(editWiring)editWiring=null;editDragCanvas={sx:e.clientX,sy:e.clientY,px:editPanX,py:editPanY};cnv.style.cursor='grabbing';drawEditor();};cnv.onmousemove=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;if(editDragCanvas){editPanX=editDragCanvas.px+(e.clientX-editDragCanvas.sx);editPanY=editDragCanvas.py+(e.clientY-editDragCanvas.sy);drawEditor();return;}if(editDragNode!==null){editNodes[editDragNode]._lx=mx-editDragOffX;editNodes[editDragNode]._ly=my-editDragOffY;drawEditor();return;}if(editWiring&&editWiring.fromIdx=0?'grab':'default');};cnv.onmouseup=function(e){if(editDragCanvas){editDragCanvas=null;cnv.style.cursor='default';return;}if(editDragNode!==null){var nd=editNodes[editDragNode],ni=editDragNode;var dx=Math.abs((e.clientX-cnv.getBoundingClientRect().left-editPanX)/editScale-nd._lx-editDragOffX);var dy=Math.abs((e.clientY-cnv.getBoundingClientRect().top-editPanY)/editScale-nd._ly-editDragOffY);if(dx<3&&dy<3){var n=Date.now();if(ni===editLastClickNode&&(n-editLastClickTime)<400){onNodeDblClick(ni);editLastClickTime=0;editLastClickNode=-1;}else{editLastClickTime=n;editLastClickNode=ni;}}}editDragNode=null;};cnv.onmouseleave=function(){editDragNode=null;editDragCanvas=null;cnv.style.cursor='default';};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,r=cnv.getBoundingClientRect();var ns=Math.max(0.2,Math.min(3,editScale*d));editPanX=(e.clientX-r.left)-((e.clientX-r.left)-editPanX)*(ns/editScale);editPanY=(e.clientY-r.top)-((e.clientY-r.top)-editPanY)*(ns/editScale);editScale=ns;drawEditor();};cnv.oncontextmenu=function(e){e.preventDefault();if(editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;editSelectedNode=-1;showToast('连线已删除','ok');drawEditor();return;}if(editSelectedNode>=0){deleteSelectedNode();return;}};cnv.tabIndex=0;cnv.onkeydown=function(e){if(e.key==='Escape'){editWiring=null;editSelectedNode=-1;editSelectedLink=-1;drawEditor();}if(e.key==='Delete'&&editSelectedNode>=0){deleteSelectedNode();}if(e.key==='Delete'&&editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;showToast('连线已删除','ok');drawEditor();}};} + function setupEditorCanvas(){var wrap=document.getElementById('plc-edit-canvas-wrap'),cnv=document.getElementById('plc-edit-canvas');if(!wrap||!cnv)return;function resize(){cnv.width=wrap.clientWidth;cnv.height=Math.max(500,wrap.clientHeight-4);editScale=0.7;editPanX=cnv.width/2;editPanY=cnv.height/2;drawEditor();}setTimeout(resize,50);window.addEventListener('resize',function(){resize();});cnv.onmousedown=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;var ph=hitTestPin(mx,my);if(ph){if(editWiring){if(editWiring.fromIdx!==ph.nodeIdx){if(editWiring.fromSide==='out'&&ph.side==='in'){var si=NODE_INFO[editNodes[editWiring.fromIdx].type]||{},om=si.outMax||99,oc=0;for(var lc=0;lc=om){showToast('输出上限('+om+')','err');editWiring=null;editSelectedNode=-1;drawEditor();return;}var lk={srcIdx:editWiring.fromIdx,dstIdx:ph.nodeIdx,dstPin:ph.pinIdx};var dn=editNodes[ph.nodeIdx];var dInfo=NODE_INFO[dn.type]||{};if(dInfo.inPins>1){for(var lc2=0;lc2=0){editSelectedLink=lh;editSelectedNode=-1;editDragNode=null;if(editWiring)editWiring=null;drawEditor();return;}var nh=hitTestNode(mx,my);if(nh>=0){editSelectedNode=nh;editSelectedLink=-1;editDragNode=nh;editDragOffX=mx-editNodes[nh]._lx;editDragOffY=my-editNodes[nh]._ly;drawEditor();return;}editSelectedNode=-1;editSelectedLink=-1;if(editWiring)editWiring=null;editDragCanvas={sx:e.clientX,sy:e.clientY,px:editPanX,py:editPanY};cnv.style.cursor='grabbing';drawEditor();};cnv.onmousemove=function(e){var r=cnv.getBoundingClientRect(),mx=(e.clientX-r.left-editPanX)/editScale,my=(e.clientY-r.top-editPanY)/editScale;if(editDragCanvas){editPanX=editDragCanvas.px+(e.clientX-editDragCanvas.sx);editPanY=editDragCanvas.py+(e.clientY-editDragCanvas.sy);drawEditor();return;}if(editDragNode!==null){editNodes[editDragNode]._lx=mx-editDragOffX;editNodes[editDragNode]._ly=my-editDragOffY;drawEditor();return;}if(editWiring&&editWiring.fromIdx=0?'grab':'default');};cnv.onmouseup=function(e){if(editDragCanvas){editDragCanvas=null;cnv.style.cursor='default';return;}if(editDragNode!==null){var nd=editNodes[editDragNode],ni=editDragNode;var dx=Math.abs((e.clientX-cnv.getBoundingClientRect().left-editPanX)/editScale-nd._lx-editDragOffX);var dy=Math.abs((e.clientY-cnv.getBoundingClientRect().top-editPanY)/editScale-nd._ly-editDragOffY);if(dx<3&&dy<3){var n=Date.now();if(ni===editLastClickNode&&(n-editLastClickTime)<400){onNodeDblClick(ni);editLastClickTime=0;editLastClickNode=-1;}else{editLastClickTime=n;editLastClickNode=ni;}}}editDragNode=null;};cnv.onmouseleave=function(){editDragNode=null;editDragCanvas=null;cnv.style.cursor='default';};cnv.onwheel=function(e){e.preventDefault();var d=e.deltaY>0?0.9:1.1,r=cnv.getBoundingClientRect();var ns=Math.max(0.2,Math.min(3,editScale*d));editPanX=(e.clientX-r.left)-((e.clientX-r.left)-editPanX)*(ns/editScale);editPanY=(e.clientY-r.top)-((e.clientY-r.top)-editPanY)*(ns/editScale);editScale=ns;drawEditor();};cnv.oncontextmenu=function(e){e.preventDefault();if(editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;editSelectedNode=-1;showToast('连线已删除','ok');drawEditor();return;}if(editSelectedNode>=0){deleteSelectedNode();return;}};cnv.tabIndex=0;cnv.onkeydown=function(e){if(e.key==='Escape'){editWiring=null;editSelectedNode=-1;editSelectedLink=-1;drawEditor();}if(e.key==='Delete'&&editSelectedNode>=0){deleteSelectedNode();}if(e.key==='Delete'&&editSelectedLink>=0){editLinks.splice(editSelectedLink,1);editSelectedLink=-1;showToast('连线已删除','ok');drawEditor();}};} function hitTestNode(mx,my){for(var i=editNodes.length-1;i>=0;i--){var n=editNodes[i],sz=getNodeSize(n);if(mx>n._lx-sz.w/2&&mxn._ly-sz.h/2&&my=0;i--){var n=editNodes[i],sz=getNodeSize(n),info=NODE_INFO[n.type]||NODE_INFO[0];var isIO=(n.type===NT_INPUT||n.type===NT_OUTPUT),isSR=(n.type===NT_SR||n.type===NT_RS);var inP=getNodeInPins(n);if(info.inPins>0){var tp=isSR?2:(isIO?inP:1);for(var p=0;p0){if(Math.sqrt((mx-(n._lx+sz.w/2))*(mx-(n._lx+sz.w/2))+(my-n._ly)*(my-n._ly))<=pinR)return{nodeIdx:i,side:'out',pinIdx:0};}}return null;} + function hitTestPin(mx,my){var pinR=8;for(var i=editNodes.length-1;i>=0;i--){var n=editNodes[i],sz=getNodeSize(n),info=NODE_INFO[n.type]||NODE_INFO[0];var isIO=(n.type===NT_INPUT||n.type===NT_OUTPUT),isSR=(n.type===NT_SR||n.type===NT_RS);var inP=getNodeInPins(n);if(info.inPins>0){var tp=isSR?2:(isIO?inP:inP);for(var p=0;p0){if(Math.sqrt((mx-(n._lx+sz.w/2))*(mx-(n._lx+sz.w/2))+(my-n._ly)*(my-n._ly))<=pinR)return{nodeIdx:i,side:'out',pinIdx:0};}}return null;} function hitTestLink(mx,my){for(var i=0;i=editNodes.length||l.dstIdx>=editNodes.length)continue;var sn=editNodes[l.srcIdx],dn=editNodes[l.dstIdx],szS=getNodeSize(sn),szD=getNodeSize(dn);var x1=sn._lx+szS.w/2,y1=sn._ly,isSR=(dn.type===NT_SR||dn.type===NT_RS);var inP=getNodeInPins(dn),dpi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-szD.h/2+szD.h*(dpi+1)/(isSR?3:(inP+1)),x2=dn._lx-szD.w/2;var t=Math.max(0,Math.min(1,((mx-x1)*(x2-x1)+(my-y1)*(y2-y1))/((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))));var px=x1+t*(x2-x1),py=y1+t*(y2-y1);if(Math.sqrt((mx-px)*(mx-px)+(my-py)*(my-py))<10)return i;}return -1;} function drawEditor(){var cnv=document.getElementById('plc-edit-canvas');if(!cnv)return;var ctx=cnv.getContext('2d'),W=cnv.width,H=cnv.height;ctx.clearRect(0,0,W,H);ctx.fillStyle='#0a0f16';ctx.fillRect(0,0,W,H);ctx.save();ctx.translate(editPanX,editPanY);ctx.scale(editScale,editScale);ctx.strokeStyle='rgba(255,255,255,0.05)';ctx.lineWidth=1;var gs=40,sx=Math.floor(-editPanX/editScale/gs)*gs,sy=Math.floor(-editPanY/editScale/gs)*gs;for(var gx=sx-gs;gx=0&&editSelectedNode=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode];if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(editSelectedNode);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(editSelectedNode);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(editSelectedNode);else showGateCfgModal(editSelectedNode);} + function onNodeDblClick(idx){var nd=editNodes[idx];editSelectedNode=idx;drawEditor();if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(idx);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(idx);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(idx);else if(nd.type>=50&&nd.type<=56)showMXInfoModal(idx);else showGateCfgModal(idx);} + function modifySelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode];if(nd.type===NT_INPUT||nd.type===NT_OUTPUT)showRebindModal(editSelectedNode);else if(nd.type===NT_62P||nd.type===NT_62D)showDelayEditModal(editSelectedNode);else if(nd.type===NT_SR||nd.type===NT_RS)showRSInfoModal(editSelectedNode);else if(nd.type>=50&&nd.type<=56)showMXInfoModal(editSelectedNode);else showGateCfgModal(editSelectedNode);} function deleteSelectedNode(){if(editSelectedNode<0||editSelectedNode>=editNodes.length){showToast('请先选中模块','err');return;}var nd=editNodes[editSelectedNode],idx=editSelectedNode,lb=getNodeDisplayLabel(nd);showConfirm('🗑 确认删除','确定删除 '+escHtml(lb)+' 及其连线?',function(){var dt=nd.type;editLinks=editLinks.filter(function(l){return l.srcIdx!==idx&&l.dstIdx!==idx;});for(var i=0;iidx)editLinks[i].srcIdx--;if(editLinks[i].dstIdx>idx)editLinks[i].dstIdx--;}editNodes.splice(idx,1);editSelectedNode=-1;if(dt!==NT_INPUT&&dt!==NT_OUTPUT){var st=[];for(var s=0;s'+g.i+' '+g.n+'
'+g.d+'
';}h+='
';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt),info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;if(gt===NT_62P||gt===NT_62D){closeModal();showDelayInputModal(gt,ni,info);return;}addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;closeModal();drawEditor();};});} + function showAddGateModal(){var gates=[{t:NT_AND,n:'与门AND',d:'全1出1',i:'🔷'},{t:NT_OR,n:'或门OR',d:'有1出1',i:'🔶'},{t:NT_NOT,n:'非门NOT',d:'取反',i:'🔺'},{t:NT_62P,n:'62p延时',d:'开延时',i:'⏱️'},{t:NT_62D,n:'62d延时',d:'关延时',i:'⏱️'},{t:NT_RISING,n:'上升沿',d:'0→1脉冲',i:'⬆️'},{t:NT_FALLING,n:'下降沿',d:'1→0脉冲',i:'⬇️'},{t:NT_TOGGLE,n:'翻转元件',d:'输入变化翻转',i:'🔄'},{t:NT_SR,n:'SR触发器',d:'S优先',i:'🔒'},{t:NT_RS,n:'RS触发器',d:'R优先',i:'🔒'},{t:NT_MX_ADD,n:'加法元件',d:'两输入求和',i:'➕'},{t:NT_MX_SUB,n:'减法元件',d:'两输入差绝对值',i:'➖'},{t:NT_MX_MAX,n:'最大值元件',d:'两输入取大',i:'⬆️'},{t:NT_MX_MIN,n:'最小值元件',d:'两输入取小',i:'⬇️'},{t:NT_MX_MUL,n:'乘法元件',d:'两输入乘积',i:'✖️'},{t:NT_MX_DIV,n:'除法元件',d:'两输入之商(除0出0)',i:'➗'},{t:NT_MX_ABS,n:'绝对值元件',d:'单输入取绝对值',i:'📏'},{t:NT_MX_CMP,n:'比较元件',d:'A>B出1,否则出0',i:'⚖️'}];var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.querySelectorAll('#plc-modal .plc-modal-row').forEach(function(r){r.onclick=function(e){e.stopPropagation();var gt=parseInt(this.dataset.gt),info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0;if(gt===NT_62P||gt===NT_62D){closeModal();showDelayInputModal(gt,ni,info);return;}addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;closeModal();drawEditor();};});} function showDelayInputModal(gateType,nextIdx,info){var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}closeModal();addGateNode(gateType,nextIdx,v,info);editNextGateIdx[gateType]=nextIdx+1;drawEditor();};document.getElementById('plc-delay-input').onkeydown=function(e){if(e.key==='Enter')document.getElementById('plc-modal-ok').click();};setTimeout(function(){var inp=document.getElementById('plc-delay-input');if(inp){inp.focus();inp.select();}},100);} function showDelayEditModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},cur=nd.delayMs||1000;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-ok').onclick=function(){var v=parseInt(document.getElementById('plc-delay-input').value);if(isNaN(v)||v<0){showToast('无效延时','err');return;}editNodes[idx].delayMs=v;closeModal();drawEditor();showToast('延时更新: '+v+' ms','ok');};document.getElementById('plc-delay-input').onkeydown=function(e){if(e.key==='Enter')document.getElementById('plc-modal-ok').click();};setTimeout(function(){var inp2=document.getElementById('plc-delay-input');if(inp2){inp2.focus();inp2.select();}},100);} function showRSInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},sSrc='',rSrc='';for(var li=0;li

S(置位): '+(sSrc||'未连接')+'

R(复位): '+(rSrc||'未连接')+'

💡 拖输出到S/R引脚连线

';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;} - function showGateCfgModal(idx){var nd=editNodes[idx],cur=getNodeInPins(nd),info=NODE_INFO[nd.type]||{},mx=info.inMax||99;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-save').onclick=function(){var v=parseInt(document.getElementById('plc-gate-pins').value);if(isNaN(v)||v<1){showToast('最少1个','err');return;}if(v>mx){showToast('最多'+mx+'个','err');return;}editNodes[idx].inPins=v;closeModal();drawEditor();showToast(info.name+' 输入='+v,'ok');};} + function showMXInfoModal(idx){var nd=editNodes[idx],info=NODE_INFO[nd.type]||{},inPins=getNodeInPins(nd);var aSrc='',bSrc='';for(var li=0;li⚠️ 顺序敏感: A - B

';else if(nd.type===NT_MX_DIV)orderNote='

⚠️ 顺序敏感: A ÷ B (B=0→结果为0)

';else if(nd.type===NT_MX_CMP)orderNote='

⚠️ 顺序敏感: A > B → 1,否则 → 0

';else orderNote='

💡 输入可交换,顺序不影响结果

';var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;} +function showGateCfgModal(idx){var nd=editNodes[idx],cur=getNodeInPins(nd),info=NODE_INFO[nd.type]||{},mx=info.inMax||99;var h='';var d=document.createElement('div');d.innerHTML=h;document.body.appendChild(d.firstElementChild);document.getElementById('plc-modal-cancel').onclick=closeModal;document.getElementById('plc-modal-save').onclick=function(){var v=parseInt(document.getElementById('plc-gate-pins').value);if(isNaN(v)||v<1){showToast('最少1个','err');return;}if(v>mx){showToast('最多'+mx+'个','err');return;}editNodes[idx].inPins=v;closeModal();drawEditor();showToast(info.name+' 输入='+v,'ok');};} function addGateNode(gateType,nextIdx,delayMs,info){var pos=findEmptyPos();var nd={type:gateType,id:nextIdx,x:0,y:0,_lx:pos.x,_ly:pos.y,saddr:'',desc:'',no:0};if(delayMs>0)nd.delayMs=delayMs;editNodes.push(nd);showToast('已添加: '+(info?info.name:NODE_INFO[gateType].name)+' #'+nextIdx,'ok');} function findEmptyPos(){if(editNodes.length===0)return{x:0,y:0};var mxX=-Infinity,mnX=Infinity,mxY=-Infinity,mnY=Infinity;for(var i=0;imxX)mxX=n._lx;if(n._lxmxY)mxY=n._ly;if(n._ly=nodes.length||l.dstIdx>=nodes.length)continue;var sn=nodes[l.srcIdx],dn=nodes[l.dstIdx];var sIO=(sn.type===NT_OUTPUT||sn.type===NT_INPUT),dIO=(dn.type===NT_OUTPUT||dn.type===NT_INPUT);var dSR=(dn.type===NT_SR||dn.type===NT_RS);var sW=sIO?IW:GW,dW=dIO?IW:GW,dH=dIO?IH:(dSR?86:GH);var x1=sn._lx+sW/2,y1=sn._ly;var dIP=getNodeInPins(dn),epi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-dH/2+dH*(epi+1)/(dSR?3:(dIP+1)),x2=dn._lx-dW/2;var isAct=wiring&&wiring.fromIdx===l.srcIdx,isSel=(selectedLinkIdx===i);var mid=(x1+x2)/2,la=isAct||isSel?1:0.6,lw=isAct||isSel?3:2;var lc=isSel?'#FF5722':(isAct?'#FFC107':'rgba(180,200,220,'+la+')');ctx.beginPath();ctx.strokeStyle=isSel?'rgba(255,87,34,0.3)':'rgba(255,200,0,'+(la*0.2)+')';ctx.lineWidth=lw+4;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.beginPath();ctx.strokeStyle=lc;ctx.lineWidth=lw;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.fillStyle=lc;ctx.beginPath();ctx.moveTo(x2,y2);ctx.lineTo(x2-7,y2-3.5);ctx.lineTo(x2-7,y2+3.5);ctx.closePath();ctx.fill();if(isSel){var lx2=(x1+x2)/2,ly2=(y1+y2)/2-18;ctx.fillStyle='#FF5722';ctx.font='bold 14px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('✕ 右键删除',lx2,ly2);}}if(wiring&&wiring.fromIdx18?dt2.substring(0,16)+'…':dt2,x,y+14);}}else{var gi=n.id&0xFF,ri=0,ro=0;for(var li=0;li0){var isSR=(n.type===NT_SR||n.type===NT_RS),rp=isSR?2:(isIO?inP:1);for(var p=0;p0){ctx.fillStyle='#2e2e4a';ctx.beginPath();ctx.arc(x+hw,y,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#FF9800';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle='#FF9800';ctx.beginPath();ctx.arc(x+hw,y,3.5,0,Math.PI*2);ctx.fill();}}} + function drawGraphContent(ctx,nodes,links,selectedIdx,selectedLinkIdx,wiring){var IW=250,IH=80,GW=170,GH=66,CR=12,SO=3,LBW=8;for(var i=0;i=nodes.length||l.dstIdx>=nodes.length)continue;var sn=nodes[l.srcIdx],dn=nodes[l.dstIdx];var sIO=(sn.type===NT_OUTPUT||sn.type===NT_INPUT),dIO=(dn.type===NT_OUTPUT||dn.type===NT_INPUT);var dSR=(dn.type===NT_SR||dn.type===NT_RS);var sW=sIO?IW:GW,dW=dIO?IW:GW,dH=dIO?IH:(dSR?86:GH);var x1=sn._lx+sW/2,y1=sn._ly;var dIP=getNodeInPins(dn),epi=(l.dstPin!==undefined)?l.dstPin:0;var y2=dn._ly-dH/2+dH*(epi+1)/(dSR?3:(dIP+1)),x2=dn._lx-dW/2;var isAct=wiring&&wiring.fromIdx===l.srcIdx,isSel=(selectedLinkIdx===i);var mid=(x1+x2)/2,la=isAct||isSel?1:0.6,lw=isAct||isSel?3:2;var lc=isSel?'#FF5722':(isAct?'#FFC107':'rgba(180,200,220,'+la+')');ctx.beginPath();ctx.strokeStyle=isSel?'rgba(255,87,34,0.3)':'rgba(255,200,0,'+(la*0.2)+')';ctx.lineWidth=lw+4;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.beginPath();ctx.strokeStyle=lc;ctx.lineWidth=lw;ctx.moveTo(x1,y1);ctx.bezierCurveTo(mid,y1,mid,y2,x2,y2);ctx.stroke();ctx.fillStyle=lc;ctx.beginPath();ctx.moveTo(x2,y2);ctx.lineTo(x2-7,y2-3.5);ctx.lineTo(x2-7,y2+3.5);ctx.closePath();ctx.fill();if(isSel){var lx2=(x1+x2)/2,ly2=(y1+y2)/2-18;ctx.fillStyle='#FF5722';ctx.font='bold 14px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('✕ 右键删除',lx2,ly2);}}if(wiring&&wiring.fromIdx18?dt2.substring(0,16)+'…':dt2,x,y+14);}}else{var gi=n.id&0xFF,ri=0,ro=0;for(var li=0;li0){var isSR=(n.type===NT_SR||n.type===NT_RS),rp=isSR?2:(isIO?inP:inP);for(var p=0;p=50&&n.type<=57);if(isMX){ctx.fillStyle='#fff';ctx.font='bold 10px monospace';ctx.textAlign='right';ctx.textBaseline='middle';ctx.fillText(p===0?'A':'B',x-hw-12,py);}}}if(info.outPins>0){ctx.fillStyle='#2e2e4a';ctx.beginPath();ctx.arc(x+hw,y,7,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#FF9800';ctx.lineWidth=2;ctx.stroke();ctx.fillStyle='#FF9800';ctx.beginPath();ctx.arc(x+hw,y,3.5,0,Math.PI*2);ctx.fill();}}} /* ================================================================ * XML 解析 (与原工程完全一致) * ================================================================ */ - function parseLogicXml(xmlStr,sourceName){var parser=new DOMParser(),doc=parser.parseFromString(xmlStr,'text/xml'),root=doc.documentElement;if(!root||root.nodeName!=='PLCConfig'){showToast('根元素不是 PLCConfig','err');return;}var thisKey=root.getAttribute('key')||sourceName,thisName=root.getAttribute('name')||'';var g={key:thisKey,name:thisName,nodes:[],links:[]};var GTM={INPUT:1,OUTPUT:0,OR:2,AND:3,NOT:4,T62P:5,T62D:6,RISING:7,SR:8,RS:9,FALLING:10};var sigMap={};function ens(ioType,no,x,y){var k=ioType+':'+no;if(sigMap[k]!==undefined){if(x!==undefined){g.nodes[sigMap[k]].x=x;g.nodes[sigMap[k]].y=y;}return sigMap[k];}g.nodes.push({type:ioType,id:no,x:x||0,y:y||0});sigMap[k]=g.nodes.length-1;return g.nodes.length-1;}function eng(type,id,x,y,inPins,delayMs){for(var i=0;i=0)g.links.push({src_type:g.nodes[upi].type,src_id:g.nodes[upi].id,dst_type:t,dst_id:gid});}c=c.nextElementSibling;}var ie=ce.firstElementChild;while(ie&&ie.nodeName!=='Inputs')ie=ie.nextElementSibling;if(ie){var s=ie.firstElementChild;while(s){if(s.nodeName==='Signal'){var no=parseInt(s.getAttribute('no'));ens(NT_INPUT,no,parseInt(s.getAttribute('x')),parseInt(s.getAttribute('y')));g.links.push({src_type:NT_INPUT,src_id:no,dst_type:t,dst_id:gid});}s=s.nextElementSibling;}}var oe=ce.firstElementChild;while(oe&&oe.nodeName!=='Outputs')oe=oe.nextElementSibling;if(oe){var s2=oe.firstElementChild;while(s2){if(s2.nodeName==='Signal'){var no2=parseInt(s2.getAttribute('no'));ens(NT_OUTPUT,no2,parseInt(s2.getAttribute('x')),parseInt(s2.getAttribute('y')));g.links.push({src_type:t,src_id:gid,dst_type:NT_OUTPUT,dst_id:no2});}s2=s2.nextElementSibling;}}return gi;}var chains=root.getElementsByTagName('Chain');for(var ci=0;ci=0)g.links.push({src_type:g.nodes[upi].type,src_id:g.nodes[upi].id,dst_type:t,dst_id:gid});}c=c.nextElementSibling;}var ie=ce.firstElementChild;while(ie&&ie.nodeName!=='Inputs')ie=ie.nextElementSibling;if(ie){var s=ie.firstElementChild;while(s){if(s.nodeName==='Signal'){var no=parseInt(s.getAttribute('no'));ens(NT_INPUT,no,parseInt(s.getAttribute('x')),parseInt(s.getAttribute('y')));g.links.push({src_type:NT_INPUT,src_id:no,dst_type:t,dst_id:gid});}s=s.nextElementSibling;}}var oe=ce.firstElementChild;while(oe&&oe.nodeName!=='Outputs')oe=oe.nextElementSibling;if(oe){var s2=oe.firstElementChild;while(s2){if(s2.nodeName==='Signal'){var no2=parseInt(s2.getAttribute('no'));ens(NT_OUTPUT,no2,parseInt(s2.getAttribute('x')),parseInt(s2.getAttribute('y')));g.links.push({src_type:t,src_id:gid,dst_type:NT_OUTPUT,dst_id:no2});}s2=s2.nextElementSibling;}}return gi;}var chains=root.getElementsByTagName('Chain');for(var ci=0;ci'; } + function sx(n, pin) { var s = '= 0) s += ' pin="' + pin + '"'; s += ' />'; return s; } function gt(n, ni) { var tn = tN[n.type] || '?'; var a = ' type="' + tn + '" id="' + (n.id & 0xFF) + '" x="' + Math.round(n._lx) + '" y="' + Math.round(n._ly) + '"'; @@ -500,7 +533,20 @@ var PlcConfigPage = (function() { function onLeave(){editWiring=null;editDragNode=null;editSelectedNode=-1;editSelectedLink=-1;} function onData(data){if(!data||typeof data!=='object')return;if(data.type==='file_content'){var p=data.path,c=data.content||'',e=data.error||'';loadedFiles[p]=e?null:c;if(PlcConfigPage._pending&&PlcConfigPage._pending[p]){PlcConfigPage._pending[p](e?null:c);delete PlcConfigPage._pending[p];}if(PlcConfigPage._retryList){var rIdx=PlcConfigPage._retryList.indexOf(p);if(rIdx>=0)PlcConfigPage._retryList.splice(rIdx,1);}return;}if(data.type==='file_saved'){if(data.success)showToast('已保存: '+data.path,'ok');else showToast('保存失败: '+(data.error||''),'err');}} - return {render:render,onEnter:onEnter,onLeave:onLeave,onData:onData,_pending:{}}; + return {render:render,onEnter:onEnter,onLeave:onLeave,onData:onData,_pending:{}, + _addSigIO:function(no,saddr,desc,ioType){ + if(mode!=='edit')return; + var type=ioType===1?NT_INPUT:NT_OUTPUT; + editNodes.push({type:type,id:no,x:0,y:0,_lx:type===NT_INPUT?-260:260,_ly:editNodes.filter(function(n){return n.type===type;}).length*110,saddr:saddr,desc:desc||'',no:no}); + drawEditor();showToast('已添加'+(type===NT_INPUT?'输入':'输出')+': '+saddr,'ok'); + }, + _addOp:function(gt){ + if(mode!=='edit')return; + var info=NODE_INFO[gt],ni=editNextGateIdx[gt]||0; + if(gt===NT_62P||gt===NT_62D){addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor();return;} + addGateNode(gt,ni,0,info);editNextGateIdx[gt]=ni+1;drawEditor(); + } + }; })(); Router.register('#plc_config', function(c) { PlcConfigPage.render(c); }); diff --git a/config/web/js/pages/plc_debug.js b/config/web/js/pages/plc_debug.js index 4ec6731..f79ea7c 100644 --- a/config/web/js/pages/plc_debug.js +++ b/config/web/js/pages/plc_debug.js @@ -1,70 +1,58 @@ /** * @file plc_debug.js - * @brief PLC 调试页 — 指示灯/按键绑定 out 信号 + 增量刷新 + * @brief PLC 调试页 — 三列布局:左(遥信输入·按钮/遥测输入·设值) | 中(信号表) | 右(遥信输出·LED/遥测输出·展示) + * @details 所有条目从中间信号表选择添加,增量刷新 */ -var PlcDebugLeds = []; -var PlcDebugButtons = []; +var PlcDebugStIn = []; // 遥信输入(按钮) +var PlcDebugMxIn = []; // 遥测输入(设值) +var PlcDebugStOut = []; // 遥信输出(LED) +var PlcDebugMxOut = []; // 遥测输出(展示) Router.register('#plc_debug', function(c) { PlcDebugPage.render(c); }); var PlcDebugPage = (function() { var outSignals = []; - var ledChecked = {}, btnChecked = {}, sigChecked = {}; + var stInChk = {}, mxInChk = {}, stOutChk = {}, mxOutChk = {}, sigChk = {}; function getOut() { var out = SignalStore.get('out'); return (Array.isArray(out) && out.length) ? out : outSignals; } - function showPicker(cb) { - var sigs = getOut(); - if (!sigs.length) { toast(I18n.t('plc_debug.no_sig'), 'error'); return; } - var rows = ''; - sigs.forEach(function(s) { - rows += '
' - + '' + esc(s.saddr) + '' - + (s.desc ? ' ' + esc(s.desc) + '' : '') - + '' + fmtVal(s.val) + '
'; - }); - var ov = document.createElement('div'); - ov.className = 'modal-overlay'; - ov.innerHTML = ''; - document.body.appendChild(ov); - document.getElementById('plc-pick-cancel').onclick = function() { ov.remove(); }; - ov.addEventListener('click', function(e) { if (e.target === ov) ov.remove(); }); - ov.querySelectorAll('.plc-pick-row').forEach(function(r) { - r.onclick = function() { - var sa = this.getAttribute('data-saddr'), de = this.getAttribute('data-desc'); - ov.remove(); - if (confirm(I18n.t('plc_debug.confirm_bind') + ' ' + sa + ' ?')) cb(sa, de); - }; - }); - } - function findSig(sa) { - for (var i = 0; i < outSignals.length; i++) if (outSignals[i].saddr === sa) return outSignals[i]; + for (var i = 0; i < outSignals.length; i++) + if (outSignals[i].saddr === sa) return outSignals[i]; return null; } function refreshData() { var o = getOut(); if (Array.isArray(o)) outSignals = o; - PlcDebugLeds.forEach(function(l) { var f = findSig(l.saddr); l.val = f ? f.val : 0; }); - PlcDebugButtons.forEach(function(b) { var f = findSig(b.saddr); b.val = f ? f.val : 0; }); + PlcDebugStIn.forEach(function(l) { var f = findSig(l.saddr); l.val = f ? f.val : 0; }); + PlcDebugMxIn.forEach(function(m) { var f = findSig(m.saddr); m.val = f ? f.val : 0; }); + PlcDebugStOut.forEach(function(b) { var f = findSig(b.saddr); b.val = f ? f.val : 0; }); + PlcDebugMxOut.forEach(function(m) { var f = findSig(m.saddr); m.val = f ? f.val : 0; }); } function incrRefresh() { refreshData(); - PlcDebugLeds.forEach(function(l, i) { + PlcDebugStIn.forEach(function(b, i) { + var on = parseFloat(b.val) > 0; + var ve = document.getElementById('sti-val-' + i); + if (ve) { ve.textContent = '(' + fmtVal(b.val) + ')'; ve.style.color = on ? 'var(--accent)' : 'var(--text-muted)'; } + }); + PlcDebugMxIn.forEach(function(m, i) { + var ve = document.getElementById('mxi-cur-' + i); + if (ve) ve.textContent = '(' + fmtVal(m.val) + ')'; + }); + PlcDebugStOut.forEach(function(l, i) { var on = parseFloat(l.val) > 0; - var ve = document.getElementById('led-val-' + i), ie = document.getElementById('led-icon-' + i); - if (ve) { ve.textContent = '(' + l.val + ')'; ve.style.color = on ? 'var(--accent)' : 'var(--text-muted)'; } + var ve = document.getElementById('sto-val-' + i), ie = document.getElementById('sto-icon-' + i); + if (ve) { ve.textContent = '(' + fmtVal(l.val) + ')'; ve.style.color = on ? 'var(--accent)' : 'var(--text-muted)'; } if (ie) ie.className = 'plc-led' + (on ? ' plc-led-on' : ''); }); - PlcDebugButtons.forEach(function(b, j) { - var ve = document.getElementById('btn-val-' + j); - if (ve) { ve.textContent = '(' + b.val + ')'; ve.style.color = parseFloat(b.val) > 0 ? 'var(--accent)' : 'var(--text-muted)'; } + PlcDebugMxOut.forEach(function(m, i) { + var ve = document.getElementById('mxo-val-' + i); + if (ve) ve.textContent = fmtVal(m.val); }); } @@ -72,111 +60,168 @@ var PlcDebugPage = (function() { function delChecked() { var todel = []; - for (var k in ledChecked) if (ledChecked.hasOwnProperty(k)) todel.push({ type: 'led', idx: parseInt(k) }); - for (var k in btnChecked) if (btnChecked.hasOwnProperty(k)) todel.push({ type: 'btn', idx: parseInt(k) }); + for (var k in stInChk) if (stInChk.hasOwnProperty(k)) todel.push({ t: 'sti', idx: parseInt(k) }); + for (var k in mxInChk) if (mxInChk.hasOwnProperty(k)) todel.push({ t: 'mxi', idx: parseInt(k) }); + for (var k in stOutChk) if (stOutChk.hasOwnProperty(k)) todel.push({ t: 'sto', idx: parseInt(k) }); + for (var k in mxOutChk) if (mxOutChk.hasOwnProperty(k)) todel.push({ t: 'mxo', idx: parseInt(k) }); if (!todel.length) { toast(I18n.t('toast.no_selected'), 'error'); return; } if (!confirm(I18n.t('plc_debug.confirm_del') + ' ' + todel.length + ' ?')) return; todel.sort(function(a, b) { return b.idx - a.idx; }).forEach(function(t) { - if (t.type === 'led') PlcDebugLeds.splice(t.idx, 1); - else PlcDebugButtons.splice(t.idx, 1); + if (t.t === 'sti') PlcDebugStIn.splice(t.idx, 1); + else if (t.t === 'mxi') PlcDebugMxIn.splice(t.idx, 1); + else if (t.t === 'sto') PlcDebugStOut.splice(t.idx, 1); + else PlcDebugMxOut.splice(t.idx, 1); }); - ledChecked = {}; btnChecked = {}; + stInChk = {}; mxInChk = {}; stOutChk = {}; mxOutChk = {}; renderAll(); } + function makeBlock(title, icon, arr, chkObj, idPrefix, blockType) { + var html = ''; + if (!arr.length) { + html = '
暂无
'; + } else { + arr.forEach(function(item, i) { + var on = parseFloat(item.val) > 0, chk = !!chkObj[i]; + html += '
'; + html += ''; + + if (blockType === 'btn') { + html += ''; + } else if (blockType === 'led') { + html += '
'; + } + + html += '
'; + if (item.saddr) { + html += '' + esc(item.saddr) + ''; + if (item.desc) html += '' + esc(item.desc) + ''; + } else { + html += '未绑定'; + } + html += '
'; + + if (blockType === 'btn' || blockType === 'led') { + html += '(' + fmtVal(item.val) + ')'; + } else if (blockType === 'mxi') { + html += '(' + fmtVal(item.val) + ')'; + html += ''; + html += ''; + } else if (blockType === 'mxo') { + html += '' + fmtVal(item.val) + ''; + } + html += '
'; + }); + } + return '
' + icon + ' ' + title + '' + + '' + arr.length + '
' + + '
' + html + '
'; + } + function renderAll() { var c = document.getElementById('plc-content'); if (!c) return; refreshData(); - /* 指示灯列 */ - var ledHtml = ''; - if (!PlcDebugLeds.length) { - ledHtml = '
' + I18n.t('plc_debug.no_led') + '
'; - } else { - PlcDebugLeds.forEach(function(l, i) { - var on = parseFloat(l.val) > 0, chk = !!ledChecked[i]; - ledHtml += '
' - + '
' - + '
' - + (l.saddr ? '' + esc(l.saddr) + ' (' + fmtVal(l.val) + ')' - + (l.desc ? '' + esc(l.desc) + '' : '') - : '' + I18n.t('plc_debug.unbound') + '') - + '
' - + '' - + '
'; - }); - } - - /* 按键列 */ - var btnHtml = ''; - if (!PlcDebugButtons.length) { - btnHtml = '
' + I18n.t('plc_debug.no_btn') + '
'; - } else { - PlcDebugButtons.forEach(function(b, j) { - var chk = !!btnChecked[j]; - btnHtml += '
' - + '' - + '
' - + (b.saddr ? '' + esc(b.saddr) + ' (' + fmtVal(b.val) + ')' - + (b.desc ? '' + esc(b.desc) + '' : '') - : '' + I18n.t('plc_debug.unbound') + '') - + '
' - + '' - + '
'; - }); - } - - /* 信号列 */ + /* 信号表 (中间列) */ var o = getOut(); var sigHtml = ''; if (!o.length) { - sigHtml = '
' + I18n.t('plc_debug.no_sig') + '
'; + sigHtml = '
' + I18n.t('plc_debug.no_sig') + '
'; } else { o.forEach(function(s, i) { - var chk = !!sigChecked[i]; - sigHtml += '
' - + '' - + '' + esc(s.saddr) + '' - + '' + esc(s.desc || '') + '' - + '' + fmtVal(s.val) + '' + var chk = !!sigChk[i]; + sigHtml += '
' + + '' + + '#' + i + '' + + '' + esc(s.saddr) + '' + + (s.desc ? '' + esc(s.desc) + '' : '') + + '' + fmtVal(s.val) + '' + '
'; }); } c.innerHTML = '
' - + '
💡 ' + I18n.t('plc_debug.led_col') + '' - + '
' - + '' - + '
' + ledHtml + '
' - + '
📋 ' + I18n.t('plc_debug.sig_col') + '' - + '
' - + '' - + '' - + '
' + sigHtml + '
' - + '
🔘 ' + I18n.t('plc_debug.btn_col') + '' - + '
' - + '' - + '
' + btnHtml + '
' + /* 左列: 遥信输入(上) + 遥测输入(下) */ + + '
' + + makeBlock('遥信输入', '📤', PlcDebugStIn, stInChk, 'sti', 'btn') + + makeBlock('遥测输入', '📊', PlcDebugMxIn, mxInChk, 'mxi', 'mxi') + + '
' + /* 中列: 信号表 */ + + '
📋 信号表' + + '' + o.length + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + sigHtml + '
' + /* 右列: 遥信输出(上) + 遥测输出(下) */ + + '
' + + makeBlock('遥信输出', '📥', PlcDebugStOut, stOutChk, 'sto', 'led') + + makeBlock('遥测输出', '📊', PlcDebugMxOut, mxOutChk, 'mxo', 'mxo') + + '
' + '
'; + /* 绑定事件 */ + [].forEach.call(c.querySelectorAll('.plc-btn-icon'), function(el) { + el.onclick = function() { + var m = this.id.match(/^sti-icon-(\d+)$/); + if (m) PlcDebugPage._toggleStIn(parseInt(m[1])); + }; + }); + [].forEach.call(c.querySelectorAll('.plc-led'), function(el) { + el.onclick = function() { + var m = this.id.match(/^sto-icon-(\d+)$/); + if (m) PlcDebugPage._bindStOut(parseInt(m[1])); + }; + }); + [].forEach.call(c.querySelectorAll('[id$="-setbtn-"]'), function(el) { + el.onclick = function() { + var m = this.id.match(/^mxi-setbtn-(\d+)$/); + if (m) PlcDebugPage._setMxIn(parseInt(m[1])); + }; + }); + [].forEach.call(c.querySelectorAll('.plc-sig-check-item'), function(row) { + row.onclick = function(e) { + if (e.target.tagName === 'INPUT') return; + var cb = this.querySelector('input[type="checkbox"]'); + if (cb) { + cb.checked = !cb.checked; + var si = parseInt(cb.getAttribute('data-sig-idx')); + if (!isNaN(si)) { if (cb.checked) sigChk[si] = true; else delete sigChk[si]; } + } + }; + }); + [].forEach.call(c.querySelectorAll('.plc-sig-check-item input[type="checkbox"]'), function(cb) { + cb.onclick = function(e) { + e.stopPropagation(); + var si = parseInt(this.getAttribute('data-sig-idx')); + if (!isNaN(si)) { if (this.checked) sigChk[si] = true; else delete sigChk[si]; } + }; + }); + updateDelBtn(); } function updateDelBtn() { var el = document.getElementById('plc-del-btn'); if (!el) return; - var n = countChecked(ledChecked) + countChecked(btnChecked); + var n = countChecked(stInChk) + countChecked(mxInChk) + countChecked(stOutChk) + countChecked(mxOutChk); el.textContent = I18n.t('plc_debug.del_sel') + (n ? ' (' + n + ')' : ''); el.disabled = !n; } return { render: function(c) { - ledChecked = {}; btnChecked = {}; sigChecked = {}; + stInChk = {}; mxInChk = {}; stOutChk = {}; mxOutChk = {}; sigChk = {}; refreshData(); c.innerHTML = '
' - + '
' + I18n.t('plc_debug.title') + '' + + '
📟 ' + I18n.t('plc_debug.title') + '' + '
' + '' + '' @@ -184,38 +229,105 @@ var PlcDebugPage = (function() { + '
'; renderAll(); }, - _addLed: function() { PlcDebugLeds.push({ saddr: '', desc: '', val: 0 }); renderAll(); }, - _addBtn: function() { PlcDebugButtons.push({ saddr: '', desc: '', val: 0 }); renderAll(); }, - _bindLed: function(i) { showPicker(function(sa, de) { PlcDebugLeds[i].saddr = sa; PlcDebugLeds[i].desc = de; var f = findSig(sa); if (f) PlcDebugLeds[i].val = f.val; renderAll(); }); }, - _toggleBtn: function(j) { - var b = PlcDebugButtons[j]; - if (!b.saddr) { showPicker(function(sa, de) { b.saddr = sa; b.desc = de; var f = findSig(sa); if (f) b.val = f.val; renderAll(); }); return; } + _toggleChk: function(prefix, i, v) { + if (prefix === 'sti') { if (v) stInChk[i] = true; else delete stInChk[i]; } + if (prefix === 'mxi') { if (v) mxInChk[i] = true; else delete mxInChk[i]; } + if (prefix === 'sto') { if (v) stOutChk[i] = true; else delete stOutChk[i]; } + if (prefix === 'mxo') { if (v) mxOutChk[i] = true; else delete mxOutChk[i]; } + updateDelBtn(); + }, + _toggleStIn: function(i) { + var b = PlcDebugStIn[i]; + if (!b.saddr) { + PlcDebugPage._showPicker(function(sa, de) { + b.saddr = sa; b.desc = de; + var f = findSig(sa); if (f) b.val = f.val; + renderAll(); + }); + return; + } var nv = parseFloat(b.val) > 0 ? 0 : 1; WsClient.send({ curd: 'set', signal_type: 'out', saddr: b.saddr, signal_data: String(nv), setting_zone: '0' }); b.val = nv; - /* 同步写入 SignalStore,确保 refreshData() 不覆盖新值 */ var outs = SignalStore.get('out'); - for (var i = 0; i < outs.length; i++) { - if (outs[i].saddr === b.saddr) { outs[i].val = String(nv); break; } - } + for (var j = 0; j < outs.length; j++) + if (outs[j].saddr === b.saddr) { outs[j].val = String(nv); break; } renderAll(); }, - _toggleLed: function(i, v) { ledChecked[i] = v; updateDelBtn(); }, - _toggleBtnChk: function(j, v) { btnChecked[j] = v; updateDelBtn(); }, - _toggleSig: function(i, v) { sigChecked[i] = v; }, + _bindStOut: function(i) { + PlcDebugPage._showPicker(function(sa, de) { + PlcDebugStOut[i].saddr = sa; PlcDebugStOut[i].desc = de; + var f = findSig(sa); if (f) PlcDebugStOut[i].val = f.val; + renderAll(); + }); + }, + _setMxIn: function(i) { + var m = PlcDebugMxIn[i]; + if (!m || !m.saddr) { + PlcDebugPage._showPicker(function(sa, de) { + if (!PlcDebugMxIn[i]) return; + PlcDebugMxIn[i].saddr = sa; PlcDebugMxIn[i].desc = de; + var f = findSig(sa); if (f) PlcDebugMxIn[i].val = f.val; + renderAll(); + }); + return; + } + var inp = document.getElementById('mxi-input-' + i); + if (!inp) { toast('找不到输入框', 'error'); return; } + var nv = inp.value.trim(); + if (nv === '') { toast('请输入值', 'error'); return; } + var sent = WsClient.send({ curd: 'set', signal_type: 'out', saddr: m.saddr, signal_data: nv, setting_zone: '0' }); + if (!sent) { toast('发送失败', 'error'); return; } + m.val = parseFloat(nv) || 0; + m.inputVal = ''; + var outs = SignalStore.get('out'); + for (var j = 0; j < outs.length; j++) + if (outs[j].saddr === m.saddr) { outs[j].val = nv; break; } + renderAll(); + }, + _showPicker: function(cb) { + var sigs = getOut(); + if (!sigs.length) { toast(I18n.t('plc_debug.no_sig'), 'error'); return; } + var rows = ''; + sigs.forEach(function(s) { + rows += '
' + + '' + esc(s.saddr) + '' + + (s.desc ? ' ' + esc(s.desc) + '' : '') + + '' + fmtVal(s.val) + '
'; + }); + var ov = document.createElement('div'); + ov.className = 'modal-overlay'; + ov.innerHTML = ''; + document.body.appendChild(ov); + document.getElementById('plc-pick-cancel').onclick = function() { ov.remove(); }; + ov.addEventListener('click', function(e) { if (e.target === ov) ov.remove(); }); + ov.querySelectorAll('.plc-pick-row').forEach(function(r) { + r.onclick = function() { + var sa = this.getAttribute('data-saddr'), de = this.getAttribute('data-desc'); + ov.remove(); + if (confirm(I18n.t('plc_debug.confirm_bind') + ' ' + sa + ' ?')) cb(sa, de); + }; + }); + }, _moveSel: function(target) { var picked = []; - for (var k in sigChecked) if (sigChecked.hasOwnProperty(k)) picked.push(parseInt(k)); + for (var k in sigChk) if (sigChk.hasOwnProperty(k)) picked.push(parseInt(k)); if (!picked.length) { toast(I18n.t('toast.no_selected'), 'error'); return; } picked.sort(function(a, b) { return a - b; }).forEach(function(i) { var s = outSignals[i]; if (!s) return; - if (target === 'led') PlcDebugLeds.push({ saddr: s.saddr, desc: s.desc || '', val: s.val || 0 }); - else PlcDebugButtons.push({ saddr: s.saddr, desc: s.desc || '', val: s.val || 0 }); + var item = { saddr: s.saddr, desc: s.desc || '', val: s.val || 0 }; + if (target === 'mxi') item.inputVal = ''; + if (target === 'sti') PlcDebugStIn.push(item); + else if (target === 'sto') PlcDebugStOut.push(item); + else if (target === 'mxi') PlcDebugMxIn.push(item); + else PlcDebugMxOut.push(item); }); - sigChecked = {}; renderAll(); + sigChk = {}; renderAll(); }, - _refresh: function() { refreshData(); incrRefresh(); }, + _refresh: function() { refreshData(); renderAll(); }, incrRefresh: function() { refreshData(); incrRefresh(); }, _delChecked: delChecked, }; diff --git a/src/system/libplc/src/plc.cpp b/src/system/libplc/src/plc.cpp index 970c17c..826ed5b 100644 --- a/src/system/libplc/src/plc.cpp +++ b/src/system/libplc/src/plc.cpp @@ -51,9 +51,12 @@ static int g_plc_dbg_enabled = 0; /**< PLC 调试打印开关, 0=关闭, 1=打 * 模块常量 & 全局变量 * ================================================================== */ -#define MAX_OUT_NODES 20 /**< 最大输出节点数 */ +#define MAX_OUT_ST_NODES 20 /**< 最大遥信输出节点数 */ +#define MAX_OUT_MX_NODES 20 /**< 最大遥测输出节点数 */ -LOCAL uint8_t g_plc_st_out[MAX_OUT_NODES] = {0}; + +LOCAL uint8_t g_plc_st_out[MAX_OUT_ST_NODES] = {0}; +LOCAL float g_plc_mx_out[MAX_OUT_MX_NODES] = {0.0f}; LOCAL uint32_t *gp_run_cnt_in = NULL; LOCAL uint8_t *gp_st[10] = {NULL}; @@ -87,6 +90,15 @@ std::vector g_plc_cfg = {}; #define NODE_TYPE_SR_LATCH 8 /**< SR 触发器 (S 优先) */ #define NODE_TYPE_RS_LATCH 9 /**< RS 触发器 (R 优先) */ #define NODE_TYPE_FALLING_EDGE 10 /**< 下降沿检测 (1→0 脉冲) */ +#define NODE_TYPE_TOGGLE 11 /**< 翻转元件 (输入变化时输出翻转) */ +#define NODE_TYPE_MX_ADD 50 /**< 遥测加法元件 (两输入求和) */ +#define NODE_TYPE_MX_SUB 51 /**< 遥测减法元件 (A - B,可负数) */ +#define NODE_TYPE_MX_MAX 52 /**< 遥测最大值元件 (两输入取大) */ +#define NODE_TYPE_MX_MIN 53 /**< 遥测最小值元件 (两输入取小) */ +#define NODE_TYPE_MX_MUL 54 /**< 遥测乘法元件 (两输入之积) */ +#define NODE_TYPE_MX_DIV 55 /**< 遥测除法元件 (两输入之商) */ +#define NODE_TYPE_MX_ABS 56 /**< 遥测绝对值元件 (单输入取绝对值) */ +#define NODE_TYPE_MX_CMP 57 /**< 遥测比较元件 (A>B 出1,否则出0) */ /** 动态链表 — 硬件点 (运行时 I/O 状态) */ typedef struct HWPoint @@ -134,7 +146,7 @@ typedef struct int src_id; int dest_type; int dest_id; - int dst_pin; /**< 目标引脚 (0=S, 1=R, -1=默认), 仅 SR/RS 有效 */ + int dst_pin; /**< 目标引脚 (0=A/S, 1=B/R, -1=默认) */ } LogicLink; /** 逻辑图 — 一张完整的 PLC 逻辑图 */ @@ -183,6 +195,14 @@ LOCAL int plc_set_out_by_id(int id, int value) return -1; } + /* 遥测输出 (plc.mx.out.*): DATA_TYPE_F32,需要完整浮点值 */ + if (strstr(saddr, "plc.mx.") == saddr) + { + float fval = (float)value; + return dc_set_out_signal_val(saddr, &fval, "plc"); + } + + uint8_t val = (uint8_t)(value ? 1 : 0); return dc_set_out_signal_val(saddr, &val, "plc"); } @@ -223,15 +243,15 @@ LOCAL int plc_get_out_value_by_id(int id) } else if (data_type == DATA_TYPE_U16) { - return (*(uint16_t *)p_data) ? 1 : 0; + return *(uint16_t *)p_data; } else if (data_type == DATA_TYPE_U32) { - return (*(uint32_t *)p_data) ? 1 : 0; + return *(uint32_t *)p_data; } else if (data_type == DATA_TYPE_F32) { - return (*(float *)p_data > 0.5f) ? 1 : 0; + return (int)(*(float *)p_data); } return 0; @@ -451,6 +471,42 @@ static int gate_type_from_xml_str(const char *type_str) return NODE_TYPE_FALLING_EDGE; } + if (strcmp(type_str, "TOGGLE") == 0) + { + return NODE_TYPE_TOGGLE; + } + if (strcmp(type_str, "MX_ADD") == 0) + { + return NODE_TYPE_MX_ADD; + } + if (strcmp(type_str, "MX_SUB") == 0) + { + return NODE_TYPE_MX_SUB; + } + if (strcmp(type_str, "MX_MAX") == 0) + { + return NODE_TYPE_MX_MAX; + } + if (strcmp(type_str, "MX_MIN") == 0) + { + return NODE_TYPE_MX_MIN; + } + if (strcmp(type_str, "MX_MUL") == 0) + { + return NODE_TYPE_MX_MUL; + } + if (strcmp(type_str, "MX_DIV") == 0) + { + return NODE_TYPE_MX_DIV; + } + if (strcmp(type_str, "MX_ABS") == 0) + { + return NODE_TYPE_MX_ABS; + } + if (strcmp(type_str, "MX_CMP") == 0) + { + return NODE_TYPE_MX_CMP; + } return -1; } @@ -676,7 +732,8 @@ static int parse_xml_comb(tinyxml2::XMLElement *comb_el, LogicGraph *graph) { LogicLink &l = graph->links[graph->link_count++]; memset(&l, 0, sizeof(l)); - l.dst_pin = -1; + const char *pin_str_cb = sig->Attribute("pin"); + l.dst_pin = pin_str_cb ? atoi(pin_str_cb) : -1; l.src_type = NODE_TYPE_HW_INPUT; l.src_id = graph->nodes[si].id; l.dest_type = gn.type; @@ -783,6 +840,18 @@ int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *gra gn.timer_pending_output = -1; gn.type = gate_type_from_xml_str(gate->Attribute("type")); gn.id = gate->IntAttribute("id"); + + /* 读取延时毫秒 (62P/62D 链级 Gate) */ + const char *chain_delay = gate->Attribute("delayMs"); + + if (chain_delay) + { + gn.delay_ms = atoi(chain_delay); + } + else + { + gn.delay_ms = 1000; + } } tinyxml2::XMLElement *outputs = chain->FirstChildElement("Outputs"); @@ -952,7 +1021,8 @@ int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *gra { LogicLink &l = cur->links[cur->link_count++]; memset(&l, 0, sizeof(l)); - l.dst_pin = -1; + const char *pin_str_hc = sig->Attribute("pin"); + l.dst_pin = pin_str_hc ? atoi(pin_str_hc) : -1; l.src_type = NODE_TYPE_HW_INPUT; l.src_id = cur->nodes[si].id; l.dest_type = cur->nodes[cgi].type; @@ -1139,9 +1209,9 @@ static int collect_input_signals(LogicGraph *graph, LogicNode *element_node, // SR/RS: 按 dst_pin 填入对应位置 int is_srrs = (element_node->type == NODE_TYPE_SR_LATCH || element_node->type == NODE_TYPE_RS_LATCH); - if (is_srrs && link->dst_pin >= 0) + if (link->dst_pin >= 0) { - if (input_count < 2) + if (link->dst_pin < max_inputs) { input_values[link->dst_pin] = src_node->value; if (link->dst_pin >= input_count) @@ -1342,6 +1412,121 @@ static void execute_rs_latch(LogicNode *node, int *input_values, int input_count } } +static void execute_mx_add_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + int b = (input_count >= 2) ? input_values[1] : 0; + node->value = a + b; + if (input_count >= 2) + { + PLC_DBG(" 加法元件:%d + %d = %d\n", a, b, node->value); + } + else + { + PLC_DBG(" 加法元件:输入不足(%d),输出=%d\n", input_count, node->value); + } +} +static void execute_mx_sub_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + int b = (input_count >= 2) ? input_values[1] : 0; + node->value = a - b; + if (input_count >= 2) + { + PLC_DBG(" 减法元件:%d - %d = %d\n", a, b, node->value); + } + else + { + PLC_DBG(" 减法元件:输入不足(%d),输出=%d\n", input_count, node->value); + } +} + +static void execute_mx_max_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + int b = (input_count >= 2) ? input_values[1] : 0; + node->value = (a > b) ? a : b; + if (input_count >= 2) + { + PLC_DBG(" 最大值元件:max(%d, %d) = %d\n", a, b, node->value); + } + else + { + PLC_DBG(" 最大值元件:输入不足(%d),输出=%d\n", input_count, node->value); + } +} + +static void execute_mx_min_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + int b = (input_count >= 2) ? input_values[1] : 0; + node->value = (a < b) ? a : b; + if (input_count >= 2) + { + PLC_DBG(" 最小值元件:min(%d, %d) = %d\n", a, b, node->value); + } + else + { + PLC_DBG(" 最小值元件:输入不足(%d),输出=%d\n", input_count, node->value); + } +} + +static void execute_mx_mul_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + int b = (input_count >= 2) ? input_values[1] : 0; + node->value = a * b; + if (input_count >= 2) + { + PLC_DBG(" 乘法元件:%d * %d = %d\n", a, b, node->value); + } + else + { + PLC_DBG(" 乘法元件:输入不足(%d),输出=%d\n", input_count, node->value); + } +} + +static void execute_mx_div_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + int b = (input_count >= 2) ? input_values[1] : 0; + if (b == 0) + { + node->value = 0; + PLC_DBG(" 除法元件:%d / %d,除数为0,输出=0\n", a, b); + } + else + { + node->value = a / b; + PLC_DBG(" 除法元件:%d / %d = %d\n", a, b, node->value); + } +} + +static void execute_mx_abs_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + node->value = (a < 0) ? -a : a; + PLC_DBG(" 绝对值元件:|%d| = %d\n", a, node->value); + +} + +static void execute_mx_cmp_gate(LogicNode *node, int *input_values, int input_count) +{ + int a = (input_count >= 1) ? input_values[0] : 0; + int b = (input_count >= 2) ? input_values[1] : 0; + node->value = (a > b) ? 1 : 0; + if (input_count >= 2) + { + PLC_DBG(" 比较元件:%d > %d → %d\n", a, b, node->value); + } + else + { + PLC_DBG(" 比较元件:输入不足(%d),输出=%d\n", input_count, node->value); + } + +} + + /* ================================================================== * 拓扑排序 (Kahn 算法) * ================================================================== */ @@ -1463,6 +1648,15 @@ int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) case NODE_TYPE_SR_LATCH: type_name = "SR触发器"; break; case NODE_TYPE_RS_LATCH: type_name = "RS触发器"; break; case NODE_TYPE_FALLING_EDGE: type_name = "下降沿元件"; break; + case NODE_TYPE_TOGGLE: type_name = "翻转元件"; break; + case NODE_TYPE_MX_ADD: type_name = "遥测加法元件"; break; + case NODE_TYPE_MX_SUB: type_name = "遥测减法元件"; break; + case NODE_TYPE_MX_MAX: type_name = "遥测最大值元件"; break; + case NODE_TYPE_MX_MIN: type_name = "遥测最小值元件"; break; + case NODE_TYPE_MX_MUL: type_name = "遥测乘法元件"; break; + case NODE_TYPE_MX_DIV: type_name = "遥测除法元件"; break; + case NODE_TYPE_MX_ABS: type_name = "遥测绝对值元件"; break; + case NODE_TYPE_MX_CMP: type_name = "遥测比较元件"; break; default: type_name = "未知节点"; break; } PLC_DBG(" %s (类型=%d, ID=%d): 初始值=%d\n", @@ -1553,7 +1747,49 @@ int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) PLC_DBG("\n 处理RS触发器 (ID=%d)\n", node->id); input_count = collect_input_signals(graph, node, input_values, 10); execute_rs_latch(node, input_values, input_count); - break; + break; + case NODE_TYPE_MX_ADD: + PLC_DBG("\n 处理遥测加法元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_mx_add_gate(node, input_values, input_count); + break; + case NODE_TYPE_MX_SUB: + PLC_DBG("\n 处理遥测减法元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_mx_sub_gate(node, input_values, input_count); + break; + case NODE_TYPE_MX_MAX: + PLC_DBG("\n 处理遥测最大值元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_mx_max_gate(node, input_values, input_count); + break; + case NODE_TYPE_MX_MIN: + PLC_DBG("\n 处理遥测最小值元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_mx_min_gate(node, input_values, input_count); + break; + case NODE_TYPE_MX_MUL: + PLC_DBG("\n 处理遥测乘法元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_mx_mul_gate(node, input_values, input_count); + break; + case NODE_TYPE_MX_DIV: + PLC_DBG("\n 处理遥测除法元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_mx_div_gate(node, input_values, input_count); + break; + case NODE_TYPE_MX_ABS: + PLC_DBG("\n 处理遥测绝对值元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + if (input_count > 0) execute_mx_abs_gate(node, input_values, input_count); + else { PLC_DBG(" 警告:绝对值元件没有输入信号\n"); node->value = 0; } + break; + case NODE_TYPE_MX_CMP: + PLC_DBG("\n 处理遥测比较元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + if (input_count > 0) execute_mx_cmp_gate(node, input_values, input_count); + else { PLC_DBG(" 警告:比较元件没有输入信号\n"); node->value = 0; } + break; } } @@ -1600,6 +1836,15 @@ int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) case NODE_TYPE_SR_LATCH: type_name = "SR触发器"; break; case NODE_TYPE_RS_LATCH: type_name = "RS触发器"; break; case NODE_TYPE_FALLING_EDGE: type_name = "下降沿元件"; break; + case NODE_TYPE_TOGGLE: type_name = "翻转元件"; break; + case NODE_TYPE_MX_ADD: type_name = "遥测加法元件"; break; + case NODE_TYPE_MX_SUB: type_name = "遥测减法元件"; break; + case NODE_TYPE_MX_MAX: type_name = "遥测最大值元件"; break; + case NODE_TYPE_MX_MIN: type_name = "遥测最小值元件"; break; + case NODE_TYPE_MX_MUL: type_name = "遥测乘法元件"; break; + case NODE_TYPE_MX_DIV: type_name = "遥测除法元件"; break; + case NODE_TYPE_MX_ABS: type_name = "遥测绝对值元件"; break; + case NODE_TYPE_MX_CMP: type_name = "遥测比较元件"; break; default: type_name = "未知节点"; break; } PLC_DBG(" %s (ID=%d): 最终值=%d\n", type_name, node->id, node->value); @@ -1754,7 +1999,7 @@ int app_plc_init1(void *arg) int ret = 0; ret |= dc_signal_out("plc.run_cnt", "plc线程计数", DATA_TYPE_U32, &p_app->run_cnt, "plc"); - for (int i = 0; i < MAX_OUT_NODES; i++) + for (int i = 0; i < MAX_OUT_ST_NODES; i++) { char saddr[64]; char desc[64]; @@ -1763,6 +2008,15 @@ int app_plc_init1(void *arg) ret |= dc_signal_out(saddr, desc, DATA_TYPE_U8, &g_plc_st_out[i], "plc"); } + for (int i = 0; i < MAX_OUT_MX_NODES; i++) + { + char saddr[64]; + char desc[64]; + snprintf(saddr, sizeof(saddr), "plc.mx.out.%d", i); + snprintf(desc, sizeof(desc), "plc遥测输出%d", i); + ret |= dc_signal_out(saddr, desc, DATA_TYPE_F32, &g_plc_mx_out[i], "plc"); + } + if (ret != 0) { LOG_E("app_plc_init1 dc_signal_out failed"); diff --git a/src/system/libweb_server/src/ws_method.cpp b/src/system/libweb_server/src/ws_method.cpp index 351b8ca..c8488e4 100644 --- a/src/system/libweb_server/src/ws_method.cpp +++ b/src/system/libweb_server/src/ws_method.cpp @@ -509,6 +509,20 @@ void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len) sub++; } + /* 当注册 out/all 时,保存 datacenter out 信号到 XML */ + if (0 == strcmp(sub, "out") || 0 == strncmp(sub, "out ", 4) + || 0 == strcmp(sub, "all") || 0 == strncmp(sub, "all ", 4)) + { + char resolved[512]; + char proc_dir[256] = {0}; + + if (0 == func_proc_self_dir(proc_dir, sizeof(proc_dir))) + { + snprintf(resolved, sizeof(resolved), "%s/config/SYSTEM/datacenter_out.xml", proc_dir); + dc_save_out_signals_xml(resolved); + } + } + /* 构建信号元数据 JSON */ const char *dc_types[4]; int dc_type_count = 0;