/** * @file plc_debug.js * @brief PLC 调试页 — 三列布局:左(遥信输入·按钮/遥测输入·设值) | 中(信号表) | 右(遥信输出·LED/遥测输出·展示) * @details 所有条目从中间信号表选择添加,增量刷新 */ var PlcDebugStIn = []; // 遥信输入(按钮) var PlcDebugMxIn = []; // 遥测输入(设值) var PlcDebugStOut = []; // 遥信输出(LED) var PlcDebugMxOut = []; // 遥测输出(展示) Router.register('#plc_debug', function(c) { PlcDebugPage.render(c); }); var PlcDebugPage = (function() { var outSignals = []; var stInChk = {}, mxInChk = {}, stOutChk = {}, mxOutChk = {}, sigChk = {}; function getOut() { var out = SignalStore.get('out'); return (Array.isArray(out) && out.length) ? out : outSignals; } function findSig(sa) { 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; 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(); 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('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' : ''); }); PlcDebugMxOut.forEach(function(m, i) { var ve = document.getElementById('mxo-val-' + i); if (ve) ve.textContent = fmtVal(m.val); }); } function countChecked(obj) { var n = 0; for (var k in obj) if (obj.hasOwnProperty(k)) n++; return n; } function delChecked() { var todel = []; 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.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); }); 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 o = getOut(); var sigHtml = ''; if (!o.length) { sigHtml = '
' + I18n.t('plc_debug.no_sig') + '
'; } else { o.forEach(function(s, i) { var chk = !!sigChk[i]; sigHtml += '
' + '' + '#' + i + '' + '' + esc(s.saddr) + '' + (s.desc ? '' + esc(s.desc) + '' : '') + '' + fmtVal(s.val) + '' + '
'; }); } c.innerHTML = '
' /* 左列: 遥信输入(上) + 遥测输入(下) */ + '
' + 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(stInChk) + countChecked(mxInChk) + countChecked(stOutChk) + countChecked(mxOutChk); el.textContent = I18n.t('plc_debug.del_sel') + (n ? ' (' + n + ')' : ''); el.disabled = !n; } return { render: function(c) { stInChk = {}; mxInChk = {}; stOutChk = {}; mxOutChk = {}; sigChk = {}; refreshData(); c.innerHTML = '
' + '
📟 ' + I18n.t('plc_debug.title') + '' + '
' + '' + '' + '
' + '
'; renderAll(); }, _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; var outs = SignalStore.get('out'); for (var j = 0; j < outs.length; j++) if (outs[j].saddr === b.saddr) { outs[j].val = String(nv); break; } renderAll(); }, _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 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; 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); }); sigChk = {}; renderAll(); }, _refresh: function() { refreshData(); renderAll(); }, incrRefresh: function() { refreshData(); incrRefresh(); }, _delChecked: delChecked, }; })();