obcon-scada / app /include /SPC.js
chanmin0723's picture
Initial obcon SCADA deploy
e4bf523
Raw
History Blame Contribute Delete
31 kB
'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;