'use strict' /** * Copyright (c) 2017~2023, OBCon Inc. * All rights reserved. */ /** * @file * @copyright 2017~2023, OBCon Inc. * @author gye hyun james kim [pnuskgh@gmail.com] */ const moment = require('moment'); //--- 'YYYY-MM-DD HH:mm:ss.SSS ZZ' /** * SPC (Smart Pressure Controller) * Cat.M1 (TCP/IP)을 사용하여 통신 * * Interface * setSettings : 설정 정보 등록 * getSettings : 설정 정보 읽기 */ class SPC { //--- To-Do: 향후 서버에서 SPC를 동작하도록 기능 추가. (현재는 장비에서만 동작 한다.) constructor() { this._isServer = false; //--- Server/Device this._protocol = 'IECP'; //--- IECP, IECP_Mini this._iecp = null; //--- IECP, IECP_Mini this._server = null; //--- tcp_client this._deviceInfo = null; //--- 장비 기본 정보 this._networkInfo = null; //--- 장비의 통신 정보 this._actionGenerate = null; //--- 장비 데이터 생성을 위한 interval this._actionSend = null; //--- 장비 데이터 전송을 위한 interval this.settings = null; //--- 장비 설정값 this._data = null; //--- 장비에 저장하는 데이터 this._status = null; //--- 자동 운전을 위한 장비 상태 관리 this._transactionId = 0; //--- 테스트시 사용 this._isTestSend = false; this._testSendCount = 1; } set isServer(newValue) { this._isServer = newValue; } set protocol(newValue) { this._protocol = newValue; } set iecp(newValue) { this._iecp = newValue; } set server(newValue) { this._server = newValue; } set deviceInfo(newValue) { this._deviceInfo = newValue; } set networkInfo(newValue) { this._networkInfo = newValue; } getNextTransactionId() { this._transactionId = (this._transactionId + 1) % 10000; return this._transactionId; } initialize(server) { this._server = server; //--- Server (Reserved) 또는 Device if (this._isServer == false) { this.settings = this._getDefaultSettings(); this._data = this._getDefaultData(); this._status = this._getDefaultStatus(); // setInterval(this.autoControl.bind(this), 1000); //--- 1초에 한번씩 autoControl() 함수를 실행 한다. //--- 디바이스 데이터 자동 생성 this._actionGenerate = setInterval(async function() { await this._devicedataGenerate(); }.bind(this), this._getDataCycle() * 1000); //--- 디바이스 데이터를 서버로 송신 this._actionSend = setInterval(async function() { if (this._isTestSend) { if (0 < this._testSendCount) { this._testSendCount = this._testSendCount - 1; await this._devicedataSend(); } } else { await this._devicedataSend(); } }.bind(this), this._getNetworkCycle() * 1000); } setTimeout(async function() { if (this._isTestSend) { if (0 < this._testSendCount) { this._testSendCount = this._testSendCount - 1; // await this._zz_request_800(); // await this._zz_alarm_2(); // await this._zz_alarm_3(); } } else { await this._zz_request_800(); await this._zz_alarm_2(); await this._zz_alarm_3(); } }.bind(this), 10 * 60 * 1000); setTimeout(async function() { await this._zz_request_702(); await this._zz_alarm_2(); await this._zz_alarm_3(); // await this._zz_request_800(); }.bind(this), 5 * 1000); } _getDefaultSettings() { return { operationStatus: '0', //--- 운전 상태 :: 0: 정지, 1: 설정 운전, 2: 자동 운전 Pcon_datetime: moment().format('YYYYMMDD'), //--- 설정 압력 일자 (YYYYMMDD) Pcon_init: 2.0, //--- 저장된 초기 설정 압력 Pcon: 2.0, //--- 설정 압력 (저장된 설정 압력이 자동 운전에 따라 변경된 설정 압력) Pmax: 2.30, //--- 최고 압력 Pmin: 1.50, //--- 최저 압력 Pcon_p: 0.01, //--- 설정 압력 증분 Pcon_m: 0.01, //--- 설정 압력 감분 UDcon: 0.03, //--- 최소 압력차 : 계산한 설정 압력차가 UDcon을 초과하는 경우 승압/감압을 진행 한다. VDcon: 0.10, //--- 직전 압력차 : 계산한 직전 압력차가 VDcon을 초과하는 경우 승압/감압을 진행 한다. PEL1: 1.65, //--- 관말/AMI 상한 PEL2: 1.55, //--- 관말/AMI 하한 isUseMax: false, //--- 상한 사용 여부 isUseMin: false, //--- 하한 사용 여부 DPcon: 0.10, //--- 관말/AMI 압력차 : 계산한 연산 압력차가 DPcon을 초과하는 경우 승압/감압을 진행 한다. delayTime: 60, //--- 대기 시간 delayTimeUnit: '초', //--- 대기 시간 단위: 초, 분 motorStepAngle: 7.5, //--- 모터스텝각 reducer: 30, //--- 감속기 turnAngle: 15, //--- 1TURN 각도 alarmSetting001: 'T', alarmSetting002: 'T', alarmSetting003: 'T', alarmSetting004: 'F', alarmSetting005: 'F', alarmSetting006: 'F', alarmSetting007: 'F', alarmSetting008: 'F' // alarmSettings: [ //--- 알람 설정 // true, //--- 통신 누락 발생시 알람 여부 // true, //--- 자동 운전 최고/최저 초과/미만시 알람 여부 // true, //--- 정지상태에서 1분 이상 전원공급시 알림 여부 // false, //--- Reserved // false, //--- Reserved // false //--- Reserved // ] // devices: [], //--- 관말/AMI 장비 목록 }; } _getDefaultData() { return { //--- 저장하는 데이터 PEcur: 1.60, //--- 서버로부터 받은 연산 압력 (관말/AMI 압력으로 연산한 값) devicedatas: [], //--- 장비 데이터 //--- pppqqq, 알람 발생 조건은 ? : 통신 누락 발생시 알람 여부 //--- pppqqq, 알람 발생 조건은 ? : 자동 운전 최고/최저 초과/미만시 알람 여부 //--- pppqqq, 알람 발생 조건은 ? : 정지상태에서 1분 이상 전원공급시 알림 여부 alarms: [], //--- 발생한 알람 //--- To-Do: Excel에서 업로드/다운로드 한다. //--- pppqqq, Pupl 값은 언제 사용하는가 ? (자동 계산시 또는 365일 설정값 사용시) //--- To-Do: 2월 29일은 2월 28일과 동일한 설정 압력을 사용 한다. Pupls: [ //--- 연간(365일) 설정 압력 // { // date: 'YYYY-MM-DD', //--- 설정일 // Pupl: 0.2 //--- 설정 압력 // } ], //--- 현재 압력을 실시간(2초)으로 수집 //--- 서버로 2분 ~ 60분 간격으로 전송 PTCurs: [ //--- 장비로부터 주기적으로 수집된 압력 // { // PTcur_datetime: 'YYYY-MM-DD HH:mm:ss', //--- 발생 일시 // PTcur: 0.2 //--- 장비 압력 // } ], //--- SPC 장비에서 서버로 요청하는 경우에, 서버에서 값을 반환 한다. PEcurs: [ // { // datetime: 'YYYY-MM-DD HH:mm:ss', //--- Turn 시작일시 // devicedatas: [], //--- 장비 데이터 // PEcur: 0.2, //--- 관말/AMI 압력 최소값 // PTcur: 0.2 //--- 현재 압력 // } ], }; } _getDefaultStatus() { //--- To-Do: delayTime마다 한번씩 실행 한다. return { //--- 상태 firstConnectionDatetime: null, //--- 최초 장비 연동 일시, 'YYYY-MM-DD HH:mm:ss' (서버에서 관리) PTb_datetime: moment().format('YYYYMMDDHHmmss'), //--- 직전 압력 일자 PTb: 2.0, //--- 직전 압력 PTcur_datetime: moment().format('YYYYMMDDHHmmss'), //--- 현재 압력 일자 PTcur: 2.0, //--- 현재 압력 operation: '1', //--- 제어 활동: 1: 활동 없음, 2: 승압중, 3: 감압중 remainTimeTo: null, //--- 제어 활동의 종료 시간 (moment 값으로 저장) // remainTime: 0, //--- 잔여 대기 시간 // remainTimeUnit: '초', //--- 잔여 대기 시간 단위: 초, 분 }; } _getRemainTime() { if (this._status.remainTimeTo == null) { return 0; } const duration = moment.duration(this._status.remainTimeTo.diff(moment())); return (duration <= 0) ? 0:duration.seconds(); } _getCycle(cycle, unit) { switch (unit) { case '시': cycle = cycle * 60 * 60; break; case '분': cycle = cycle * 60; break; } return cycle; } _getDataCycle() { return this._getCycle(this._networkInfo.dataCycle, this._networkInfo.dataCycleUnit); } _getNetworkCycle() { return this._getCycle(this._networkInfo.networkCycle, this._networkInfo.networkCycleUnit); } async _devicedataGenerate() { const CycleUnit = { '초': '1', '분': '2', '시': '3' } try { //--- 현재 압력 갱신 const inpDevice = this._getInputFromDevice(); console.log(inpDevice); this._status.PTcur_datetime = inpDevice.PTcur_datetime; this._status.PTcur = inpDevice.PTcur; const devicedata = { dataCycle: this._networkInfo.dataCycle, dataCycleUnit: CycleUnit[this._networkInfo.dataCycleUnit], networkCycle: this._networkInfo.networkCycle, networkCycleUnit: CycleUnit[this._networkInfo.networkCycleUnit], statusDatetime: this._status.PTcur_datetime, operationStatus: this.settings.operationStatus, PTcur: this._status.PTcur, PTb_datetime: this._status.PTb_datetime, PTb: this._status.PTb, Pcon_datetime: this.settings.Pcon_datetime, Pcon_init: this.settings.Pcon_init, Pcon: this.settings.Pcon, PEcur: this._data.PEcur, operation: this._status.operation, remainTimeMinutes: Math.floor(this._getRemainTime() / 60), remainTimeSeconds: this._getRemainTime() % 60, statusCode: 77 }; this._data.devicedatas.push(devicedata); console.log('장비 데이터 생성', devicedata); } catch(ex) { console.error('Exception', ex); } } async _devicedataSend() { try { const frame = { transactionId: this.getNextTransactionId(), requestType: '1', destination: 'S', deviceId: this._deviceInfo.deviceId, functionCode: '700', pageId: '00', address: 0, dataLength: 17, }; while (0 < this._data.devicedatas.length) { const devicedata = this._data.devicedatas.shift(); this._iecp.data = await this._iecp.getDefaultPacket(this._iecp, frame, devicedata); console.log('전송할 장비 데이터', this._iecp.data); const socketData = await this._iecp.dataToSocketData(); if (socketData != null) { console.log('장비 데이터 전송', socketData.toString('UTF-8')); this._server._socket.write(socketData); } else { logger.error(`SocketData를 생성하지 못하였습니다 : ${JSON.stringify(this._iecp.data)}`); } } } catch(ex) { console.error('Exception', ex); } } async _zz_request_702() { try { const frame = { transactionId: this.getNextTransactionId(), requestType: '1', destination: 'S', deviceId: this._deviceInfo.deviceId, functionCode: '702', pageId: '00', address: 16, dataLength: 1, }; this._iecp.data = await this._iecp.getDefaultPacket(this._iecp, frame, { statusCode: 55 }); console.log('상태 코드 전송', this._iecp.data); const socketData = await this._iecp.dataToSocketData(); if (socketData != null) { console.log('상태 코드 전송', socketData.toString('UTF-8')); this._server._socket.write(socketData); } else { logger.error(`SocketData를 생성하지 못하였습니다 : ${JSON.stringify(this._iecp.data)}`); } } catch(ex) { console.error('Exception', ex); } } async _zz_request_800() { try { const frame = { transactionId: this.getNextTransactionId(), requestType: '1', destination: 'S', deviceId: this._deviceInfo.deviceId, functionCode: '800', pageId: '00', address: 0, dataLength: 0, }; this._iecp.data = await this._iecp.getDefaultPacket(this._iecp, frame, { }); console.log('연산 압력 요청', this._iecp.data); const socketData = await this._iecp.dataToSocketData(); if (socketData != null) { console.log('연산 압력 요청 전송', socketData.toString('UTF-8')); this._server._socket.write(socketData); } else { logger.error(`SocketData를 생성하지 못하였습니다 : ${JSON.stringify(this._iecp.data)}`); } } catch(ex) { console.error('Exception', ex); } } async _zz_alarm_2() { try { const frame = { transactionId: this.getNextTransactionId(), requestType: '1', destination: 'S', deviceId: this._deviceInfo.deviceId, functionCode: '701', pageId: '01', address: 0, dataLength: 3, }; const alarm2 = { alarmDatetime: moment().format('YYYYMMDDHHmmss'), alarmType: '1', alarmCode: 1, //--- 1. 최고 압력 초과 // alarmMessage: '최고 압력 초과' //--- 2. 최저 압력 미만 // alarmMessage: '12345678901234567890123456789012345678901234567890123456789012345678901234567890' //--- 2. 최저 압력 미만 }; this._iecp.data = await this._iecp.getDefaultPacket(this._iecp, frame, alarm2); console.log('전송할 알람 2', this._iecp.data); const socketData = await this._iecp.dataToSocketData(); if (socketData != null) { console.log('알람 2 전송', socketData.toString('UTF-8')); this._server._socket.write(socketData); } else { logger.error(`SocketData를 생성하지 못하였습니다 : ${JSON.stringify(this._iecp.data)}`); } } catch(ex) { console.error('Exception', ex); } } async _zz_alarm_3() { try { const frame = { transactionId: this.getNextTransactionId(), requestType: '1', destination: 'S', deviceId: this._deviceInfo.deviceId, functionCode: '701', pageId: '01', address: 0, dataLength: 3, }; const alarm2 = { alarmDatetime: moment().format('YYYYMMDDHHmmss'), alarmType: '2', alarmCode: 1, //--- 1. 최고 압력 초과 // alarmMessage: '전원 공급' //--- 2. 최저 압력 미만 }; this._iecp.data = await this._iecp.getDefaultPacket(this._iecp, frame, alarm2); console.log('전송할 알람 3', this._iecp.data); const socketData = await this._iecp.dataToSocketData(); if (socketData != null) { console.log('알람 3 전송', socketData.toString('UTF-8')); this._server._socket.write(socketData); } else { logger.error(`SocketData를 생성하지 못하였습니다 : ${JSON.stringify(this._iecp.data)}`); } } catch(ex) { console.error('Exception', ex); } } _getFloatWithLow(value, low = 100) { return Math.floor(value * low) / low; } _randomFloat(value, min, max, low=-1, high=1) { value = value + utils.randomFloat(low, high); if (value < min) { value = min; } else if (max < value) { value = max; } return value; } //--- 현재 압력을 실시간(2초)으로 수집 //--- 서버로 2분 ~ 60분 간격으로 전송 _getInputFromDevice() { const PTcur = this._randomFloat(this._status.PTcur, this.settings.Pmin - 0.5, this.settings.Pmax + 0.5); return { PTcur_datetime: moment().format('YYYYMMDDHHmmss'), //--- 발생 일시 PTcur: this._getFloatWithLow(PTcur, 100) //--- 장비 압력 }; } // _getInputFromServer() { // //--- To-DO: 1. 장비 목록으로 장비 데이터를 가져 온다. // //--- To-DO: 2. 장비 데이터로 관말/AMI 압력 최소값을 계산 한다. // return { // // devices: this.settings.devices, //--- 장비 목록 // code: 0, // message: 'ok', // devicedatas: [], //--- 장비 데이터 // PEcur: 0.2 //--- 관말/AMI 압력 최소값 // }; // } // //--- 설정 등록 // async setSettings(settings) { // } // //--- 설정 반환 // async getSettings() { // return this.settings; // } // //--- 장비 데이터 읽기 // async readData() { // } // //--- 장비 데이터 저장 // async saveData() { // } // //--- 연간(365일) 설정 압력을 저장 한다. // setPupls(data) { // this._data.Pupls = data; // } // //--- 운전 버튼 선택시 실행 // start() { // } // //--- 정지 버튼 선택시 실행 // stop() { // //--- 환경 설정 등은 정지시에만 설정 한다. // } // //--- 자동 제어 : 운전중일때만 실행 한다. // autoControl() { // const result = this._autoControl(); // if (result.code == 101) { // this._delayTime(); // if (result.PEyn) { // const yyyymmdd = this._status.PTcur_datetime.substring(0, 8); // if (yyyymmdd != moment().format('YYYYMMDD')) { // const ppp1 = '전일 기존 설정 압력'; //--- pppqqq ??? // const ppp2 = '전일 현재 설정 압력'; //--- pppqqq ??? // this.settings.Pcon_datetime = yyyymmdd.replace(/-/g, ''); // this.settings.Pcon = this.settings.Pcon + (ppp1 - ppp2); // } // } // } // } // _autoControl() { // //--- 1-1. 장비로부터 데이터를 가져 온다. // const inpDevice = this._getInputFromDevice(); // inpDevice.PTb = this._status.PTcur; //--- 직전 압력 : pppqqq, 직전 압력을 어떻게 계산하는지 확인 필요 // //--- 1-2. 서버로부터 데이터를 가져 온다. // const inpServer = this._getInputFromServer(); // if (inpServer.code != '0') { // const alarmSetting001 = this.settings.alarmSettings[0]; //--- 통신 누락 발생시 알람 여부 // if (alarmSetting001) { // this._data.alarms.push({ //--- 발생한 알람 // alarmDatetime: moment().format('YYYY-MM-DD HH:mm:ss'), //--- 알람 발생일시 // alarmType: 1, //--- 알람 타입 // alarmCode: 0, //--- 알람 코드 // alarmMessage: '장비데이터 수신시 통신 누락이 발생' //--- 알람 메시지 // }); // } // return { code: 504, message: '장비데이터 수신시 통신 누락이 발생 하였습니다.' }; // } // this._data.PEcurs.push({ // datetime: moment().format('YYYY-MM-DD HH:mm:ss'), //--- Turn 시작일시 // devicedatas: inpServer.devicedatas, // PEcur: inpServer.PEcur, // PTcur: inpDevice.PTcur // }); // //--- pppqqq 2. 상태 정보를 갱신 한다. // this._status.today = moment().format('YYYY-MM-DD'); // this._status.PTcur_datetime = inpDevice.PTcur_datetime; //--- 장비 압력 발생일시 // this._status.PTcur = inpDevice.PTcur; //--- 장비 압력 // //--- pppqqq 3. 수집한 데이터를 저장 한다. // this._data.PTCurs.push(inpDevice); // this._data.devicedatas.push(...inpServer.devicedatas); // //--- To-Do: 연간(365일) 설정 압력을 업로드할 때 등록 일자와 등록 설정값을 검사 한다. // if (this._data.Pupls.length < 365) { // return { code: 501, message: '연간(365일) 설정 압력을 등록 하세요.' }; // } // //--- To-Do: 운전 시작시, 운전에 필요한 모든 값이 설정되어 있는지 검사 한다. // // if (this.settings.isAutoOperation == false) { // if (this.settings.operationStatus == '0') { // return { code: 502, message: '운전중이 아닙니다.' }; // } // const PEyn = this.settings.isUseMin || this.settings.isUseMax; //--- 관말/AMI 사용 여부 // if (PEyn) { // //--- PEcur (관말/AMI 압력 최소값)으로 Pcon (설정 압력) 값 조정 // //--- pppqqq, SPC 장비의 현재 압력과 관말/AMI에서 계산한 압력의 차가이 크지 않은 경우, 설정 압력에 상관없이 동작하지 않는 경우가 발생할 수 있다. // const DP = inpServer.PEcur - inpDevice.PTcur; //--- 계산한 연산 압력차 : 계산한 압력 - 현재 압력 // if (Math.abs(DP) <= this.settings.DPcon) { //--- DPcon (관말/AMI 압력차) // return { code: 101, message: '대기 합니다. 1', DPcur: 0, PEyn: PEyn }; // } // if (inpDevice.PTcur <= inpServer.PEcur) { //--- 현재 압력이 관말/AMI 압력 최소값보다 작은 경우 대기 한다. // return { code: 101, message: '대기 합니다. 2', DPcur: 0, PEyn: PEyn }; // } // //--- 관말/AMI 상한을 초과하는 경우, 설정 압력을 감소 시킨다. // //--- pppqqq 무한 감소를 방지하는 방안은 ? // if (this.settings.PEL1 < inpServer.PEcur) { // this.settings.Pcon = this.settings.Pcon - this.settings.Pcon_m; // } // //--- 관말/AMI 하만 미만인 경우, 설정 압력을 증가 시킨다. // //--- pppqqq 무한 증가를 방지하는 방안은 ? // if (inpServer.PEcur < this.settings.PEL2) { // this.settings.Pcon = this.settings.Pcon + this.settings.Pcon_p; // } // } else { // //--- 설정한 값으로 자동 운전 // //--- pppqqq : 연간 설정 압력을 사용하는 곳이 없음 // //--- pppqqq PTb의 의미는 ? 365일 설정에서 전일 설정인가 아니면 실시간 계산되는 직전 압력인가 ? // const Dpt = inpDevice.PTcur - inpDevice.PTb; //--- 계산한 직전 압력차: 현재 압력 - 직전 압력 // if (Math.abs(Dpt) <= this.settings.VDcon) { // //--- 설정한 직전 압력차 보다 계산한 진전 압력차가 적은 경우, 대기 한다. // return { code: 101, message: '대기 합니다. 3', DPcur: 0, PEyn: PEyn }; // } // } // //--- pppqqq 의미가 이상함? // if ((inpDevice.PTcur < this.settings.Pmin) || // (this.settings.Pmax < inpDevice.PTcur)) { // const alarmSetting002 = this.settings.alarmSettings[1]; //--- 자동 운전 최고/최저 초과/미만시 알람 여부 // if (alarmSetting002) { // if (inpDevice.PTcur < this.settings.Pmin) { // this._data.alarms.push({ //--- 발생한 알람 // alarmDatetime: moment().format('YYYY-MM-DD HH:mm:ss'), //--- 알람 발생일시 // alarmType: 2, //--- 알람 타입 // alarmCode: 1, //--- 알람 코드 // alarmMessage: '자동 운전 최저 미만' //--- 알람 메시지 // }); // } // if (this.settings.Pmax < inpDevice.PTcur) { // this._data.alarms.push({ //--- 발생한 알람 // alarmDatetime: moment().format('YYYY-MM-DD HH:mm:ss'), //--- 알람 발생일시 // alarmType: 2, //--- 알람 타입 // alarmCode: 2, //--- 알람 코드 // alarmMessage: '자동 운전 최고 초과' //--- 알람 메시지 // }); // } // } // return { code: 101, message: '대기 합니다. 4', DPcur: 0, PEyn: PEyn }; // } // //--- pppqqq, 365일 설정값을 사용하는 경우, Pcon은 언제 변경이 되는가 ? // const DPcur = this.settings.Pcon - inpDevice.PTcur; //--- 계산한 설정 압력차 // if (Math.abs(DPcur) <= this.settings.UDcon) { // return { code: 101, message: '대기 합니다. 5', DPcur: 0, PEyn: PEyn }; // } // if (0 <= DPcur) { // console.log('승압중'); // } else { // console.log('감압중'); // } // return { // code: 0, // message: 'ok', // DPcur: DPcur //--- 압력 변동 (0. 변동 없음, 양수. 승압, 음수, 감압) // }; // } // _delayTime() { // //--- 대기시간 동안 대기 한다. // } } module.exports = SPC;