← Files EW EDC Protocol MapperARCHIVED FILE
skills/ew-edc-protocol-point-wizard/references/core.cjs
11.7 KB · Oct 4, 2026 · 12:34 UTC
(function(root,f){const a=f();if(typeof module==='object'&&module.exports)module.exports=a;else root.EWModbus=a;})(typeof globalThis!=='undefined'?globalThis:this,function(){'use strict';
const SCHEMA='ew-edc.modbus-tcp/v1',columns=['設備名稱','IP位址','通訊埠','站號','品牌','型號','通道名稱','物理量','單位','功能碼','協定起始位址','手冊位址','位址基準','資料型態','暫存器數','位元組順序','採樣秒數','倍率','偏移量','來源','原始值範例','工程值範例','已確認'];
const sizes={int16:1,uint16:1,int32:2,uint32:2,float32:2,float64:4},orders={1:['AB','BA'],2:['ABCD','BADC','CDAB','DCBA'],4:['ABCDEFGH','HGFEDCBA','BADCFEHG','GHEFCDAB']};
function fail(s){throw Error(s);}function text(v,l,max=160){if(typeof v!=='string'||!v.trim()||v.length>max||/[\x00-\x1f]/.test(v))fail(l+' 不可空白或含控制字元');return v.trim();}function num(v,l,min,max,int=false){if(typeof v!=='number'||!Number.isFinite(v)||v<min||v>max||(int&&!Number.isInteger(v)))fail(l+' 數值不合法');return v;}
function validate(p){if(!p||p.schema!==SCHEMA||!Array.isArray(p.devices)||!p.devices.length||p.devices.length>100)fail('部署檔格式或設備數不合法');const names=new Set(),ends=new Set();let count=0;return {schema:SCHEMA,devices:p.devices.map(d=>{
const name=text(d.name,'設備名稱',64);if(names.has(name))fail('設備名稱重複:'+name);names.add(name);const ip=text(d.ip,'IP 位址',15);if(!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)||ip.split('.').some(x=>Number(x)>255||String(Number(x))!==x)||['0.0.0.0','255.255.255.255'].includes(ip)||Number(ip.split('.')[0])>=224)fail('請填明確 IPv4 設備位址');const port=num(d.port,'通訊埠',1,65535,true),unitId=num(d.unitId,'站號',0,255,true),key=[ip,port,unitId].join(':');if(ends.has(key))fail('設備端點重複');ends.add(key);if(!Array.isArray(d.channels)||!d.channels.length)fail(name+' 沒有通道');const cn=new Set();
const channels=d.channels.map(c=>{if(++count>1000)fail('每批最多 1000 通道');const n=text(c.name,'通道名稱',64);if(cn.has(n))fail('通道名稱重複');cn.add(n);const functionCode=num(c.functionCode,'功能碼',3,4,true),address=num(c.address,'協定位址',0,65535,true),dataType=text(c.dataType,'資料型態'),registerCount=sizes[dataType];if(c.addressBasis!=='protocol-zero-based')fail(n+' 必須確認零起算協定位址');if(!registerCount||c.registerCount!==registerCount||address+registerCount>65536)fail(n+' 型態、暫存器數或位址範圍不一致');const byteOrder=text(c.byteOrder,'位元組順序');if(!orders[registerCount].includes(byteOrder))fail(n+' 位元組順序不符');const pollSeconds=num(c.pollSeconds,'採樣秒數',1,86400,true),slope=num(c.slope,'倍率',-1e12,1e12),offset=num(c.offset,'偏移量',-1e12,1e12),exampleRaw=num(c.exampleRaw,'原始值範例',-1e100,1e100),exampleEngineering=num(c.exampleEngineering,'工程值範例',-1e100,1e100);if(Math.abs(exampleRaw*slope+offset-exampleEngineering)>1e-8*Math.max(1,Math.abs(exampleEngineering)))fail(n+' 工程值範例不符合 y=倍率*x+偏移量');if(c.confirmed!==true)fail(n+' 協議資料未確認');return {name:n,quantity:text(c.quantity,'物理量'),unit:text(c.unit,'單位(無單位填 1)'),functionCode,address,manualAddress:text(String(c.manualAddress??''),'手冊位址'),addressBasis:c.addressBasis,dataType,registerCount,byteOrder,pollSeconds,slope,offset,source:text(c.source,'來源頁碼或確認紀錄',500),exampleRaw,exampleEngineering,confirmed:true};});return {name,ip,port,unitId,brand:typeof d.brand==='string'?d.brand.trim():'',model:typeof d.model==='string'?d.model.trim():'',channels};})};}
function numeric(v,l){if(v===''||v===null||v===undefined||typeof v==='boolean')fail(l+' 不可空白');const n=Number(v);if(!Number.isFinite(n))fail(l+' 必須為數字');return n;}
function fromRows(rows){if(!rows.length)fail('點表為空');const header=rows[0].map(String);if(new Set(header).size!==header.length)fail('欄位重複');for(const c of columns)if(!header.includes(c))fail('缺少欄位:'+c);const group=new Map();for(let i=1;i<rows.length;i++){const row=rows[i];if(row.every(v=>v===null||v===undefined||v===''))continue;const get=k=>row[header.indexOf(k)]??'',n=k=>numeric(get(k),'第 '+(i+1)+' 列 '+k),name=String(get('設備名稱'));const d={name,ip:String(get('IP位址')),port:n('通訊埠'),unitId:n('站號'),brand:String(get('品牌')),model:String(get('型號'))};if(!group.has(name))group.set(name,{...d,channels:[]});else for(const k of ['ip','port','unitId','brand','model'])if(group.get(name)[k]!==d[k])fail(name+' 設備欄位不一致');group.get(name).channels.push({name:String(get('通道名稱')),quantity:String(get('物理量')),unit:String(get('單位')),functionCode:n('功能碼'),address:n('協定起始位址'),manualAddress:String(get('手冊位址')),addressBasis:String(get('位址基準')),dataType:String(get('資料型態')),registerCount:n('暫存器數'),byteOrder:String(get('位元組順序')),pollSeconds:n('採樣秒數'),slope:n('倍率'),offset:n('偏移量'),source:String(get('來源')),exampleRaw:n('原始值範例'),exampleEngineering:n('工程值範例'),confirmed:['是','true',true].includes(get('已確認'))});}return validate({schema:SCHEMA,devices:[...group.values()]});}
function toRows(p){const rows=[columns];for(const d of p.devices)for(const c of d.channels)rows.push([d.name,d.ip,d.port,d.unitId,d.brand,d.model,c.name,c.quantity,c.unit,c.functionCode,c.address,c.manualAddress,c.addressBasis,c.dataType,c.registerCount,c.byteOrder,c.pollSeconds,c.slope,c.offset,c.source,c.exampleRaw,c.exampleEngineering,c.confirmed?'是':'否']);return rows;}
const sensorFields=[['unitId','11','tcpModbusAddr'],['ip','12','tcpModbusIPAddr'],['port','13','tcpModbusPort']],channelFields=[['address','11','serialModbusChannelStartAddr'],['functionCode','12','serialModbusChannelRegisterType'],['dataType','13','serialModbusChannelDataType'],['byteOrder','14','tcpModbusChannelDataOrder'],['pollSeconds','15','tcpModbusChannelReadIntervalTime'],['slope','16','tcpModbusChannelSlope'],['offset','17','serialModbusChannelReadIntervalTime']];
function cfg(list,id,inst){if(!Array.isArray(list))fail('EDC 未回傳設定陣列');const a=list.filter(c=>String(c.cfgID)===id);if(a.length!==1||a[0].InstID!==inst)fail('EDC 版本設定介面規格不相容:'+id+'/'+inst);return a[0];}
function desired(field,v,c){if(!['functionCode','dataType','byteOrder'].includes(field))return String(v);const options=typeof c.cfgItemList==='string'?c.cfgItemList.split(',').filter(Boolean):[];let re;if(field==='functionCode')re=new RegExp('(?:0x0'+v+'|^0?'+v+'(?:\\b|$))','i');else if(field==='dataType')re=new RegExp('(?:^|[^a-z0-9_])'+({int16:'int16_t',uint16:'uint16_t',int32:'int32_t',uint32:'uint32_t',float32:'float',float64:'double'}[v])+'(?:$|[^a-z0-9_])','i');else re=new RegExp('^'+v+'$');const found=options.filter(s=>re.test(s.trim()));if(found.length!==1)fail('EDC 未提供唯一的 '+field+' 選項');return found[0];}
function sensorName(s){const c=Array.isArray(s.sensorCfg)&&s.sensorCfg.find(x=>String(x.cfgID)==='10');return c?String(c.val):String(s.sensorNickName||s.sensorName||'');}
function checkDuplicates(p,list){if(!Array.isArray(list))fail('設備清單格式未知');for(const d of p.devices)for(const s of list){if(sensorName(s)===d.name)fail('EDC 已有同名設備:'+d.name);if(!Array.isArray(s.sensorCfg))fail('EDC 既有設備缺少設定,請先核對版本');for(const [,id,inst] of sensorFields)cfg(s.sensorCfg,id,inst);const val=id=>{const c=Array.isArray(s.sensorCfg)&&s.sensorCfg.find(x=>String(x.cfgID)===id);return c?String(c.val):null;};if(val('12')===d.ip&&val('13')===String(d.port)&&val('11')===String(d.unitId))fail('EDC 已有同 IP/通訊埠/站號設備');}}
function equal(a,b){return String(a)===String(b)||(String(a??'').trim()!==''&&String(b).trim()!==''&&Number.isFinite(Number(a))&&Number.isFinite(Number(b))&&Number(a)===Number(b));}
async function deploy(p,api,journal,progress=()=>{}){p=validate(p);if(journal.attempted)fail('此批已嘗試部署,請先核對結果,不可直接重送');Object.assign(journal,{attempted:true,schema:SCHEMA,startedAt:new Date().toISOString(),status:'部署中',devices:[],approvedPlan:p});const read=()=>api('getModbusTcpSensors',''),sensor=async uid=>{const list=await read();if(!Array.isArray(list))fail('設備清單格式未知');const a=list.filter(s=>String(s.uid)===String(uid));if(a.length!==1)fail('無法唯一讀回設備');return a[0];};try{const allBefore=await api('getAllSensorList','');if(!Array.isArray(allBefore))fail('物聯網感測器清單格式未知,停止部署');for(const d of p.devices)if(allBefore.some(s=>sensorName(s)===d.name))fail('物聯網管理已有同名感測器:'+d.name);checkDuplicates(p,await read());for(const d of p.devices){checkDuplicates({devices:[d]},await read());const rec={name:d.name,ip:d.ip,port:d.port,unitId:d.unitId,brand:d.brand,model:d.model,status:'新增設備中',channels:[]};journal.devices.push(rec);rec.pendingOperation='addModbusTcpSensor';progress(journal);const uid=await api('addModbusTcpSensor',{sensorName:d.name,addr:d.unitId});if(typeof uid!=='string'||!uid.trim())fail('新增設備 ID 型態未知,請核對清單');rec.suid=uid;delete rec.pendingOperation;let s=await sensor(uid);if(sensorName(s)!==d.name)fail('設備名稱讀回不一致');const set=sensorFields.map(([f,id,inst])=>({id,wanted:desired(f,d[f],cfg(s.sensorCfg,id,inst))}));for(const {id,wanted} of set){rec.pendingOperation='changeSensorCfgValue:'+id;await api('changeSensorCfgValue',{suid:uid,cfgid:id,newValue:wanted});s=await sensor(uid);if(!equal(s.sensorCfg.find(x=>String(x.cfgID)===id)?.val,wanted))fail('設備設定讀回不一致:'+id);delete rec.pendingOperation;}for(const c of d.channels){const cr={name:c.name,status:'新增通道中',quantity:c.quantity,unit:c.unit,source:c.source};rec.channels.push(cr);rec.pendingOperation='addModbusTcpChannel';progress(journal);const cuid=await api('addModbusTcpChannel',{suid:uid,channelName:c.name});if(typeof cuid!=='string'||!/^\d+$/.test(cuid))fail('新增通道 ID 型態未知,請核對清單');cr.cuid=cuid;delete rec.pendingOperation;s=await sensor(uid);let ch=Array.isArray(s.channelList)&&s.channelList.find(x=>String(x.cuid)===cuid);if(!ch)fail('無法讀回新增通道');const nameEntry=Array.isArray(ch.channelCfg)&&ch.channelCfg.find(x=>String(x.cfgID)==='10');if(!nameEntry||String(nameEntry.val)!==c.name)fail('通道名稱讀回不一致');const settings=channelFields.map(([f,id,inst])=>({id,wanted:desired(f,c[f],cfg(ch.channelCfg,id,inst))}));for(const {id,wanted} of settings){rec.pendingOperation='changeChannelCfgValue:'+id;await api('changeChannelCfgValue',{suid:uid,cuid,cfgid:id,newValue:wanted});s=await sensor(uid);ch=s.channelList.find(x=>String(x.cuid)===cuid);if(!equal(ch?.channelCfg?.find(x=>String(x.cfgID)===id)?.val,wanted))fail('通道設定讀回不一致:'+id);delete rec.pendingOperation;}cr.status='設定已讀回;資料擷取待驗證';progress(journal);}const all=await api('getAllSensorList','');if(!Array.isArray(all))fail('物聯網感測器清單格式未知');const registered=all.filter(x=>String(x.uid)===uid);if(registered.length!==1||!Array.isArray(registered[0].channelList)||rec.channels.some(c=>!registered[0].channelList.some(x=>String(x.cuid)===c.cuid)))fail('感測器或通道未列入物聯網管理清單,請核對已建立項目');rec.iotListVerified=true;rec.status='已列入物聯網感測器清單;設定已讀回';}journal.status='設定部署完成;資料擷取待驗證';}catch(e){journal.status='停止,請核對部分完成項目';journal.error=e.message;progress(journal);throw e;}finally{journal.finishedAt=new Date().toISOString();progress(journal);}return journal;}
return {SCHEMA,columns,validate,fromRows,toRows,deploy,checkDuplicates,sensorFields,channelFields};});
SHA-256: 177e14f340c9b85d83c6c024b638a8ced5931a640e4527c74c2350d9cd4f8e84